
之前我们在上篇完成了 Unity 的基础环境搭建和编辑器操作认识了场景、游戏物体GameObject和组件Component的基本概念。很多朋友反馈说跟着文章把 Cube 拖进场景后接下来就不知道该怎么让游戏真正“动”起来。这篇我们继续深入围绕 C# 脚本、角色控制、物理系统和 AI 寻路展开最后把工程打包到 Windows、macOS 和 Android 平台完成一个能跑的第三人称角色 demo。如果你还没有完成上篇的环境准备建议先回头把 Unity Hub 和编辑器装好再来跟本文的节奏走。本文适合刚学完 Unity 基础操作、准备进入实战阶段的新手也适合已经有其他编程基础、想快速了解 Unity 脚本与物理配合方式的开发者。1. 核心概念Unity 的脚本、物理与 AI 分别解决什么问题很多初学者在接触 Unity 时最迷茫的不是某个 API 怎么调用而是不理解这些功能在整个游戏开发流程里的位置。这里我们先做一个整体梳理把 Unity 中几个核心模块的职责分清楚。1.1 C# 脚本在 Unity 中的角色Unity 的编辑器本身只负责搭建场景、摆放物体、调整参数但要实现“按 W 键往前走”“碰到敌人扣血”“怪物自动追踪玩家”这些动态逻辑必须通过脚本来驱动。在 Unity 中C# 脚本本质上是一个继承自MonoBehaviour的类。MonoBehaviour是 Unity 提供的基类它让脚本可以挂载到 GameObject 上并接收 Unity 引擎在特定时机发送的生命周期回调。最常见的回调包括Awake()对象被创建时调用适合做初始化早于Start。Start()脚本启用后、第一帧更新前调用适合获取组件引用。Update()每帧调用一次适合处理持续变化的逻辑例如移动、检测输入。FixedUpdate()以固定时间步长调用适合处理物理相关逻辑例如施加力、刚体移动。LateUpdate()在所有Update执行完后调用适合相机跟随等需要滞后一帧的逻辑。理解这些回调的区别非常重要。新手最容易犯的错误是把移动代码写在Update里操作刚体导致物理表现不稳定。后面实战环节会专门处理这个问题。1.2 Unity 物理系统的组成Unity 的物理系统分为 3D 和 2D 两套分别使用Rigidbody刚体与Collider碰撞器等组件。3D 物理默认基于 PhysX 引擎2D 物理基于 Box2D。两者 API 名称很接近但使用时要区分命名空间例如 3D 是Rigidbody2D 是Rigidbody2D。物理系统的核心作用是让物体遵循重力、碰撞、摩擦等规则。比如角色从高处跳下会下落碰到墙壁会被挡住子弹命中敌人会触发伤害这些都不需要手动写复杂的数学计算而是通过物理引擎自动模拟。需要特别注意的是“物理模拟”和“动画位移”是两个不同的概念。如果物体带有Rigidbody建议通过AddForce或velocity来改变运动状态而不是直接修改transform.position否则容易与物理引擎的模拟产生冲突。1.3 Unity AI 的基本范畴Unity 中的 AI 通常指游戏内非玩家角色NPC的决策与移动逻辑常见形式包括敌人朝玩家方向移动。NPC 按照固定路径巡逻。敌人发现玩家后进入攻击状态。怪物在迷宫中自动寻找可达路径。Unity 提供了一套名为 NavMesh导航网格的寻路系统。它先把场景中可行走的区域烘焙成网格然后让带有NavMeshAgent组件的物体沿着网格计算出的路径移动避开障碍物。这种方式比直接“朝目标点移动”要高效得多因为寻路算法会自动考虑墙壁、台阶和动态障碍物。也就是说AI 在 Unity 中并不是一个神秘的大模型而是一套可组合的“决策逻辑 寻路能力”。我们可以用状态机例如巡逻、追击、攻击三种状态配合 NavMeshAgent 实现一个非常完整的敌人 AI。1.4 三者的协作关系从架构角度看C# 脚本负责逻辑控制物理系统负责真实世界规则模拟AI 系统负责非玩家角色的行为决策。三者并不是独立的而是互相配合例如一个射击游戏的敌人AI 脚本判定玩家进入攻击范围。敌人通过 NavMeshAgent 移动到合适位置。开火时生成子弹。子弹通过 Rigidbody 向前飞行。子弹的 Collider 碰到玩家 Collider 后触发伤害逻辑。这条链路里脚本负责串联物理负责碰撞检测AI 负责决策。理解这个协作关系后再看下面每个环节的具体实现会轻松很多。2. 环境准备与项目创建本文的实战部分需要你已经准备好 Unity 编辑器。由于 Unity 版本迭代频繁不同版本在 UI 布局和部分 API 上可能略有差异本文示例以 Unity 2021.3 LTS 或 Unity 2022.3 LTS 的稳定环境为例这些细节需要根据你自己的编辑器版本微调但核心思路是一致的。2.1 创建 3D 项目打开 Unity Hub点击“新项目”选择“3D Core”模板填写项目名称例如UnityActionDemo选择保存路径后点击创建。等待编辑器加载完成后你会看到一个包含主相机Main Camera和定向光Directional Light的空场景。建议先执行一次CtrlS保存场景命名为Main。2.2 准备基础场景为了让后续物理和 AI 演示有合适的场地我们先做一个简单的“地面 障碍物”场景在 Hierarchy 窗口点击右键选择3D Object - Plane将 Plane 作为地面。将 Plane 的 Scale 设置为(10, 1, 10)让地面大一些。再创建几个 Cube调整位置和缩放作为障碍物或平台。给地面和障碍物添加不同颜色的材质Material方便观察。这里有一个很容易被忽略的点Plane 默认自带Mesh ColliderCube 自带Box Collider所以它们已经具备碰撞检测能力。如果后续想让物体穿过它们反而需要主动关掉 Collider。2.3 创建主角与敌人接下来创建两个关键角色主角创建一个 Capsule胶囊体命名为Player调整到地面正上方添加一个相机作为子物体来模拟第三人称视角。敌人创建一个 Cube 或 Capsule命名为Enemy放在较远位置。先不去追求好看的角色模型用基本几何体把逻辑跑通是学习阶段最高效的方式。等熟悉了脚本和物理套路再去替换正式模型。3. C# 脚本基础你必须掌握的 Unity 脚本写法3.1 创建脚本的两种方式第一种方式在 Project 窗口点击右键选择Create - C# Script命名为PlayerController。第二种方式在 Hierarchy 中选中Player在 Inspector 面板点击Add Component搜索Player Controller并添加。Unity 会自动创建脚本文件。脚本创建后双击打开默认编辑器Visual Studio 或 Rider。初始模板如下using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }这个模板每个新手都见过但很多人不理解为什么必须这样写。简单说Start和Update是 Unity 的约定方法名Unity 引擎会自动调用它们。你不可以随意改名也不可以随意改访问修饰符。如果想要 Unity 在 Inspector 中显示变量需要加上[SerializeField]或使用public字段。3.2 组件引用的获取方式一个脚本要控制角色移动首先要拿到Transform、Rigidbody、Animator等组件引用。常见写法有三种public class PlayerController : MonoBehaviour { // 1. 在 Inspector 中手动拖拽赋值 public Rigidbody rb; // 2. 使用 GetComponent 在运行时获取 private Rigidbody rb2; // 3. 使用 RequireComponent 保证组件存在 private Rigidbody rb3; void Start() { rb2 GetComponentRigidbody(); rb3 GetComponentRigidbody(); } }第一种方式直观但容易漏拖引用导致空引用异常第二种方式更稳妥但在Update中频繁调用GetComponent会影响性能所以推荐在Awake或Start中缓存引用。第三种方式可以在脚本挂载时自动添加缺失的组件适合必须依赖某个组件的场景。实际项目中最常见的健壮写法是在Awake里缓存组件public class PlayerController : MonoBehaviour { private Rigidbody rb; private Transform cam; void Awake() { rb GetComponentRigidbody(); cam Camera.main?.transform; } }3.3 输入系统的选择Unity 的输入系统有两个时代老版的Input ManagerInput.GetAxis和新版的Input System。如果你创建的是 3D Core 模板项目默认激活的是老版输入。老版输入的核心 APIfloat horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical);这两个轴分别对应键盘的 A/D 和 S/W 键以及手柄的摇杆。返回值为 -1 到 1 的浮点数所以支持平滑过渡。如果是检测按钮事件使用Input.GetButtonDown(Jump)。新版 Input System 功能更强支持按键重绑定、设备分离等但需要安装包并切换 Active Input Handling。新手阶段建议先用老版输入把逻辑跑通后再迁移到新版。4. 实战一第三人称角色控制4.1 需求拆解我们要实现一个第三人称角色控制具体行为包括按 WASD 控制角色前后左右移动。移动方向与相机朝向相关而不是与世界坐标轴固定相关。按空格键跳跃。角色遇到斜坡或台阶时能自然跨越而不是滑步。这是一个非常经典的需求也是很多 3D 游戏的基础。我们拆解成三步添加刚体、编写移动逻辑、处理相机跟随。4.2 给 Player 添加刚体选中 Player点击Add Component添加Rigidbody。默认配置下Rigidbody会受到重力影响但直接在Update中修改transform.position会导致物理和逻辑冲突。所以我们要启用Constraints中的冻结旋转Freeze Rotation X / Y / Z全部打勾。Interpolate设置为Interpolate让刚体位置变化更平滑。这样角色不会因为碰撞而翻滚倒地。4.3 编写移动脚本下面是完整的PlayerController.cs可以直接复制到项目中using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class PlayerController : MonoBehaviour { [Header(移动参数)] public float moveSpeed 6f; public float jumpForce 8f; public float groundCheckDistance 0.2f; public LayerMask groundMask; [Header(组件引用)] public Transform cameraTransform; private Rigidbody rb; private bool isGrounded; void Awake() { rb GetComponentRigidbody(); if (cameraTransform null) { cameraTransform Camera.main?.transform; } } void Update() { // 处理跳跃输入 if (Input.GetButtonDown(Jump) isGrounded) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } void FixedUpdate() { // 检测是否在地面上 isGrounded Physics.CheckSphere( transform.position Vector3.down * 0.1f, groundCheckDistance, groundMask ); // 获取输入 float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); // 计算与相机方向相关的移动方向 Vector3 camForward cameraTransform.forward; Vector3 camRight cameraTransform.right; camForward.y 0f; camRight.y 0f; camForward.Normalize(); camRight.Normalize(); Vector3 moveDirection (camForward * vertical camRight * horizontal).normalized; // 使用刚体的速度进行移动保留重力效果 Vector3 velocity rb.velocity; velocity.x moveDirection.x * moveSpeed; velocity.z moveDirection.z * moveSpeed; rb.velocity velocity; } }这段代码中有几个关键点需要详细解释为什么移动写在 FixedUpdate 而不是 Update物理引擎的模拟频率是固定的默认每秒 50 次而Update的调用频率受帧率影响。如果移动逻辑放在Update中帧率波动时物体的移动步长也会波动放在FixedUpdate中则能保证每次物理步进都得到对应的速度赋值运动过程更稳定。为什么计算方向时要先取 forward 和 right再去掉 Y 分量因为相机的 forward 向量通常不是完全水平的相机朝向会有俯仰角。如果直接用相机的 forward 作为移动方向角色会随着相机抬头低头而飞起来。去掉 Y 分量然后归一化可以保证角色的移动方向始终在一个水平面内。为什么使用 rb.velocity 而不是 rb.AddForce对于“角色控制”这种需要直接操控速度的场景直接设置 velocity 比施加力更可控。角色按一下 W 应该立即以恒定速度前进而不是像物体被推了一下那样缓慢加速。对于跳跃使用AddForce和ForceMode.Impulse则能模拟瞬间冲量效果更自然。4.4 编写相机跟随脚本第三人称视角下相机需要跟随角色并且根据鼠标输入旋转。using UnityEngine; public class CameraFollow : MonoBehaviour { public Transform target; public float distance 5f; public float height 2f; public float sensitivity 3f; public float smoothTime 0.1f; private float currentX 0f; private float currentY 0f; private Vector3 velocity Vector3.zero; void LateUpdate() { if (target null) return; currentX Input.GetAxis(Mouse X) * sensitivity; currentY - Input.GetAxis(Mouse Y) * sensitivity; currentY Mathf.Clamp(currentY, -30f, 60f); Quaternion rotation Quaternion.Euler(currentY, currentX, 0f); Vector3 desiredPosition target.position - rotation * Vector3.forward * distance Vector3.up * height; transform.position Vector3.SmoothDamp( transform.position, desiredPosition, ref velocity, smoothTime ); transform.LookAt(target.position Vector3.up * 1.5f); } }将脚本挂到 Camera 上把target指向 Player把 Camera 放到场景中合适位置即可。相机跟随写在LateUpdate中是为了确保在所有Update逻辑角色移动都执行完后相机再更新位置避免产生一帧的滞后感。4.5 运行验证点击编辑器顶部的 Play 按钮你应该能看到按 W/S/A/D 时角色以恒定速度移动。移动方向始终与相机朝向一致不会因为相机旋转而混乱。按空格时角色跳起并落到地面。鼠标移动时相机围绕角色旋转。如果角色无法跳跃检查 Player 的groundMask是否设为了地面层。默认情况下 Unity 的物体都在Default层你需要在 Inspector 中把groundMask设置为Default。5. 实战二物理系统进阶——碰撞检测与触发器5.1 碰撞与触发器的区别Unity 的 Collider 有两种工作模式碰撞模式Collision两个物体互相阻挡物理引擎会计算接触并阻止穿透。例如地面挡住角色下落。触发器模式Is Trigger物体不产生物理阻挡但 Unity 会在物体进入、停留、离开时发送事件。例如走进某个区域触发机关。在代码中碰撞事件的方法名与触发事件方法名不同千万不能混用。类型进入停留离开碰撞OnCollisionEnterOnCollisionStayOnCollisionExit触发器OnTriggerEnterOnTriggerStayOnTriggerExit5.2 实现一个金币收集系统下面我们做一个简单的金币收集逻辑。创建一个 Capsule设置好缩放和旋转给它添加Sphere Collider勾选Is Trigger然后在上面挂一个收集脚本。using UnityEngine; public class Coin : MonoBehaviour { public int value 1; public GameObject pickupEffect; void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { // 通知玩家增加金币 other.GetComponentPlayerStats()?.AddCoin(value); // 播放拾取特效 if (pickupEffect ! null) { Instantiate(pickupEffect, transform.position, Quaternion.identity); } // 销毁金币 Destroy(gameObject); } } }这段代码使用了CompareTag来判断进入触发器的对象是否为玩家。注意使用标签对比比other.name Player更可靠因为名称可以随意改而标签是独立的分类系统。同时脚本通过GetComponentPlayerStats()获取玩家身上的另一个脚本这是 Unity 中脚本之间通信的常见方式。你可以为 Player 添加一个PlayerStats脚本using UnityEngine; public class PlayerStats : MonoBehaviour { public int coinCount 0; public void AddCoin(int amount) { coinCount amount; Debug.Log(当前金币 coinCount); } }5.3 物理材质控制滑动与弹跳如果你希望角色在冰面上滑动、物体落地有弹性需要用到Physic Material物理材质。创建方式在 Project 窗口右键 -Create - Physic Material。常用参数有Dynamic Friction运动中的摩擦力。Static Friction静止时的摩擦力。Bounciness弹性系数。Friction Combine两个物体摩擦力合并方式。给地面添加一个有摩擦的物理材质可以防止角色像在冰面上一样滑行。默认物理材质参数适合大多数场景但如果你的角色移动时总是滑步出发可以尝试提高地面的摩擦力。5.4 粒子特效与物理的配合搜索热词里有一条提到“粒子特效内存泄露 unity”这里顺带提一个常见的工程问题。Unity 的粒子特效如果使用Instantiate大量生成且不销毁会导致内存持续增长。解决方案有两种使用对象池Object Pool复用粒子系统。在特效脚本中设置自动销毁或定时销毁。using UnityEngine; public class AutoDestroy : MonoBehaviour { public float lifetime 2f; void Start() { Destroy(gameObject, lifetime); } }把这个脚本挂到粒子特效预设体上通过Destroy(gameObject, lifetime)延迟销毁是最简单的防内存泄露手段。稍后我们在最佳实践章节会更详细地展开对象池方案。6. 实战三敌人 AI 巡检与追击6.1 使用 NavMesh 寻路Unity 的 NavMesh 寻路分为两步烘焙导航网格、让物体沿路径移动。首先把场景中所有静态障碍物标记为静态方法是选中 Cube在 Inspector 右上角勾选Static下拉菜单选择Navigation Static。然后打开菜单Window - AI - Navigation点击Bake按钮烘焙。如果你在地面上看到蓝色的可行走区域说明烘焙成功。6.2 添加 NavMeshAgent给 Enemy 添加NavMesh Agent组件。这个组件自带一个胶囊碰撞器它可以驱动物体沿着 NavMesh 移动。关键参数Speed移动速度。Angular Speed转向速度。Stopping Distance到达目标点后停止的距离。Auto Braking到达目标点附近时是否逐渐减速。6.3 编写 AI 控制脚本下面是一个简单的 AI 逻辑当玩家距离较远时敌人在几个固定点之间巡逻当玩家进入视野范围后敌人切换为追击状态。using UnityEngine; using UnityEngine.AI; public class EnemyAI : MonoBehaviour { public Transform player; public float chaseRange 10f; public float patrolRange 5f; public Transform[] patrolPoints; private NavMeshAgent agent; private int currentPoint 0; private bool isChasing false; void Awake() { agent GetComponentNavMeshAgent(); } void Update() { if (player null) return; float distance Vector3.Distance(transform.position, player.position); if (distance chaseRange) { isChasing true; } else if (distance chaseRange * 1.5f) { isChasing false; } if (isChasing) { agent.SetDestination(player.position); } else { Patrol(); } } void Patrol() { if (patrolPoints.Length 0) return; agent.SetDestination(patrolPoints[currentPoint].position); if (agent.remainingDistance 1f !agent.pathPending) { currentPoint (currentPoint 1) % patrolPoints.Length; } } void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, chaseRange); } }这个脚本有一个细节值得注意追击状态与巡逻状态之间使用了不同的阈值chaseRange和chaseRange * 1.5f这叫“滞回”设计。如果检测到进入范围的阈值和退出范围的阈值一样敌人会在边界来回抖动。通过加一个小阈值差可以避免这个问题。OnDrawGizmosSelected是一个编辑器回调函数选中该物体时会在 Scene 视图中绘制调试信息。这里的红色线框就是敌人的追击范围方便在编辑器中调整参数。6.4 AI 中状态机的概念上面的脚本用bool isChasing区分了巡逻和追击两种状态。在更复杂的需求中比如敌人还有攻击、逃跑、死亡状态用 bool 组合会变得难以维护。此时应该引入简单的状态机。public enum EnemyState { Patrol, Chase, Attack, Dead }然后在Update中根据当前状态执行不同分支逻辑。状态机让 AI 行为变得清晰、可维护这是 Unity 开发进阶的必备技能。常见错误是把状态切换逻辑散落在各个流程中导致调试困难。最佳实践是在一个统一入口处理状态转换例如写一个SwitchState方法集中管理进入状态时的初始化。7. 实战四多平台构建发布7.1 构建前的检查清单构建平台前先检查以下项目场景是否已添加到 Build Settings。脚本中是否有平台相关 API 未做条件编译。材质和 Shader 是否支持目标平台。纹理压缩格式是否适配。字体是否嵌入。打开File - Build Settings点击Add Open Scenes将当前场景加入。7.2 构建 Windows 平台在 Build Settings 中Platform选择PC, Mac Linux StandaloneTarget Platform选择WindowsArchitecture选择x86_64。然后点击Build选择输出目录即可。Windows 构建产物包含一个.exe文件和一个_Data文件夹发布时必须一起分发。7.3 构建 macOS 平台同样选择PC, Mac Linux StandaloneTarget Platform选择macOS。Unity 在 macOS 上构建会生成一个.app文件。注意macOS 构建通常在 macOS 系统上执行如果需要在 Windows 上交叉构建 macOSUnity 支持有限建议使用 macOS 机器或 CI 工具。7.4 构建 Android 平台构建 Android 前需要先配置 Android SDK 和 JDK。Unity Hub 安装时可以选择安装 Android Build Support 模块。也可以使用 Unity 自带的 SDK 管理器。构建前在Project Settings - Player中设置Package Name应用包名例如com.example.unityactiondemo。Minimum API Level根据目标设备设置。Texture Compression选择合适的格式。此外Android 构建还需要处理“物理返回键”的交互问题。Android 设备自带返回键Unity 默认按下返回键会退出应用。如果你想在返回键弹出确认对话框需要监听Input.GetKeyDown(KeyCode.Escape)using UnityEngine; public class AndroidBackButton : MonoBehaviour { void Update() { if (Application.platform RuntimePlatform.Android Input.GetKeyDown(KeyCode.Escape)) { // 弹出退出确认或暂停游戏 Debug.Log(Android 返回键被按下); } } }使用Application.platform判断当前运行平台是一种安全的平台适配写法。7.5 不同平台的注意事项性能差异移动端的 CPU/GPU 性能远低于 PC构建 Android 版时要注意减少实时灯光数量。使用更轻量的光影方案。控制角色面数和粒子数量。输入差异PC 端使用键盘鼠标Android 端使用触屏。如果你的角色控制只依赖Input.GetAxis(Horizontal)移动端将无法操作。解决方案是接入Input System的组合控制或使用虚拟摇杆插件。构建体积Android 构建体积通常较大可以使用AssetBundle拆分资源包或者使用 IL2CPP 脚本后端减少包体不过 IL2CPP 会增加构建时间需要在包体大小和迭代速度之间做取舍。8. 常见问题与排查思路这里整理几个 Unity 入门实战中最高频的问题以及对应的排查顺序。问题现象常见原因解决思路角色移动时抖动或穿模位移逻辑写在 Update、刚体速度在 FixedUpdate 中被覆盖统一在 FixedUpdate 中处理刚体移动角色跳不起来groundMask 未设置或地面与角色不在同一层将 groundMask 设为 Default 或创建 Ground 层NavMeshAgent 不动场景没有烘焙导航网格或目标点不可达重新 Bake检查目标点是否在蓝色区域内触发器没有触发碰撞双方至少一方没有 Collider或关键物体未开启 Is Trigger检查 Collider 与 Is Trigger 设置鼠标旋转视角时相机抖动相机跟随逻辑写在 Update 而非 LateUpdate改为 LateUpdate构建 Android 后点击屏幕无反应老版 Input 在 Android 上无法获得键盘轴输入接入虚拟摇杆或迁移 Input System粒子特效频繁实例化后卡顿大量特效对象未销毁导致内存上升使用对象池或延迟销毁方案构建包太大未做纹理压缩、Shader 过多、包含未使用资源使用 Build Report 分析包体构成值得单独说明的是 Unity 中“物体速度怎么获取”的问题。很多人想实现“根据速度播放跑步动画”可以使用刚体的velocity.magnitudefloat speed rb.velocity.magnitude; animator.SetFloat(Speed, speed);这里的velocity是三维向量magnitude输出的是向量长度也就是实际速率。如果只想要某个轴的速度可以直接取rb.velocity.x或rb.velocity.z。9. 最佳实践与工程建议9.1 脚本与组件设计规范随着项目变大把所有逻辑塞进一个 MonoBehaviour 的做法会让代码很快腐烂。建议按职责拆分脚本例如输入控制脚本只负责读取输入不负责移动逻辑。移动逻辑脚本接收一个移动方向执行位移。动画控制脚本监听移动速度驱动动画状态机。角色属性脚本管理血量、金币、状态。这样每个脚本职责单一方便测试和复用。Unity 的 Inspector 可视化参数调优是这个框架的巨大优势尽量把需要频繁调整的数值暴露成 public 字段或[SerializeField]私有字段。9.2 物理系统的性能与安全物理系统涉及大量计算因此要注意以下几点不要为了复杂而使用多层高精度 Collider。可以用Physics.IgnoreCollision忽略不需要碰撞的物体对。静止物体添加 Collider 后不需要 Rigidbody但运动的物体必须有 Rigidbody。复杂场景中建议使用 Layer 和 LayerMask 来过滤碰撞而不是依赖if判断。同时要强调的是物理效果在编辑器里和真机上可能表现不同这与固定的物理步长和平台帧率有关。发布前要在目标平台上真机测试跳跃手感、碰撞体感和按钮响应。9.3 对象池应对高频生成不管是子弹、怪物还是粒子特效频繁Instantiate和Destroy会产生 GC 压力。对象池是最常用的优化方案。一个简单的对象池核心逻辑如下using System.Collections.Generic; using UnityEngine; public class SimpleObjectPool : MonoBehaviour { public GameObject prefab; public int poolSize 20; private QueueGameObject pool new QueueGameObject(); void Awake() { for (int i 0; i poolSize; i) { GameObject obj Instantiate(prefab); obj.SetActive(false); pool.Enqueue(obj); } } public GameObject Get() { if (pool.Count 0) { pool.Enqueue(Instantiate(prefab)); } GameObject obj pool.Dequeue(); obj.SetActive(true); return obj; } public void Return(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj); } }使用对象池时需要注意取出对象后的初始化逻辑以及归还对象时重置状态避免复用时残留上次的数据。9.4 AI 与 NavMesh 的工程注意NavMesh 烘焙的是静态场景如果场景中的障碍物可以移动比如大门开关、可破坏墙体需要额外处理。最简单的方案是使用NavMeshObstacle组件它会实时影响 NavMeshAgent 的避障但带来的性能开销也需要评估。复杂场景中推荐将寻路与决策分离决策层判断该去哪寻路层负责计算路径。不要让 AI 脚本里既写状态切换又写寻路细节否则后期维护很痛苦。9.5 版本管理与命名规范即使是个人的学习项目也建议一开始就使用 Git 管理。Unity 项目需要注意.gitignore排除Library、Temp、Obj、Logs等自动生成目录。场景文件是二进制格式多人协作时尽量避免同时编辑同一场景。脚本文件名必须与类名一致否则挂载时会出现错误。10. 总结下一步学什么到这一步你已经通过一个可运行的角色 demo串联了 Unity 中最核心的几个模块C# 脚本编写、角色移动、物理碰撞、触发器事件、NavMesh AI 寻路以及多平台构建发布。如果继续深入 Unity 游戏开发建议按下面的路线学习动画系统把 Animator Controller 与角色移动速度结合替换简单几何体为带骨骼动画的角色模型。UI 系统实现血条、金币计数、暂停菜单掌握 Canvas 和 UI 事件。场景切换与数据持久化在多场景间传递玩家数据使用 ScriptableObject 存配置。对象池与资源加载学习 AssetBundle 和 Addressables 处理大型项目的资源管理。网络同步尝试用 Mirror 或 Photon 做一个多人联机小游戏理解状态同步与客户端预测。Shader 与渲染学习 URP 管线下的自定义 Shader控制角色描边、特效表现。实际开发中角色手感、物理参数和 AI 行为都是需要反复调参才能获得满意效果的。不要指望一次写完就完美多运行、多调试、多对比不同参数下的体感差异是游戏开发的基本功。如果这篇文章对你有帮助可以先收藏备用。动手把示例跑起来再尝试改一改移动速度、追击范围、跳跃力度这些参数你会对 Unity 的组件化开发模式有更深的体会。