Unity 3D入门实战:从角色移动到敌人AI与多平台构建

发布时间:2026/10/7 13:15:08
Unity 3D入门实战:从角色移动到敌人AI与多平台构建 很多 Unity 开发者在入门中期会陷入同一种状态场景会搭了材质会换了脚本也能挂上去了但一运行就出各种“怪毛病”——角色按下方向键不动或者原地抽搐明明有碰撞体角色还是穿墙敌人站在那里怎么都不追击玩家。表面看是不同功能出了问题实际上根因是一样的把 C# 脚本、输入系统、物理组件、AI 寻路和多平台构建当成孤立知识点学却没有建立它们之间的协作关系。这篇文章是 Unity 3D 游戏开发入门实战的下半篇重点从“搭场景”转向“让场景真正动起来”。读完你会理解 Unity 生命周期中各个方法的调用时机角色移动应该写在哪里碰撞体与触发器如何配合敌人如何用导航网格追击玩家以及完成一个小游戏后如何在 Windows、Android、WebGL 等平台构建发布。全文保持入门向但会把原理讲透让你后续自己扩展功能时不再全靠试错。这里想先说一个明确判断Unity 入门阶段最重要的不是记住更多按钮和 API而是学会用“组件协作”的思维方式看问题。只要把输入、物理、脚本调用这三条关系理清入门阶段常见的抖动、穿墙、AI 不响应等问题会消失大半。接下来直接用完整案例带你走一遍链路先看清整体关系再动手写代码。1. 先理清 Unity 开发里那张“关系网”Unity 的场景编辑器看起来很“可视化”拖拽、点击、改参数就能做出很多效果。但这只是表象。Hierarchy 窗口里的每个游戏对象本质是一系列组件的容器而 C# 脚本本身也是一种组件。真正驱动游戏运行的不是你在编辑器里看到的那张平面图而是组件之间的事件调用和数据交换。以最常规的角色控制为例一张最小协作网是这样构成的输入系统读取键盘、手柄或触摸屏的输入转换成数值交给脚本。角色脚本把输入值转成移动方向决定角色该往哪走。刚体组件Rigidbody由物理引擎接管角色的位置和速度变化避免开发者直接改 Transform 造成抖动或穿墙。碰撞体组件Collider告诉物理引擎“这个对象占用多少空间”以及与其他碰撞体接触时发生什么。相机脚本跟随角色位置并调整观察方向。这里最大的误解是很多新手认为“角色的位置应该由脚本直接改 Transform”。这个思路在简单 2D 小游戏中可以凑合但到了 3D 物理场景一旦对象挂上了 Rigidbody物理引擎就认为位置和速度应该由它接管。如果你在同一帧里反复改 transform.position物理引擎同时又在更新刚体状态角色就会抖动或表现异常。Unity 官方在这方面的建议很明确对带刚体的对象尽量通过 AddForce、velocity 等物理接口改变运动而不是直接改 Transform。另一个容易被忽略的概念是生命周期调用关系。Update 每帧执行和帧率绑定FixedUpdate 按固定时间步执行通常用于物理运算。如果你在 Update 里改变刚体速度而物理引擎在同一固定时间步里处理碰撞两者会出现不同步。这块理解不到位后面写移动、写跳跃都会遇到诡异现象。所以这个章节的核心目标是先建立一张心智地图脚本是数据加工器物理组件是执行器输入是信号源构建流程是最后一道关卡。凭借这张地图进入下面的实践效率会高很多。2. 环境准备与项目结构规划动手写脚本之前先把工程环境调整一下能省掉后期大量因版本、导入和目录混乱导致的麻烦。Unity 编辑器版本建议使用较新的 Unity 6 或 Unity 2022 LTS 之后的版本。本文示例使用传统输入接口 Input.GetAxis这是目前网上大量教程仍然采用的方式也最适合新手先跑通逻辑。如果你的 Unity 版本启用了新版 Input System在 Project Settings Player Active Input Handling 中可以选择 Both让新旧输入系统同时可用。如果只选择 Input System Package传统写法会直接报“Input 已过时”或编译错误。项目结构方面建议在 Assets 下建立固定目录不要把所有资源堆在根目录。下面是推荐结构Assets/ ├── Scenes/ # 场景文件 ├── Scripts/ # C# 脚本 ├── Prefabs/ # 预制体 ├── Materials/ # 材质 ├── Models/ # 模型资源 └── Textures/ # 贴图资源很多初学者习惯把脚本直接挂在对象上文件也乱放项目小的时候没感觉场景一复杂就会出现脚本丢失引用、插件目录冲突的问题。建议从第一天就建立目录规范。后续导入第三方资源和插件时也不要让 Assets 根目录失控。测试场景可以复用上一篇文章创建的地形或房间。为了演示本章内容需要一个平整地面、一个角色胶囊体、一个立方体或球体作为收集物。角色建议由一个空物体和子模型组成比如创建一个名为 Player 的空物体下面放置 Capsule 子物体。这样后续修改模型、调整碰撞体和挂载脚本都比较清晰。搭建完成后在 Project 窗口把场景保存到 Scenes 目录。在 Scripts 目录右键创建 C# Script 时文件名建议和类名保持一致。如果文件名叫 PlayerMovement.cs类名自动为 PlayerMovement。不要在创建后随意改名字否则会触发“类与文件不一致”的编译错误。3. C# 脚本生命周期每个方法该放什么代码Unity 的 C# 脚本不需要手动 new 对象。编辑器会在场景加载时自动实例化挂在游戏对象上的 MonoBehaviour 脚本并在特定时机调用对应方法。理解这些调用时机比记住几十个 API 更重要。常用生命周期方法只有这几个方法调用时机典型用途Awake对象创建时立即调用获取组件引用、初始化私有字段OnEnable对象或脚本被启用时调用开启监听、订阅事件Start第一次 Update 前调用设置初始状态引用外部对象Update每帧调用处理输入、动画状态、普通判断FixedUpdate固定时间间隔调用处理刚体、物理碰撞逻辑LateUpdate所有 Update 执行之后调用相机跟随、后处理类逻辑新手常常纠结既然 Update 每帧调用为什么刚体移动要写到 FixedUpdate因为物理引擎的模拟步长是固定的默认固定时间步约为 0.02 秒也就是每秒 50 次。帧率则会随硬件波动可能是 60 帧、120 帧甚至更低。如果移动逻辑跟着帧率跑而物理引擎按固定步长跑两个系统的数据就会互相干扰。刚体位置变化由物理引擎推演自然应该与它的模拟步长对齐。下面是一个生命周期演示脚本注意每个方法里的代码说明// 文件路径Assets/Scripts/LifecycleDemo.cs using UnityEngine; public class LifecycleDemo : MonoBehaviour { private Rigidbody rb; private float counter; private void Awake() { rb GetComponentRigidbody(); Debug.Log(Awake 调用); } private void Start() { Debug.Log(Start 调用); } private void Update() { counter Time.deltaTime; transform.Rotate(0f, 30f * Time.deltaTime, 0f); } private void FixedUpdate() { // 对刚体施力请放在这里 if (rb ! null) { rb.AddForce(Vector3.down * 0.5f, ForceMode.Acceleration); } } private void LateUpdate() { // 摄像机跟随通常会写在这里 } }运行场景后Console 窗口会依次打印 Awake 和 Start 调用信息你可以直观感受顺序。另一个要点是如果脚本 A 在 Awake 中初始化数据脚本 B 在 Start 中引用这些数据顺序通常是安全的。反之如果在 Awake 里引用其他脚本在 Start 中初始化的字段很可能拿到空引用。稳妥做法是在自己的 Awake 里获取自身组件在 Start 里获取外部对象或管理器。4. 角色移动控制输入、刚体与相机配合这一节真正开始实现一个可在场景里走动的角色。输入部分采用传统 Input ManagerInput.GetAxis(Horizontal) 和 Input.GetAxis(Vertical) 已经经过大量项目验证适合入门。后续需要手柄、触摸屏或虚拟按键时再切换到新 Input System 也不冲突。PlayerMovement 脚本的核心思路是在 Update 中读取输入并计算目标移动方向在 FixedUpdate 中把方向转换成刚体速度并处理角色转向。不要直接在 Update 里改 Transform这是避免抖动和物理异常的关键。// 文件路径Assets/Scripts/PlayerMovement.cs using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class PlayerMovement : MonoBehaviour { [Header(移动参数)] public float moveSpeed 5f; public float rotationSpeed 720f; private Rigidbody rb; private Vector3 moveDirection; private void Awake() { rb GetComponentRigidbody(); } private void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); moveDirection new Vector3(h, 0f, v).normalized; } private void FixedUpdate() { if (moveDirection.magnitude 0.01f) { rb.velocity new Vector3(0f, rb.velocity.y, 0f); return; } Vector3 targetVelocity moveDirection * moveSpeed; rb.velocity new Vector3(targetVelocity.x, rb.velocity.y, targetVelocity.z); Quaternion targetRotation Quaternion.LookRotation(moveDirection, Vector3.up); transform.rotation Quaternion.RotateTowards( transform.rotation, targetRotation, rotationSpeed * Time.deltaTime ); } }代码里有几个关键点值得注意。第一moveDirection 做了 normalized 处理否则斜向移动时速度会变成 1.414 倍移动速度会明显比横向或纵向更快。第二rb.velocity 在赋值时保留了 Y 轴速度如果不保留角色会在落地瞬间丢失重力速度跳跃和下坡表现都会异常。第三旋转使用 RotateTowards 而不是直接赋角度让转身有一定过渡手感更自然。这套设计的本质是速度控制而不是位移控制后续扩展跳跃、冲刺、掉落都更容易。给 Player 物体挂载 Rigidbody 和 Capsule Collider。Rigidbody 的 Use Gravity 保持勾选Interpolate 建议设置为 Interpolate能减少渲染抖动。Capsule Collider 的中心和高度要尽量贴合角色模型不要让碰撞体明显小于或大于视觉模型。相机跟随脚本放在 LateUpdate 里。因为 LateUpdate 在所有 Update 和动画处理之后执行此时玩家位置已经更新相机再移动过去不会出现角色已经移动而相机还停在上一帧位置的割裂感。参考实现如下// 文件路径Assets/Scripts/CameraFollow.cs using UnityEngine; public class CameraFollow : MonoBehaviour { [Header(跟随目标)] public Transform target; [Header(偏移量)] public Vector3 offset new Vector3(0f, 2.5f, -4f); public float smoothSpeed 8f; private void LateUpdate() { if (target null) return; Vector3 desiredPosition target.position offset; transform.position Vector3.Lerp( transform.position, desiredPosition, smoothSpeed * Time.deltaTime ); transform.LookAt(target.position Vector3.up * 1.2f); } }把脚本挂到相机上将 Player 物体拖到 Target 字段。运行场景后使用 WASD 或方向键观察角色是否正常移动和转向。如果发现角色一直下沉或侧翻通常是 Collider 没有包住模型或者 Rigidbody 的 Constraints 没有设置好。对于只在地面行走的角色勾选 Constraints 中 Freeze Rotation X/Y/Z可以防止撞到障碍物时角色被物理引擎翻倒。5. 物理系统碰撞体、触发器与力的用法物理系统是入门阶段最容易混淆的部分因为“碰撞检测”和“触发器触发”看起来相似内部机制却完全不同。碰撞体是空间几何体的抽象刚体是物理引擎对物体运动状态的描述。当两个碰撞体接触时引擎会产生碰撞事件如果把一个碰撞体标记为 Is Trigger它就不再参与物理阻挡只负责探测进出事件。碰撞体之间的典型用法是角色与地面的接触。角色胶囊体与地面碰撞时物理引擎阻止角色穿透碰到墙壁时会被挡下。这要求两个 Collider 都有效并且至少一方带 Rigidbody。角色移动用速度控制碰撞结果交给物理引擎处理脚本里不要试图在 OnCollisionEnter 中手动纠正位置否则会造成位置跳变。触发器最典型的设计是金币收集。把金币的 Box Collider 设置为 Is Trigger脚本接收 OnTriggerEnter 事件。触发事件要求双方中至少一方带 Rigidbody否则不会触发。参考实现// 文件路径Assets/Scripts/CoinPickup.cs using UnityEngine; public class CoinPickup : MonoBehaviour { [Tooltip(该金币增加的分数)] public int scoreValue 1; private void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { // 实际项目中通常调用 GameManager.Instance.AddScore(scoreValue) Debug.Log($玩家拾取了金币增加 {scoreValue} 分); Destroy(gameObject); } } }使用 CompareTag 之前记得在 Tag Manager 中创建 Player 标签并把角色物体的 Tag 设置为 Player。如果忘记了拾取事件永远不会进入 if 分支。Tag 名称拼写不一致也是新手经常遇到的坑。关于力的使用Unity 的 AddForce 提供多种 ForceModeForceMode.Force 是持续力受质量影响ForceMode.Impulse 是一瞬间的冲击力适合开炮、跳跃ForceMode.Acceleration 直接给加速度忽略质量ForceMode.VelocityChange 直接改变速度忽略质量适合特殊效果。初学者最容易困惑的是 Force 与 Impulse 的数值受刚体质量影响同样一个冲量质量不同的物体速度变化不同。如果两个角色需要跳得不一样高不要只调力的大小先检查质量参数。物理事件的方法名也有明显区别OnCollisionEnter 需要双方都是非触发器状态OnTriggerEnter 用于触发器场景。不要混淆否则会出现“方法写了但始终不触发”的情况。如果拿不准先看 Inspector 中 Collider 的 Is Trigger 是否勾选再决定写哪个事件。验证物理逻辑时可以在 Window Analysis Physics Debugger 中打开刚体与碰撞体的可视化。很多看起来“穿墙”的问题其实是 Collider 尺寸调得太小模型看着很大但碰撞区域很小两者不匹配。6. 简易敌人 AINavMeshAgent 与追击逻辑游戏 AI 可以拆成三部分感知、决策、运动。入门项目中感知用距离判断决策只做“是否追击”运动交给导航网格。Unity 的 NavMeshAgent 是处理 3D 寻路的核心组件它基于预烘焙的 NavMesh 数据计算从当前位置到目标点的可行路径并自动避障。使用 NavMeshAgent 之前需要先烘焙导航网格。在场景中选择产生阻挡的物体如墙壁、地板、障碍物打开 Window AI Navigation 窗口。在 Bake 页签设置 Agent Radius、Agent Height 等参数点击 Bake。烘焙完成后地面会被蓝色网格覆盖这就是 AI 可行走的区域。烘焙参数决定了寻路的精细程度Agent Radius 如果设置太小寻路会贴近墙边角色容易卡在转角处。给敌人物体添加 NavMeshAgent 组件后设置 Speed、Angular Speed、Stopping Distance 等参数。Stopping Distance 表示 AI 逼近目标但还没撞上目标时就会停下的距离。如果设置为 0敌人会一直顶着玩家模型。下面是一套最基本的敌人追击逻辑玩家进入检测范围后开始追击离开范围后停止。// 文件路径Assets/Scripts/EnemyAI.cs using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class EnemyAI : MonoBehaviour { [Header(AI 参数)] public Transform playerTarget; public float detectionRange 8f; public float loseRange 12f; private NavMeshAgent agent; private bool isChasing; private void Awake() { agent GetComponentNavMeshAgent(); } private void Update() { if (playerTarget null) return; float distance Vector3.Distance(transform.position, playerTarget.position); if (!isChasing distance detectionRange) { isChasing true; } else if (isChasing distance loseRange) { isChasing false; } if (isChasing) { agent.isStopped false; agent.SetDestination(playerTarget.position); } else { agent.isStopped true; } } }这个实现最值得学习的点是把进入范围和离开范围拆成两个数值形成滞回区间。这样做可以防止敌人站在临界距离时反复切换追击和取消追击造成视觉上的抽搐行为。“判断状态”和“执行行为”被分开了Update 先计算状态再根据状态控制寻路代理逻辑更清晰。运行验证时把 Player 物体拖到 EnemyAI 的 playerTarget 字段。玩家靠近敌人后敌人从静止立刻转为追击玩家远离超过 loseRange 后敌人停在当前位置。地形复杂时还要注意玩家站立的位置有没有被烘焙进 NavMesh。如果玩家站在未烘焙的平台NavMeshAgent 无法算出路径敌人会停在原地。可以在 Scene 窗口直接查看导航网格覆盖范围并留意 Inspector 中 NavMeshAgent 的 remainingDistance 属性判断代理是否在正常工作。7. 多平台构建Windows、Android、WebGLUnity 被称为跨平台引擎得益于成熟的构建管线。一套场景、一套脚本通过 Build Settings 配置即可输出 Windows、Android、WebGL 等平台的版本。但“一套代码到处构建”不意味着完全不管平台差异渲染特性、输入方式和性能约束都不同。打开 File Build Settings在 Platform 列表选择目标平台。切换平台时Unity 会重新导入一部分资源耐心等待即可。以 Windows 为例选择后点击 Build指定输出目录Unity 会生成 exe 文件和同名数据目录。这两个部分缺一不可发布时需同时提供。如果想打包成单个可执行文件需要用第三方工具把数据目录合并进 exe。构建 Android 前需要确认几项配置。在 Player Settings Android 中填写 Package Name例如 com.example.firstgame设置 Minimum API Level。Unity 默认使用 IL2CPP 后端构建 Android首次构建耗时较长而且需要本机安装 Android SDK、NDK 和 JDK。更常见的问题是 Android Build Support 模块没有安装。如果 Build Settings 无法切换到 Android 目标回 Unity Hub 的 Modules 中安装 Android Build Support包含 SDK、NDK 和 OpenJDK。WebGL 平台的构建逻辑也很有特点。WebGL 目标不能完全依赖编辑器里的 Play 模式部分浏览器端功能需要在真实浏览器中验证。实际执行 Build 后Unity 会导出一个浏览器可运行的目录需要通过本地 HTTP 服务或部署到服务器访问。WebGL 默认有内存限制过于庞大的场景、纹理和音频会导致浏览器崩溃甚至白屏。建议在 Player Settings WebGL 中降低纹理压缩等级控制首包体积或使用 Addressables 做资源分级加载。三个平台都在 Build Settings 中操作但验收标准差异很大。Windows 版本要测试不同分辨率窗口、AltF4 退出逻辑和 DPI 缩放Android 版本要测试触摸输入、屏幕适配和后端生命周期WebGL 版本要测试浏览器加载进度、包体大小和内存占用。如果项目用了手柄或新 Input System还需要在键盘、触摸屏、手柄三种输入之间来回切换验证。构建产物建议放在工程目录之外的 Build 文件夹不要混入 Assets否则版本库体积会快速膨胀。Unity 项目由大量文件和 meta 文件组成多人协作时场景文件容易冲突务必配合 Git 等版本控制工具管理并提交 meta 文件。8. 常见问题与排查思路入门阶段会遇到的问题高度集中下面是一个排查表基本覆盖了上述内容的常见失败场景。问题现象可能原因排查方式解决方案角色按下方向键不动输入模块未启用、脚本没挂载查看 Console 是否有报错确认 Input Handling检查脚本挂载和 Input Manager 设置角色抖动明显直接改 Transform 与刚体冲突检查角色是否带 Rigidbody移动代码位置改用 velocity 控制开启 Interpolate角色穿墙落地Collider 尺寸过小或位置偏差打开 Physics Debugger 查看碰撞体范围调整 Collider 中心、半径和高度OnTriggerEnter 不触发Is Trigger 未勾选或缺少 Rigidbody检查 Collider 属性和物体组件勾选 Is Trigger给一方添加 RigidbodyOnCollisionEnter 始终不执行方法名写错或状态不对检查事件方法名和碰撞体状态修正方法名确认非触发器状态敌人不追击玩家NavMesh 未烘焙、引用为空查看 Scene 中是否有蓝色导航区域重新烘焙拖拽 playerTarget 引用Android 构建一直失败SDK/NDK/JDK 缺失在 Unity Hub 检查模块安装安装 Android Build SupportWebGL 打开白屏内存超限或资源包过大查看浏览器 Console 报错压缩纹理和音频减少首包资源脚本报 CS0246using 命名空间缺失查看错误输出中类型名补充 using UnityEngine 等命名空间排查问题要讲究顺序不要一上来就改代码。先看 Console 有没有红色错误没有报错就看 Inspector 中的引用是否完整再不行就用 Debug.Log 打印关键变量确认逻辑执行到哪一步中断。这个流程比盲目搜索错误信息可靠得多。9. 最佳实践与工程建议入门实战到这里接近尾声最后说几点对未来开发真正有帮助的经验。第一脚本职责要单一。角色控制脚本只负责输入和移动AI 脚本只负责寻路行为拾取脚本只处理收集事件。不要在一个几百行的 Update 里把移动、射击、血条、金币全写了。这样便于调试和复用后续替换输入方案或 AI 行为时只需改对应脚本不影响其他模块。第二合理使用 RequireComponent。它强制依赖组件脚本挂载时 Unity 会自动补齐所需组件减少“组件缺失导致空引用”的低级错误。PlayerMovement 已经用了 [RequireComponent(typeof(Rigidbody))]后续写其他脚本时可以沿用这个思路。第三善用 Tag 和 Layer。角色、敌人、道具尽量使用独立 Tag 或 Layer而不要靠对象名判断。对象名在运行时可能被修改而 Tag 和 Layer 是稳定属性。CompareTag 比直接比较 name 更安全性能也更可控。第四控制物理物体的数量。不要在场景中放置大量实时刚体。如果要模拟碎片、爆炸效果优先考虑对象池或粒子系统而不是堆大量真实刚体。大型地图可以拆成多个小块 Collider减少碰撞检测开销尤其移动端对此非常敏感。第五构建要尽早开始。项目第二天就可以尝试切换到 WebGL 或 Android 做一次构建测试。性能问题和兼容性问题发现得越晚修改成本越高。移动端显存占用、WebGL 包体大小这类指标应该从早期就纳入验收标准。第六版本管理要规范。Unity 依赖 .meta 文件维护资源映射关系放入 Git 时不要删除 meta 文件也不要随意重命名资源否则场景里的引用会断。团队协作时统一 Unity 版本和目录结构避免不同版本互相覆盖。入门阶段最容易产生的错觉是“做完验证 Demo 就算结束”。实际上把本系列的模块串起来——角色移动、拾取金币、敌人追击、多平台构建——你已经拥有一个 3D 跑图小游戏的雏形。接下来可以继续学习 Animator 动画系统、UGUI 界面、粒子特效、对象池、行为树等进阶内容。无论学什么坚持“先理解协作关系再动手写代码”的习惯踩坑数量会明显减少。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询