ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity AI Demo 搭建避坑指南:从状态机到 NavMesh 寻路实战

Unity AI Demo 搭建避坑指南:从状态机到 NavMesh 寻路实战 简介Unity AIDemo演示项目面向具备基础Unity操作经验、希望系统学习引擎内AI实现原理的开发者通过可运行的小型示例直观展示角色寻路、决策与动画反馈的完整实现路径。压缩包共178个文件以asset场景资源、cs逻辑脚本和meta资源映射信息为主体辅以csproj/sln工程配置、dll依赖库及json数据文件整体仅1.35MB结构紧凑适合边读代码边验证。内容覆盖NavMesh导航网格烘焙与Agent调优、行为树节点设计、ML-Agents强化学习接入、Animator状态机驱动动画切换以及物理碰撞、传感器感知、多智能体协作、粒子特效触发等关键知识点串联起从环境感知到行为执行的AI开发链路。已有265人学习开发者可参考脚本注释、场景配置和资源层级理解Unity AI组件的实际挂载与参数调整方式并为自主设计NPC决策、自动寻路、战斗策略等系统提供可移植模板与排错参考。1. 说清楚Unity AIdemo 到底在解决什么问题如果你在 Unity 里做过 AI大概率经历过这种场面给 NPC 挂上 NavMeshAgent写几行 SetDestination角色确实走过去了但一遇到墙角就开始疯狂抖动玩家一靠近它要么傻追到底、要么直接穿模。Unity AIdemo 不是某个现成插件而是把 AI 想法放进 Unity 场景里快速验证的一整套小工程——角色怎么移动、怎么感知敌人、怎么在巡逻和追击之间切换、要不要接大模型。它解决的是「AI 在文档里成立、放进场景就漏洞百出」的尴尬适合独立开发者、AI 应用工程师和想给团队做原型验证的人。这个方向真正的价值在于一个能跑的 demo胜过十页设计文档。下面按选型、落地、排错、演示的顺序把一条相对可靠的路走通。2. 动手前先选型行为树、NavMesh、ML-Agents 与大模型该用哪个2.1 状态机和行为树仍然是游戏 AI 的底盘很多 Unity AIdemo 一上来就找行为树插件其实没必要。对于巡逻、追击、返回、攻击这类常见行为一个枚举加 switch 的状态机就够用。状态机的思路很简单每个时刻 AI 处于一个明确状态状态之间靠条件和事件切换。比如敌人进入视野范围触发追击追击超过 N 秒且目标丢失则切回巡逻血量归零切换到死亡。状态机适合状态数量少、切换关系清晰、不需要复杂并行逻辑的场景。行为树的优势则体现在可扩展性和可视化上一个大型 NPC 可能有几十个行为节点行为树可以在不破坏整体结构的情况下增删分支也方便策划在界面上调整。Unity 编辑器本身没有内置行为树常见做法是导入 Behavior Designer 或 Playmaker也可以自研一个简单的节点执行器——但第一版 demo 真的不需要它。状态机容易被忽略的细节是「状态退出」也要处理从追击切回巡逻时需要把 NavMeshAgent 的自动刹车打开、清空 currentPath否则 NPC 还会朝着旧目标滑行。这里我习惯先列一张状态转换表再写代码避免逻辑写完才发现分支缺漏。当前状态转换条件下一状态巡逻感知到玩家追击追击玩家离开视野超过 5 秒返回返回到达巡逻起点巡逻追击距离小于攻击距离攻击2.2 NavMesh 寻路AI 移动的地基参数决定手感Unity 的寻路方案基本集中在 NavMesh 上它把场景烘焙成一张网格AI 在这张网格上计算路径。烘焙阶段有三个参数直接影响 AI 能不能「像人一样走路」Agent Radius代理半径、Agent Height代理高度、Max Slope最大坡度。半径太小会导致 AI 从墙缝里钻过去半径太大则在窄通道里找不到路Max Slope 设置过高AI 会尝试爬上很陡的斜坡动作看起来非常违和Step Height 控制能迈上多高的台阶配合动画使用要额外小心。运行时真正决定手感的是 NavMeshAgent 组件上的参数Speed 是移动速度Angular Speed 是转向速度Acceleration 是加速度Stopping Distance 是到达目标点多远就停下来。很多人只调 Speed结果发现 AI 像台没有助跑直接刹停的卡车。正确做法是把 Angular Speed 控制在 120 到 360 之间Acceleration 略大于 Speed 的两倍Stopping Distance 取 0.5 到 1.5 米这样追击动作才有减速感。还有一个很多人踩过的点SetDestination 传入目标点时如果目标不在 NavMesh 表面Agent 会一直报错并原地停留。代码里要先判断目标点是否在 NavMesh 上或者在鼠标点击场景时做一次射线检测把射线命中点转成可用的目标点再做一次 NavMesh.SamplePosition 采样确保在网格范围内。2.3 只有这几种情况才值得上 ML-Agents 或大模型Unity 官方提供 ML-Agents 工具包用强化学习训练 AI 角色适合格斗对抗、群体行为、策略博弈这类难以用规则描述的玩法。代价是训练时间长、调参玄学、对硬件有要求而且训练出来的表现在 demo 阶段未必稳定。我见过不少团队花了三周训练一个抓取动作最后效果不如十行状态机写出来的按顺序执行这个方向要在项目早期就想清楚。接入大模型是近一年很多 Unity AIdemo 的新方向典型场景是 NPC 对话、动态任务生成、玩家反馈解析。做法是把玩家输入文本发送给模型 API得到回复后由 Unity 侧解析并驱动动画和 UI。这里最核心的工程问题是「AI 响应是异步的」Unity 主线程不能直接阻塞等待必须用协程或异步方法处理回调同时要做好超时与重试。demo 阶段可以先保守一点只在 NPC 对话这类非战斗逻辑里接入大模型战斗逻辑仍然用状态机否则一次模型超时就会导致整个演示翻车。如果要做本地部署需要额外考虑模型体积和推理速度如果使用在线模型服务需要关注延迟和配额限制。我的建议是demo 期间先用最简单的规则 AI 把核心玩法跑通再考虑是否引入 ML-Agents 或大模型不要上来就上重型方案。3. 从零搭一个最小可跑的 AI Demo巡逻、追敌、丢失目标回巡逻3.1 场景搭建与 NavMesh 烘焙场景可以简化成一块 30×30 的地面、几个散布的立方体做障碍物、一个胶囊体做玩家、一个胶囊体做 AI。先处理地面和障碍物的静态标记选中地面和所有障碍物在 Inspector 右上角勾选 Static并确保 Navigation Static 也被勾选。如果不勾选静态标记烘焙时 Unity 会忽略这些物体AI 地面看起来正常但实际寻路网格是空的。打开 Window AI Navigation 面板在 Agents 页签里确认参数后点击 Bake。Unity 6 中可以通过添加 NavMeshSurface 组件并烘焙。烘焙完成后Scene 视图里会出现浅蓝色区域那就是 AI 可以走动的范围。如果蓝色区域没覆盖到预期位置先检查是不是没有标记 Static。参数建议如下表烘焙参数常用值说明Agent Radius0.5胶囊体半径越窄越容易穿缝Agent Height2.0胶囊体高度低于层高则无法通过Max Slope45超过这个角度视为不可行走Step Height0.5可迈上的台阶高度Drop Height1.0可跳下的高度常用于悬崖动画切换烘焙完成后给 AI 胶囊体添加 NavMeshAgent 组件Radius 和 Height 与烘焙参数保持一致Speed 设 3.5Angular Speed 设 240Stopping Distance 设 1.2。3.2 写核心 C# 脚本一个不依赖插件的最小 AI核心脚本承担三个任务按巡逻点依次移动检测玩家是否进入视野范围追击中目标丢失则返回上一个巡逻点。逻辑全放在一个类里用枚举区分状态。using UnityEngine; using UnityEngine.AI; public class SimpleAI : MonoBehaviour { private enum AIState { Patrol, // 巡逻 Chase, // 追击 Return // 返回 } [Header(移动参数)] public float patrolSpeed 2.5f; // 巡逻速度 public float chaseSpeed 4.2f; // 追击速度 [Header(感知参数)] public Transform player; // 玩家角色 public float sightRange 10f; // 感知距离 public float loseSightRange 14f; // 丢失目标距离稍大于感知距离 public float sightAngle 60f; // 视野角度 [Header(巡逻参数)] public Transform[] patrolPoints; // 巡逻点数组 public float arriveDistance 1f; // 到达判定距离 private NavMeshAgent agent; private AIState state AIState.Patrol; private int patrolIndex 0; private Vector3 returnTarget; // 返回时的目标点 private float loseSightTimer 0f; void Start() { agent GetComponentNavMeshAgent(); returnTarget transform.position; SetState(AIState.Patrol); } void Update() { switch (state) { case AIState.Patrol: UpdatePatrol(); break; case AIState.Chase: UpdateChase(); break; case AIState.Return: UpdateReturn(); break; } } private void UpdatePatrol() { agent.speed patrolSpeed; // 没有巡逻点时原地等待 if (patrolPoints null || patrolPoints.Length 0) return; // 到达当前巡逻点切换到下一个 float dist Vector3.Distance(transform.position, patrolPoints[patrolIndex].position); if (dist arriveDistance) { patrolIndex (patrolIndex 1) % patrolPoints.Length; agent.SetDestination(patrolPoints[patrolIndex].position); } // 发现玩家则追击 if (CanSeePlayer()) SetState(AIState.Chase); } private void UpdateChase() { agent.speed chaseSpeed; agent.SetDestination(player.position); // 玩家跑出丢失距离并持续一段时间切换返回状态 float dist Vector3.Distance(transform.position, player.position); if (dist loseSightRange) { loseSightTimer Time.deltaTime; if (loseSightTimer 3f) { returnTarget patrolPoints[patrolIndex].position; SetState(AIState.Return); } } else { loseSightTimer 0f; } } private void UpdateReturn() { agent.speed patrolSpeed; agent.SetDestination(returnTarget); // 回到目标点附近恢复巡逻 if (Vector3.Distance(transform.position, returnTarget) arriveDistance) SetState(AIState.Patrol); // 返回途中若再次看到玩家立即追击 if (CanSeePlayer()) SetState(AIState.Chase); } private bool CanSeePlayer() { if (player null) return false; Vector3 toPlayer player.position - transform.position; float dist toPlayer.magnitude; if (dist sightRange) return false; // 视野夹角判定面向目标方向与自身正前方的夹角小于视野角度一半 float angle Vector3.Angle(transform.forward, toPlayer.normalized); return angle sightAngle * 0.5f; } private void SetState(AIState newState) { state newState; loseSightTimer 0f; Debug.Log($AI 切换到 {newState}); } // 绘制调试信息在 Scene 视图里显示状态颜色和巡逻点连线 private void OnDrawGizmosSelected() { Gizmos.color state switch { AIState.Patrol Color.green, AIState.Chase Color.red, AIState.Return Color.yellow, _ Color.white }; Gizmos.DrawWireSphere(transform.position, sightRange); if (patrolPoints ! null) { Gizmos.color Color.cyan; foreach (Transform point in patrolPoints) { Gizmos.DrawWireSphere(point.position, 0.3f); } } } }状态机的核心是把行为分成了 Patrol、Chase、Return 三个分支Update 里只负责按当前状态调用对应逻辑。CanSeePlayer 里做了距离和角度双重校验距离超过 sightRange 直接返回 false再比较自身 forward 与目标方向的夹角防止 AI 背后长眼睛。sightRange 与 loseSightRange 故意拉开差距形成「发现容易、丢失难」的效果避免 AI 在边界处反复横跳。丢失目标后先记录 returnTarget设置成当前巡逻点这样 AI 返回的不是出发点而是任务中断的位置流程上更合理。state 切换统一走 SetState方便以后挂日志或播放动画。注意 Update 里没有 Stop 之类的调用因为 NavMeshAgent 收到新目标点后会自动覆盖原来的路径。3.3 用 Gizmos 和日志验证 AI 状态切换脚本里已经加了 OnDrawGizmosSelected在编辑器里选中 AI 胶囊体时Scene 视图会用颜色区分当前状态并画出感知范围半径。还有一条更实用的技巧把 SetState 里的 Debug.Log 打开然后在 Game 视图里控制玩家角色靠近、远离观察 Console 窗口里的状态切换是否与预期一致。看不到日志就先点一下 Game 视图右上角的 Console确认 Log 没有过滤掉。调试时发现 AI 不移动先看 Inspector 里 NavMeshAgent 的 Path Status 是否显示 Complete以及agent.isOnNavMesh是否为 true这两个是排查移动问题的最短路径。演示时这些调试可视化也不会难看反而会让观众更清楚 AI 的决策逻辑。4. Unity AI Demo 的 5 个经典坑从贴墙抖动到感知失效4.1 角色在墙角反复抖动现象AI 追到墙角后不停抖动像在高速原地踏步无法转向。原因Stopping Distance 太小、Angular Speed 设置得过高加上墙角处的 NavMesh 路径点落在障碍物边缘Agent 每帧都在尝试微调位置。这类问题在窄通道和墙角最容易出现多数情况下属于 Agent 的邻居避障系统与目标点冲突。解决先调大 Stopping Distance 到 1.0 以上再降 Angular Speed 到 180 左右。如果还抖检查墙角和地面有没有高度差或重叠的面片把 NavMesh 重新烘焙一次。抖动问题基本不会只靠调一个参数解决三个位置同时看才有效果。4.2 用 LookAt 转向敌人角色直接躺平现象AI 用transform.LookAt(player.position)追逐玩家结果角色身体旋转 90 度贴在地上。原因LookAt 的第二个可选参数是基准上方向默认用自身 Y 轴。当 AI 与玩家之间存在微小高度差时LookAt 会尝试让 Y 轴对准目标方向导致角色倾斜甚至躺平。解决锁定 Y 轴把玩家的 Y 坐标设成与 AI 相同或者用 Quaternion 封装Vector3 direction player.position - transform.position; direction.y 0f; if (direction.sqrMagnitude 0.01f) { Quaternion targetRotation Quaternion.LookRotation(direction.normalized); transform.rotation Quaternion.Slerp(transform.rotation, targetRotation, 10f * Time.deltaTime); }代码先去掉高度差再用 LookRotation 生成水平朝向Slerp 做平滑旋转。这段逻辑放进 Update 里AI 追敌时转身就会自然很多。4.3 几十个 AI 同时寻路帧率瞬间掉到 30 以下现象demo 里放进 30 个 AI全部开启 NavMeshAgent 后帧率直线下降移动画面卡顿。原因每个 NavMeshAgent 每帧都会做路径搜索和避障计算30 个 Agent 同时计算即使场景简单在一体机上也会明显拖慢。解决错帧更新让 AI 每隔 0.1 到 0.2 秒才调用一次 SetDestination而不是每帧都调。另外可以在 NavMeshAgent 上关闭不需要的避障功能比如将 Obstacle Avoidance Type 换成 None这对走廊里群组移动效果明显但对单兵 AI 影响较大按需求切换。还有一个常用技巧把距离远的 AI 的 Speed 直接设 0同时暂停它的寻路计算玩家靠近再恢复。4.4 用 OnTriggerStay 做感知检测逻辑一帧跑了好几次现象AI 用 OnTriggerStay 检测玩家是否在范围内结果在范围内时连续切换了好几次追击状态日志刷屏。原因OnTriggerStay 在一帧内可能被物理引擎调用多次而状态切换逻辑没有做防重复处理。连续切换不仅消耗性能还会导致动画重复触发。解决在 OnTriggerStay 里只做标记不在里面直接切换状态。常见写法是设置一个bool playerInRangeUpdate 里用计时器或帧计数做阈值过滤。更稳妥的做法是用 OnTriggerEnter 设置进入标记OnTriggerExit 清理标记把距离判断放到 Update 里执行。这能避免物理回调频率和游戏逻辑频率不一致造成的问题。4.5 烘焙完 NavMeshAI 就是不走某块地面现象场景大面积是浅蓝色导航区域但某一块地面颜色没有变蓝AI 走到边缘就像撞了空气墙。原因最常见的是该地面物体没有勾选 Navigation Static或物体的 Collider 与视觉 Mesh 不匹配。旋转过、缩放过且带 Mesh Collider 的物体烘焙时还要注意 Collider 是否是凸包否则数据异常会导致部分网格无法生成。也有一种容易被忽略的情况地面物体上有多个 Collider烘焙时 Unity 使用了其中一个。解决选中该物体确认 Inspector 右上角 Static 旁的下拉菜单里 Navigation Static 是勾选状态。重新烘焙一次或者临时给该物体加一个 Box Collider 后再次烘焙。用这种二分法能快速定位到底是被忽略的静态标记还是 Collider 形状异常。5. 把 AIdemo 做到能演示用 Gizmos 把 AI 状态可视化顺带压一次性能演示时观众最想看到的是 AI 的「想法」不是移动轨迹。一个很实用的做法是在 OnDrawGizmos 里画两条线一条从 AI 指向当前目标点颜色表示状态一条在视野范围内画扇形区域。扇形绘制不复杂用循环生成 Mesh 三角带即可。这里用 Gizmos.DrawLine 画弧线就足够演示了private void OnDrawGizmos() { if (Application.isPlaying false) return; // 画朝向线红色代表追击绿色代表巡逻 Vector3 forward transform.forward * sightRange; Gizmos.color state AIState.Chase ? Color.red : Color.green; Gizmos.DrawRay(transform.position Vector3.up * 0.5f, forward); // 画目标点连线 if (agent ! null agent.hasPath) { Gizmos.color Color.yellow; Gizmos.DrawLine(transform.position, agent.destination); } }画完这条线之后整个 AI 的决策过程在编辑器里一目了然看到玩家时红线指向玩家丢失后黄线指向返回点巡逻时绿线指向当前巡逻点。有了可视化调 Stopping Distance、抢视野范围这种参数就不再是玄学。性能方面不要只靠肉眼感受可以在 Update 末尾用一个累加器记录 Agent 移动耗时float elapsed UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong() / 1024f / 1024f;更直接的方式是打开 Window Analysis Profiler跑一分钟演示后看 CPU 曲线里 NavMesh 相关耗时。一个健康的最小 demo 中30 个 Agent 的寻路耗时应该低于 3ms如果明显偏高回到第 4.3 条做错帧处理。最后留一个个人习惯我接手的每个 AI demo 都会在第一天就把调试可视化加进去哪怕只是画一条线因为等到逻辑堆起来再补就一定会漏掉关键状态。希望帮到你。本文还有配套的精品资源点击获取
返回列表