尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Unity AI demo拆解:导航网格寻路、状态机与感知系统实战

发布时间:2026/9/29 7:17:57

资讯中心
01
ARTICLE

Unity AI demo拆解:导航网格寻路、状态机与感知系统实战

Unity AI demo拆解:导航网格寻路、状态机与感知系统实战
简介Unity AI Demo是一份面向Unity开发者及游戏AI学习者的演示工程集中展示导航网格、行为树、机器学习等常见AI技术在引擎中的落地方式。压缩包共178个文件大小仅1.35MB内部包含Unity场景与资源文件asset、meta、C#逻辑脚本cs、程序集与配置数据dll、json、xml等结构与标准Unity项目一致方便直接打开场景运行和排查代码。目前已有265人学习下载。项目中重点覆盖NavMesh自动寻路、行为树决策、ML-Agents强化学习、动画状态机切换、物理碰撞检测以及性能优化等模块并演示了多智能体间的协作与粒子特效触发。通过实际场景和脚本读者可以理解AI角色从感知环境、做出决策到驱动动画的完整闭环学会烘焙导航网格、设计行为树节点并借鉴其代码风格实现更复杂的角色逻辑是一份能快速上手的Unity AI工程参考。1. Unity AI demo别只看移动先弄明白它到底给了你什么很多刚接触 unity AI 的人做的第一个 demo 无非是让一个 Capsule 去追另一个 Capsule代码里写死一个Transform.LookAt加一个Translate场景里看着动了就觉得 AI 学会了。这份 unity AIdemo 的工程我在 Unity 2022.3 LTS 下完整跑通过它跟那种玩具级写法最大的区别在于它把导航网格寻路、状态机、行为树、感知判定和动画同步拆成了独立模块每一层都可以单独开关和调参。适合两类人一类是想给敌人 AI 换掉“直勾勾地追击”逻辑的实战新手另一类是已经在做战斗原型、想快速接一套 AI 控制器的熟手。我要强调一点这份资源解决的核心问题不是“让物体移动”而是“让 AI 决策和移动分离”把巡逻、追击、攻击这些状态老老实实地编排起来这才是它值得复现的原因。2. 工程拆解场景结构、AI 分组和运行链路是第一步2.1 先看场景和目录别急着改代码打开工程后第一件事不是点 Play而是先看Assets/Samples/下的目录规划。这份 AIdemo 按功能把脚本分成Core基类、FSM状态机、BT行为树节点、Perception感知、Navigation导航五个文件夹场景则按演示目的拆成01_NavMeshStandalone、02_StateMachine_FPS、03_BehaviorTree_Squad三个。这种分法的价值在于当你发现某个 AI 行为异常时你能直接定位到对应模块而不是要在几百行Update()里面大海捞针。场景结构上最值得留意的是每个 AI 物体身上并不是挂了一大堆自定义脚本而是AgentBrain只做高层决策真正的移动逻辑全在NavMeshAgent组件上。也就是说AI 的“大脑”和“腿”是分离的。AgentBrain负责问“我现在该做什么”NavMeshAgent负责回答“怎么走到那里”。这个分离思路你没跑过工程可能觉得多余等你真的把重力、物理碰撞、动画根运动混在一起时就知道这是唯一的止血方案。2.2 运行链路黑匣子里面的调用顺序我把它实际运行时的事件顺序拉出来给你看一下。Start 阶段AgentBrain获取NavMeshAgent引用并关闭autoBraking场景里所有巡逻点会在OnEnable阶段统一注册到一个PatrolPointRegistry单例里。Update 阶段每帧先由PerceptionSystem检查玩家是否进入视野范围结果写入brain.CanSeePlayerAgentBrain读取该状态并决定切换 FSM 状态目标点确定后调用agent.SetDestination(target)最后动画组件通过agent.velocity驱动 Blend Tree。这段链路里有几个看似不起眼的关键点。第一PatrolPointRegistry用了静态单例避免每个敌人都在Inspector里手动拖十几个巡逻点新增巡逻点零配置。第二感知结果和决策结果分帧缓存防止同一帧内行为状态被重复触发。第三动画没有直接读取transform.position差值而是读agent.velocity这能避免动画和导航速度不匹配导致的滑步。using UnityEngine; using UnityEngine.AI; public class AgentBrain : MonoBehaviour { public bool CanSeePlayer { get; set; } public Vector3 LastKnownPosition { get; set; } private NavMeshAgent agent; private void Start() { agent GetComponentNavMeshAgent(); agent.autoBraking false; // 不自动减速让状态机自己控制 agent.updateRotation true; // 交给 NavMesh 自动转身 } private void Update() { // 感知结果由 PerceptionSystem 写入决策只读结果 if (CanSeePlayer) { LastKnownPosition PerceptionSystem.Instance.Player.position; } if (CanSeePlayer !agent.hasPath) { agent.SetDestination(LastKnownPosition); } } }这里的autoBraking false是常见争议点。导航网格代理默认在接近终点时会自动减速结果就是敌人追击时出现“快到了就刹车、再到再加速”的节奏卡顿。关掉自动刹车后stoppingDistance由你自己控制状态机就可以在攻击距离上稳定触发。updateRotation建议保留为 trueAI 的转身交给angularSpeed调速度不够时再去考虑手写插值。3. 寻路与状态切换把巡逻、追击、攻击用 NavMeshAgent 串起来3.1 FSM 基础框架状态由数据驱动而不是用一堆 if这份 demo 中 02 场景的状态机写法值得抄它没有用if (distance x) ChangeState(Chase)这种散弹式判断而是让每个状态继承一个IState接口自己负责Enter()、Execute()、Exit()。状态机本体只维护一个“当前状态”引用Execute()每帧只调用一次。你真正需要关心的是ChaseState里面对于“到达目标”的判断。很多工程翻车就翻在这里直接用agent.remainingDistance 1f去判定结果玩家站在一个悬空的平台上remainingDistance永远大于 1AI 就在悬崖边缘转圈。正确做法是先看agent.pathStatus是否为PathComplete再看remainingDistance并且给导航路径加入“高度落差检查”。这套 demo 里的处理方式是目标点每 0.5 秒重新采样一次采样点贴地后才传入SetDestination。public class ChaseState : IState { private readonly AgentBrain brain; private readonly NavMeshAgent agent; private float retryTimer; public void Execute() { retryTimer - Time.deltaTime; if (retryTimer 0f) { // 重新计算目标点每 0.5 秒一次避免每帧重算路径 if (NavMesh.SamplePosition(brain.LastKnownPosition, out NavMeshHit hit, 2f, NavMesh.AllAreas)) { agent.SetDestination(hit.position); } retryTimer 0.5f; } if (agent.pathStatus NavMeshPathStatus.PathComplete agent.remainingDistance agent.stoppingDistance) { // 到达目标状态机切到攻击 brain.ChangeState(new AttackState(brain)); } } }注意我写的NavMesh.SamplePosition这一步是整个状态的保险丝。玩家位置可能在跳跃时高于导航网格表面直接拿 y 坐标当目标点会让 AI 绕大圈子去找寻路端点。把目标点投影到导航网格表面高度落差超过 2 米的点就直接丢弃这一条下来能省掉大量“AI 走不过去”的玄学问题。retryTimer的作用是降低SetDestination的调用频率SetDestination本身会触发一次完整的路径规划大批量敌人每帧都调用的话性能会明显劣化。3.2 NavMesh 烘焙和代理参数调数值的顺序很重要在 02 场景里打开 Navigation 窗口菜单栏Window AI Navigation你会看到它把地面、斜坡、台阶分别设了不同 Area。这套设置的背后逻辑不是美术分层而是为了让跳跃点、楼梯口这些位置有更高的通过代价。如果只是做平地巡逻全部用Walkable就行一旦场景有上坡、台阶、窄门你就要给不同表面分配负担。调 NavMeshAgent 参数的顺序建议固定为radius-baseOffset-acceleration-angularSpeed-stoppingDistance。先调radius因为半径决定了代理能不能穿过窄门半径 0.5 的代理在 1.2 米宽的走廊畅通无阻但你把它调到 1同样的走廊就变成死路。然后是baseOffset这个值控制头顶到地面的偏移不匹配会导致角色悬浮或陷入地面。acceleration默认值不适用于追逐场景敌人从巡逻切到追击时加速度太小会显得反应迟钝通常我会把追击状态的加速度拉到 12 以上。angularSpeed是另一个容易忽略的坑。它默认是 240对大多数情况够用但如果你用了动画根运动角色的朝向和实际行进方向会打架。我一般在 AI 初始穿装备后检查desiredVelocity方向与transform.forward的夹角夹角持续超过 30 度就说明角速度跟不上寻路拐弯把angularSpeed再往上提到 360或换用updateRotation false手写旋转。// 调试用画出代理当前希望移动的方向 private void OnDrawGizmosSelected() { if (agent null) return; Gizmos.color Color.yellow; Gizmos.DrawRay(transform.position, agent.desiredVelocity * 2f); Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, agent.stoppingDistance); }这段代码放进AgentBrain里选中物体时就能看到两个圆和一根射线。黄色射线是导航核心给代理的期望速度方向红色球是停止距离。如果射线方向和玩家位置方向偏移超过 45 度说明路径规划绕了一大段多半是导航网格烘焙没覆盖全优先回去查 Mesh 边界而不是调速度参数。这一步是小成本换大确定性我复现任何 AI 工程都强制先做一层可视化。4. 让 AI 看起来聪明感知判定、lookAt 平滑和动画同步4.1 感知不是“距离够近”而是“视野角度 距离 遮挡”03 场景里的敌人对玩家的感知用了三层判定距离、角度、射线遮挡。光是距离和角度还不够中间隔着墙时敌人应该看不到玩家所以必须加射线检测。这里有个偷懒写法是直接Physics.Raycast放到 Update 里但这个 demo 用的是固定间隔 0.2 秒采样一次配合NavMeshAgent.velocity判断物体是否在移动效果差异肉眼可见。判断视野角度的核心是Vector3.Angle(transform.forward, directionToPlayer)它返回夹角绝对值。你需要注意的是这个函数返回的是 0 到 180 度不要直接拿它和fieldOfView / 2比较后就没下文。还要同时判断距离是否被障碍物遮挡。遮挡检测的射线起点不要取角色根部否则敌人蹲下或玩家趴在掩体后会出现漏判取transform.position Vector3.up * 1.5f也就是眼睛高度最稳。public bool IsTargetVisible(Transform target, float viewAngle, float viewRange) { Vector3 eyePos transform.position Vector3.up * 1.5f; Vector3 targetPos target.position Vector3.up * 1.2f; Vector3 dir targetPos - eyePos; // 距离判定 if (dir.magnitude viewRange) return false; // 角度判定夹角是否在扇形范围 if (Vector3.Angle(transform.forward, dir) viewAngle * 0.5f) return false; // 遮挡判定到目标的射线是否撞到障碍物 if (Physics.Raycast(eyePos, dir.normalized, out RaycastHit hit, viewRange, ~LayerMask.GetMask(Enemy))) { if (hit.collider.transform ! target) return false; } return true; }LayerMask.GetMask(Enemy)这行是典型的性能细节。射线检测默认会撞上 AI 自己的碰撞体如果不把它排除掉几乎每帧都会因为撞到自己而误判“被挡住”。另一个通用做法是让辅助视野碰撞体单独放一个 Layer这样就不需要排除多个层级。还有一点viewAngle的单位是度扇形的半角是viewAngle * 0.5f如果你写成viewAngle整个值AI 的视野会变成原来的两倍看起来像开了透视。4.2 转身动画LookAt 不是直接转而是要“追着转”场景里 AI 的转身有两种姿态切换。一种是小角度小于 30 度微调直接依赖NavMeshAgent.updateRotation另一种是大幅转身如果直接transform.LookAt(player)动画会瞬间切到跑步状态看起来像抽搐。这份 demo 的处理方式是updateRotation true时把angularSpeed调到能够匹配动画转身速度需要大角度转身时先关掉updateRotation自己写插值转身。插值转身的核心是用Quaternion.RotateTowards而不是Quaternion.Slerp。Slerp的插值系数跟帧率相关容易转得过头或者欠转RotateTowards接收的是固定角速度旋转量不会超过单帧目标稳定很多。实战里我建议朝向差值大于 90 度时额外播放一个转身动画动画时长和RotateTowards的速度按比例设置不然会出现“动画播到一半角色已经对准目标”的穿帮。private IEnumerator RotateToTarget(Vector3 target, float speed 180f) { float angleLeft 999f; while (angleLeft 0.5f) { Vector3 dir target - transform.position; dir.y 0; angleLeft Vector3.Angle(transform.forward, dir); Quaternion targetRot Quaternion.LookRotation(dir.normalized, Vector3.up); transform.rotation Quaternion.RotateTowards(transform.rotation, targetRot, speed * Time.deltaTime); yield return null; } }注意我在循环里把 dir 的 y 轴清零了。物体在地面上转身时只需要旋转围绕 Y 轴的分量如果你保留 y 轴角度差角色的头会仰起来或者俯下去产生一种“点头”的怪异感。配合动画时的技巧是转身协程启动时重置agent.velocity因为导航代理的速度矢量在转身期间仍然指向旧方向不重置的话角色会一边转身一边滑步。4.3 动画同步不要把 Update 里的速度直接灌给 AnimatorAI 的移动动画通常用一个 Float 参数控制很多人直接用animator.SetFloat(Speed, agent.velocity.magnitude)帧率不稳时动画会出现细碎抖动。这份 demo 的做法是用Mathf.SmoothDamp做一个速度阻尼让动画参数平滑逼近实际速度阻尼时间设置在 0.1 到 0.2 秒之间兼顾反应速度和视觉平滑。private float animSpeedCurrent; private float animSpeedVelocity; void Update() { float targetSpeed agent.velocity.magnitude / agent.speed; animSpeedCurrent Mathf.SmoothDamp(animSpeedCurrent, targetSpeed, ref animSpeedVelocity, 0.15f); animator.SetFloat(MoveSpeed, animSpeedCurrent); }这里的分母用agent.speed而不是直接使用velocity.magnitude是为了让速度参数归一化到 0 到 1 的范围方便 Blend Tree 设置阈值。如果你的角色有“慢走”和“快跑”两种姿态还需要再分两个SmoothDamp管道否则切换姿态时容易一卡一卡的。我从复现这个 demo 后养成的习惯是所有 Animator 的速度相关参数必须经过阻尼后再传入从来没有例外。5. 避坑复现 Unity AI demo 时最容易翻车的五个点5.1 现象AI 卡在墙角腿在动但走不过去原因大多是NavMeshAgent.radius大于门洞或走廊宽度。烘焙好的导航网格允许代理经过但代理自身半径让它撞墙。还有一个隐蔽原因是ObstacleAvoidanceType设置过高代理为了躲避其他 AI 或障碍物在窄通道里来回绕圈。解决先查agent.radius把走廊宽度和半径做一次对比。然后调ObstacleAvoidanceType场景里 AI 数量少于 10 个时用HighQuality没问题数量超过 20 个就要降到MediumQuality窄通道的躲闪才有足够余量。最后看agent.path的corners数组如果发现路径点反复来回多数是导航网格在某处断层回去修 Mesh 而不是调参数。5.2 现象敌人追到中途突然停住不动就像目标消失原因SetDestination的目标点落在了导航网格外比如玩家跳到箱子顶、站在斜坡边缘。NavMeshAgent无法生成抵达路径时会迅速把pathStatus置为PathInvalid但很多代码只判断remainingDistance没判断pathStatus于是 AI 停在原地维持追击状态。解决改正为我在第 3 章提到的NavMesh.SamplePosition方案每 0.5 秒采样一次。采样半径不要太慷慨1.5 到 2 米足够。另一种补充方案是给pathStatus加监听一旦发现PathInvalid立即回到巡逻状态给 AI 一个“认知恢复”的退路。5.3 现象多个敌人追一个玩家时互相挤位甚至出现抖动穿模原因NavMeshAgent之间的避障靠 RVO速度障碍实现如果所有代理的radius都相同且stoppingDistance较大它们会互相堵死。加上攻击状态里没有为不同敌人设置“攻击占位”多个敌人同时追同一个玩家时就会重叠。解决给不同敌人分配不同的radius或让它们进入攻击状态后agent.isStopped true只保留离玩家最近的一个保持移动。更精细的做法是给敌人分配攻击槽位玩家周围挂 4 个空物体作为攻击位每个敌人追击时选最近空位为目标这样队形也会好看很多。5.4 现象动画状态机切换正常但角色脚下像踩了冰滑步严重原因动画播放速度高于或低于实际移动速度。Animator里的MoveSpeed参数虽然名目相同但不同人用不同参照系有的直接传velocity.magnitude有的传transform.position的帧间差值数值之间互相不匹配。解决统一用agent.velocity除以agent.speed作为动画基准值动画树里只允许一个地方写入这个参数。需要微调时打开 Animator 的Scene Preview分别在慢走和快跑两个阈值下对比角色位移和动画脚步位移再调 Clip 本身的播放速度不要去调脚本里的假想系数。5.5 现象AI 在移动过程中转头但转身完路径又变回原方向原因updateRotation为 true 时NavMeshAgent每一帧都会把角色朝向纠正到路径切线方向。你在感知脚本里调用了transform.LookAt或手写了协程但下一帧又被寻路系统抢回去于是看起来像“转过去又转回来”。解决手写转身时先设agent.updateRotation false等拿到目标朝向且角度差值小于 5 度再恢复为 true。协程里必须有类似的退出条件不然updateRotation被永久关闭角色会失去导航的自判断旋转能力。从那以后我每次做 AI 转身都会检查updateRotation的开关时序问题。6. 给 AI 装一个可视化调试面板把黑匣子变成可调参数复现这份 demo 到最后阶段我强烈建议你加一个“AI 调试模式”。做法很简单写一个AIDebugWindow编辑器脚本挂在场景里使用[ExecuteAlways]让它在不播放时也能显示把每个 AI 的state、remDistance、desiredVelocity、CanSeePlayer全部列到 Inspector 上。使用场景时按一下按键就能暂停并按时间顺序查看 AI 的状态变化。using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class AIDebugInfo : MonoBehaviour { [TextArea] public string DebugInfo; private NavMeshAgent agent; private AgentBrain brain; void Start() { agent GetComponentNavMeshAgent(); brain GetComponentAgentBrain(); } void Update() { if (Application.isPlaying) { DebugInfo $State: {brain.CurrentStateName}\n $Remaining: {agent.remainingDistance:F2}\n $DesiredV: {agent.desiredVelocity}\n $SeeingPlayer: {brain.CanSeePlayer}; } } }这样你选中任何一个 AI 就会在 Inspector 里直接看到它的决策状态。再加上一行Debug.DrawLine(transform.position, brain.LastKnownPosition, Color.cyan)把 AI 记忆中“最后看到玩家的位置”画出来你会发现很多看似玄学的 AI 问题其实只是感知层输出错误。我自己的习惯是每次调 AI 行为时先看DesiredV方向正不正确再判断是不是状态切换导致平时为“AI 为什么不追我”排查浪费掉的时间能在这里砍掉一大半。还有一个最管用的验证路径把场景暂停逐一修改chaseRange、viewAngle和agent.speed看 AI 的表现是否符合预期。这种调试面板最大的价值是把决策和参数分离你不再需要去读一堆状态代码而是直接在场景里看到“这个参数对应这个行为”。从那以后我接手任何 AI 工程第一步都会强制先做这层可视化再谈改逻辑。希望这份 unity AIdemo 的拆解和踩坑经验能帮你在 AI 调试路上少走一段弯路。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。