ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity AI Demo实战:基于NavMesh的状态机NPC巡逻与追逐实现

Unity AI Demo实战:基于NavMesh的状态机NPC巡逻与追逐实现 简介Unity AIDemo 是一份面向 Unity 初、中级开发者的 AI 技术演示工程以可直接打开的 Unity 项目方式展示导航网格、行为树、ML-Agents、传感器感知、动画状态机与物理碰撞等在角色智能控制中的综合运用。资源共 178 个文件压缩包仅 1.35MB主体包括 C# 行为逻辑脚本、asset/meta 场景与资源配置、dll 插件、sln/csproj 工程文件及少量 json 配置适合结合 Unity 工程阅读调试。目前已有 266 人学习下载。通过对照工程中的导航烘焙、AI 决策树节点和强化学习示例可快速理解从简单寻路到复杂决策系统的落地写法同时掌握多智能体协作与性能优化等实战技巧是一份轻量但覆盖 Unity AI 关键点的入门参考资料。1. unity AIdemo一个能直接跑起来的 NPC 智能体演示拿到这份 AIdemo 的第一反应是它不像网上那些“只能看不能改”的展示工程而是把 unity AI 最常见的巡逻、追逐、攻击和脱战做成了闭环。我在 Unity 里点下 PlayNPC 立刻沿着 NavMesh 路径自动巡逻玩家从视野边缘进入它马上切换成追逐追丢之后原地搜索两秒再回到巡逻。它真正解决的事情是你不用自己从零搭状态机也不用反复翻资料拼 NavMesh 参数打开就能复现改几个数值就能用在原型验证里。适合两类人一类是刚入坑 Unity、想抄一套能跑的 AI 代码的初学者另一类是被状态切换和边界条件折腾过、想少踩坑的熟手。2. 拆解项目内容场景、脚本与预制体的分工2.1 场景与预制体先分清谁在干活用 Unity Hub 打开工程前我建议先用 2021.3 LTS 或更新的版本避免 Shader 编译和脚本 API 对不上的问题。导入完成后别急着点 Play先看 Assets 目录。这个 AIdemo 的组织方式是很常见的三层结构Scenes/Main.unity主测试场景包含一块地面、几个巡逻点、一个玩家胶囊体和一个 EnemyAI 预制体Prefabs/EnemyAI.prefab携带 NavMeshAgent、状态机脚本和胶囊碰撞体的 NPCScripts/GuardAI.cs、GuardSensor.cs、FollowCamera.cs分别负责状态切换、距离/视野感知和调试视角跟随。这种拆法是有理由的。把移动逻辑放在 NavMeshAgent 上把行为逻辑放在 GuardAI 里互不污染。以后你要替换巡逻点不用去脚本里改数组直接在场景里拖几个空物体当巡逻点就行。要改 NPC 的触发距离也只需要在 Inspector 上调 chaseRange不用全局搜字符串。具体操作顺序我一般是这样在 Unity Hub 里打开工程等待首次导入完成在 Project 面板进入 Assets/AIDemo/Scenes双击 Main.unity在 Hierarchy 面板选中 EnemyAI查看 Inspector 上的 GuardAI 脚本选中场景里的 NavMeshSurface点一下 Bake确保导航网格覆盖整个测试地面点击 Play观察 NPC 从第一个巡逻点走向第二个巡逻点。最容易“翻车”的是第 4 步。很多人导入后直接 Play结果 NPC 站在原点不动因为场景里的 NavMesh 是空的或者烘焙数据没有保存。打开 Navigation 窗口Window - AI - Navigation选中地面和所有障碍物确认 Navigation Area 设为 Walkable再点 Bake。文件层级可以用下面这张表快速对照资源对应对象职责Scenes/Main.unity主场景摆放巡逻点、玩家、NavMeshSurfacePrefabs/EnemyAI.prefabNPC 主体承载 NavMeshAgent、碰撞体和 AI 脚本Scripts/GuardAI.csNPC 组件状态机切换、巡逻/追逐执行Scripts/GuardSensor.csNPC 子物体视野检测、进入追逐前的感知判断Scripts/FollowCamera.cs摄像机调试用第三人称跟随2.2 核心脚本与参数速查改之前先看这里GuardAI 脚本上有几个最关键的数字它们是这个 demo 能不能“像真人”的命门。我打开 Inspector 之后第一步就是把这些默认值抄一遍参数默认值作用chaseRange8玩家与 NPC 距离小于该值进入追逐attackRange1.5距离小于该值进入攻击loseRange12距离大于该值从追逐退回巡逻patrolWaitTime2巡逻到点后原地等待的秒数stoppingDistance0.5NavMeshAgent 停下时与目标点的距离rotationSpeed10手动控制转身时的插值速度有一个容易忽略的设计loseRange 必须大于 chaseRange。这不是随手写的是故意留出的“滞回区间”。如果 chaseRange 和 loseRange 一样玩家贴着边界左右移动时状态会在 Patrol 和 Chase 之间疯狂跳NPC 看起来就像在做布朗运动。修改参数的方式也很直观在 Hierarchy 里选中 EnemyAI右侧 Inspector 中的 GuardAI 组件直接改数字。比如把 chaseRange 从 8 改成 5NPC 会晚一点发现玩家把 patrolWaitTime 改成 0巡逻节奏会变得很急促。每个参数都会直接反映到运行时的行为上适合拿来验证自己对 AI 的理解是否准确。3. 状态机与 NavMesh 协同巡逻、追逐与脱战的核心实现3.1 一套能用的状态机结构枚举、条件判断和执行函数这个 demo 的核心不是寻路算法因为 Unity 的 NavMeshAgent 已经把 A* 和避障封装好了。真正的核心是状态机——到底在什么条件下从一个状态切到另一个状态。我见过很多人用一堆 bool 变量去控制行为比如 isChasing、isAttacking最后逻辑越写越乱。常见做法是用枚举来定义状态再根据距离和传感器结果切换状态。这是 GuardAI 脚本里最关键的一段骨架using UnityEngine; using UnityEngine.AI; public enum AIState { Patrol, Chase, Attack, Search } public class GuardAI : MonoBehaviour { public AIState state AIState.Patrol; public Transform target; public Transform[] patrolPoints; public float chaseRange 8f; public float attackRange 1.5f; public float loseRange 12f; public float patrolWaitTime 2f; private NavMeshAgent agent; private int patrolIndex; private float waitTimer; void Start() { agent GetComponentNavMeshAgent(); } void Update() { float distance Vector3.Distance(transform.position, target.position); // 每一帧先根据条件切换状态再执行当前状态的行为 SwitchState(distance); ExecuteState(); } void SwitchState(float distance) { switch (state) { case AIState.Patrol: if (distance chaseRange) { EnterState(AIState.Chase); } break; case AIState.Chase: if (distance loseRange) { EnterState(AIState.Patrol); } else if (distance attackRange) { EnterState(AIState.Attack); } break; case AIState.Attack: if (distance attackRange * 1.2f) { EnterState(AIState.Chase); } break; } } void EnterState(AIState newState) { state newState; agent.ResetPath(); } }这段代码的逻辑说明SwitchState 只负责“判断”ExecuteState 负责“执行”。判断和执行分离后你想加一个新状态不需要到处改 if 分支。EnterState 里清掉旧路径是必须的否则 NPC 从追逐退回巡逻时会先去执行旧的 chase 目标点产生一段“回头走”的诡异路径。参数说明chaseRange 是触发追逐的最小距离loseRange 是脱离追逐的最大距离attackRange 则是进入攻击的距离。注意 Attack 状态下退出攻击的阈值是 attackRange * 1.2而不是 attackRange。这是滞回阈值防止攻击动作在技能摆动期间反复切换。3.2 巡逻与追逐的实现NavMesh 路径怎么算状态切完了真正让 NPC 动起来的是 Patrol 和 Chase 这两个方法。很多人踩过坑以为设置了 agent.destination 就万事大吉结果 NPC 在到达点附近来回转圈。原因是没有处理 agent.pathPending 和 remainingDistance 的边界。这里给出可用的巡逻和追逐实现void ExecuteState() { switch (state) { case AIState.Patrol: Patrol(); break; case AIState.Chase: Chase(); break; case AIState.Attack: // 攻击逻辑这里先留空由动画事件触发 break; } } void Patrol() { if (agent.pathPending) return; if (agent.remainingDistance 0.5f) { waitTimer Time.deltaTime; if (waitTimer patrolWaitTime) { patrolIndex (patrolIndex 1) % patrolPoints.Length; agent.SetDestination(patrolPoints[patrolIndex].position); waitTimer 0f; } } } void Chase() { agent.SetDestination(target.position); }这段代码有三个细节要注意。第一agent.pathPending 是“路径还在计算中”的标志此时 remainingDistance 是 0如果不跳过NPC 会误判自己已经到达巡逻点然后原地等待。第二remainingDistance 0.5f 的 0.5 要和 Inspector 里 NavMeshAgent 的 Stopping Distance 保持一致。如果你把 Agent 的 stoppingDistance 改成 1这里最好也改成 1否则 NPC 会一直尝试往中心点挤。第三Chase 里每帧调用 agent.SetDestinationUnity 不会每帧重新计算完整路径它内部会做增量优化所以性能压力不大。用 NavMeshAgent 而不是手动写 A* 的最大好处是避障是现成的。NPC 遇到动态障碍物时NavMeshAgent 会自动绕路如果用手写寻路还要自己去处理高度差和碰撞偏移非常容易翻车。这也是这个 AIdemo 最值得参考的地方把移动交给 NavMesh把状态判断留给自己。4. 感知与移动细节速度获取、LookAt 与时间缩放4.1 从 NavMeshAgent 身上拿速度别用 Transform 去猜很多新手在同步 NPC 动画时喜欢用上一帧和当前帧的 Transform 位置差来算速度结果动画忽快忽慢。常见做法是直接读取 agent.velocity这是 NavMeshAgent 内部已经算好的真实速度包含了加速、减速和转向时的速度衰减。下面这段代码在 GuardAI 里用来同步动画参数float speed agent.velocity.magnitude; if (speed 0.1f) { speed 0f; } animator.SetFloat(MoveSpeed, speed);逻辑说明agent.velocity 返回的是向量取 magnitude 得到速度大小。阈值 0.1 的作用是消除 NavMeshAgent 在目标点附近可能产生的小幅漂移。如果不归零NPC 站在巡逻点等待时动画依然会播放“走路”这一帧看起来像在踏碎步。参数说明如果 NPC 用的是 Rigidbody 物理移动可以用 GetComponent ().velocity.magnitude但在这个 demo 里NavMeshAgent 是唯一移动方式读取 Agent 自己的速度是最准确的不要让两套系统同时控制位置。4.2 朝向平滑与 TimeScale转身不再抽搐如果直接用 transform.LookAt(target)NPC 会在发现玩家的一瞬间“咔”地扭过去非常生硬。更好的方式是用 Quaternion.Slerp 做插值让转向有过渡看起来像在转动身体而不是瞬移。这是 LookAt 平滑旋转的常见写法public float rotationSpeed 10f; void Update() { Vector3 direction target.position - transform.position; direction.y 0f; if (direction.sqrMagnitude 0.01f) { Quaternion targetRotation Quaternion.LookRotation(direction); transform.rotation Quaternion.Slerp( transform.rotation, targetRotation, Time.deltaTime * rotationSpeed ); } }代码说明direction.y 归零是必须的否则 NPC 会朝玩家头顶抬头。sqrMagnitude 判断是为了避免 target 和自身重叠时产生零向量导致 LookRotation 报错。Slerp 的第三个参数是插值因子rotationSpeed 控制转向速度一般 8 到 12 比较自然太快会显得僵硬太慢会显得迟钝。关于“unity timescale”在调试 AI 时非常有用。想放慢动作观察状态切换可以按键盘数字键实时调整if (Input.GetKeyDown(KeyCode.Alpha1)) Time.timeScale 0.3f; if (Input.GetKeyDown(KeyCode.Alpha2)) Time.timeScale 1f;注意 Time.timeScale 会影响所有使用 deltaTime 的逻辑包括 NavMeshAgent 的移动速度和动画播放速度。这个功能适合暂停式观察但不要拿它来做 UI 倒计时否则 UI 时间也会被拉长。调试视角建议用摄像机跟随。最简单的做法是在 Camera 上挂一个 LateUpdate 跟随脚本public class FollowCamera : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0, 6, -8); void LateUpdate() { transform.position target.position offset; transform.LookAt(target); } }这段代码的核心是 LateUpdate它会在所有 Update 执行完之后再更新相机位置避免相机“抖帧”。offset 的 y 设为 6、z 设为 -8是为了让视角略高且略远能看到 NPC 和玩家的相对位置。如果场景里已经有 Cinemachine也可以直接用虚拟相机的 ThirdPerson Follow但轻量调试还是这个脚本更顺手。5. 避坑记录NPC 抽搐、卡墙和状态切换闪断的排查5.1 四个高频 Bug 的现象与根因对照我拆过不止一个 AI demo发现最消耗时间的永远不是逻辑写不出来而是几个反复出现的“玄学问题”。下面这张表是我自己整理的高频故障对照可以帮你快速定位问题出在哪一层。现象根因修复方向NPC 在巡逻点附近左右抽搐stoppingDistance 太小Agent 一直在修正路径调大 Stopping DistanceNPC 被障碍物卡住或绕远路Agent Radius 太大或障碍物没有同步到 NavMesh调小 Radius动态障碍物用 NavMeshObstacle状态在 Patrol / Chase 之间闪断chaseRange 与 loseRange 过于接近没有滞回拉大 loseRange或加最短状态时间转身时疯狂旋转手动 Slerp 和 NavMeshAgent 自动转向抢控制权关掉 Update Rotation或限制角速度5.2 四个修复实例的详细参数第一条NPC 在巡逻点附近左右抽搐。现象是 NPC 到达一个巡逻点后没有原地等待反而一直到达不了目标在原地来回走。原因是 Inspector 里 NavMeshAgent 的 Stopping Distance 设成了 0而状态机里用 remainingDistance 0.5f 判断到达由于 Stopping Distance 是 0Agent 会一直试图把距离压到 0不断重新规划路径视觉上就是原地抖动。解决方式先把 Stopping Distance 改成 0.5再把 Patrol 里的 remainingDistance 阈值也改成 0.5两边保持一致问题立刻消失。第二条NPC 在动态障碍物前卡住。现象是场景里新放了一个箱子NPC 能“看到”目标但走不过去一直贴箱子输出。原因是箱子只是普通 Box Collider没有加入 NavMesh 烘焙也没有 NavMeshObstacle所以 NavMeshAgent 的寻路路径仍然穿过箱子但物理碰撞把它挡死了。解决方式静态障碍物重新 Bake 一次 NavMesh动态障碍物挂上 NavMeshObstacle并开启 Carve让 Agent 在运行时动态避让。第三条状态切换闪断。现象是玩家在 chaseRange 边缘左右横移NPC 状态在 Patrol 和 Chase 之间疯狂跳每秒切换十几次。原因是进入追逐和退出追逐用的是同一个阈值距离只要微小波动就会跨过边界条件。解决方式进入追逐用 chaseRange8退出追逐用 loseRange12中间留出 4 米的滞回区。如果你希望更稳还可以在 EnterState 里记录切换时间在 0.3 秒内不允许再次切换相当于一个简单的冷却保护。第四条转身甩头。现象是 NPC 在追逐玩家时如果玩家从它身体两侧快速经过NPC 会绕一个大圈掉头视觉上像在甩头。原因是手动 Slerp 旋转时方向向量与当前朝向接近 180 度Slerp 会绕长弧线转向。解决方式先判断当前朝向和目标方向的夹角小于 5 度就直接赋值不做插值另外把 NavMeshAgent 上的 Update Rotation 勾选关掉让转向完全交给自己的脚本控制避免两套旋转逻辑互相干扰。6. 调参与验证让 AI 更像真人而不是脚本6.1 用 Gizmos 把视野和攻击范围画出来调试 AI 参数时全靠肉眼判断距离很不靠谱。常见做法是在 GuardAI 里写一个 Gizmos 绘制函数把 chaseRange、attackRange、loseRange 直接画在 Scene 视图上。private void OnDrawGizmosSelected() { Gizmos.color Color.yellow; Gizmos.DrawWireSphere(transform.position, chaseRange); Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); Gizmos.color Color.green; Gizmos.DrawWireSphere(transform.position, loseRange); }选中 NPC 时就能看到三个同心圆黄色是进战触发范围红色是攻击范围绿色是脱战范围。调参时直接改数字Scene 视图实时更新比盲调清晰很多。6.2 三个必跑的边界场景我每次做完一个 AI demo都会强制跑三个场景纯巡逻、快速转身、贴障碍走。纯巡逻看的是巡逻点之间的衔接是否流畅快速转身看的是转向的角速度是否合适贴障碍走看的是 NavMeshAgent 的避障参数能不能处理窄通道。运行中观察 Inspector 里的 state 字段如果它在一个状态停留超过两秒基本说明切换节奏是合理的。从那以后我每次给 AI demo 加新能力都会先跑一遍这三条路径跑过了才敢把工程交给别人。这套方法帮我少调了无数个“玄学参数”希望帮到你。本文还有配套的精品资源点击获取
返回列表