简介: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 Radius | 0.5 | 胶囊体半径,越窄越容易穿缝 |
| Agent Height | 2.0 | 胶囊体高度,低于层高则无法通过 |
| Max Slope | 45 | 超过这个角度视为不可行走 |
| Step Height | 0.5 | 可迈上的台阶高度 |
| Drop Height | 1.0 | 可跳下的高度,常用于悬崖动画切换 |
烘焙完成后给 AI 胶囊体添加 NavMeshAgent 组件,Radius 和 Height 与烘焙参数保持一致,Speed 设 3.5,Angular Speed 设 240,Stopping 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 = GetComponent<NavMeshAgent>(); 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 playerInRange,Update 里用计时器或帧计数做阈值过滤。更稳妥的做法是用 OnTriggerEnter 设置进入标记,OnTriggerExit 清理标记,把距离判断放到 Update 里执行。这能避免物理回调频率和游戏逻辑频率不一致造成的问题。
4.5 烘焙完 NavMesh,AI 就是不走某块地面
现象:场景大面积是浅蓝色导航区域,但某一块地面颜色没有变蓝,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 都会在第一天就把调试可视化加进去,哪怕只是画一条线,因为等到逻辑堆起来再补,就一定会漏掉关键状态。希望帮到你。
本文还有配套的精品资源,点击获取