ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity AI Demo实战:基于导航网格与状态机的NPC行为控制

Unity AI Demo实战:基于导航网格与状态机的NPC行为控制

简介:这套Unity AI演示工程面向Unity开发者和游戏AI初学者,以可运行的示例项目完整展示在引擎中接入人工智能的流程,从角色寻路、行为决策到环境感知,覆盖游戏AI的常见落地场景。压缩包共178个文件,以C#脚本、Unity资源文件、配置元数据及工程文件为主,同时包含少量动态库依赖,整体仅1.35MB,体量小巧,适合直接打开对照学习。目前已有266人学习下载,入门参考价值较高。项目重点演示NavMesh导航网格搭建、行为树节点设计、动画状态机切换,以及传感器感知、物理模拟、碰撞检测和多智能体协作,并给出ML-Agents强化学习扩展思路。通过分析场景与脚本,能快速理解AI角色从感知、决策到动作执行的完整链路,还可借鉴LateUpdate调用、动作预测平滑等性能优化做法,迁移到自己的游戏项目中,方便二次开发时直接复用。

1. Unity AIdemo 到底是什么:一份能跑的导航与状态机参考

做 AI 逻辑的时候,最怕的不是写不出行为,而是调试状态机的时候一改参数就跑飞。这份 Unity AIdemo 就是把 AI 的常见套路打包好——导航网格、感知、状态机全都有,导入后能直接跑,适合刚接触 unity AI 或者想找人形 NPC AI 参考的开发者。它解决的是三件事:让敌人追你、让 NPC 巡逻、让 AI 听到动静回头查。里面的脚本拆得比较分散,刚好适合拿来当模板,而不是一个大黑匣子。引擎版本建议选带内置 NavMesh 的版本,避免为烘焙和 Agent 单独装包。整套 Demo 只有几个场景,核心代码量不大,但每一块都卡在点上:移动不是凭空走,而是踩在导航网格上;感知不是扫一圈就完,而是结合距离、角度和遮挡判断;行为逻辑则是用一个常见状态机串起来。对你来说,这份资源最有价值的地方是它提供了一个能立刻改参数观察结果的起点。

2. 先看核心链路:导航网格、AI感知与状态机的协作方式

你要进入一个项目,第一步不是打开场景就点 Play,而是先把它分成三个互相独立的部分:移动、感知、决策。这个 Demo 的好看之处就在这——三者没有互相写在同一个类里,而是由 NPC_Controller 作为调度器,把导航组件和感知组件的结果合并到状态机上。

2.1 导航网格与 NavMeshAgent:移动不是“走过去”,而是“算出来”

AI 角色的移动是直接用的引擎内置 NavMesh 系统。导航网格本身不是运行时生成的,是一个离线烘焙出来的数据块。这个 Demo 里有一个 surface 对象,表面烘焙范围覆盖整个演示房间,烘焙出来的数据只允许 AI 走到标记为可行走的区域,不会出现穿墙或者走到桌子底下的情况。

初始化并设置第一个目标点的代码是这么写的:

using UnityEngine; using UnityEngine.AI; public class NPC_Controller : MonoBehaviour { private NavMeshAgent agent; public Transform[] patrolPoints; // 巡逻点,Inspector里拖入 void Awake() { // 获取导航组件,后面所有移动都交给它 agent = GetComponent<NavMeshAgent>(); agent.speed = 2.2f; agent.angularSpeed = 240f; agent.stoppingDistance = 0.4f; } void Start() { // 开场先往第一个巡逻点移动 if (patrolPoints != null && patrolPoints.Length > 0) { agent.SetDestination(patrolPoints[0].position); } } }

这段代码首先把移动参数、巡逻点引用前置到 Awake 中,保证任何使用 agent 的模块都不会拿到空引用。speed 控制移动速度,angularSpeed 影响转向灵敏度,stoppingDistance 是角色到达目标点后停止的距离。注意这里的 stoppingDistance 对近战型敌人要控制在 0.3 到 0.6,对远程型则要更大,不然会出现 AI 贴着目标转圈但不出手的问题。

知识点卡在这里:SetDestination 并不是把一个点“拖”过去,而是在导航网格上寻找可达路径。如果你传入的坐标点不在地网格上,返回值会是 false,角色原地不动。所以这个 Demo 里专门在巡逻点放置了地面标记对象,确保每个目标点都被烘焙数据覆盖,这也是新手最容易忽略的事。

2.2 感知系统:视觉锥与基于距离的听觉判断

感知部分拆成两个脚本:VisionSensor 和 AudioSensor。VisionSensor 负责视觉,AudioSensor 处理声音事件。视觉不是用相机实现的,用的是典型的“视野锥”算法:每隔 0.15 秒在角色前方画一个扇形区域,通过角度和距离判断目标是否在视野里。

public bool IsTargetInView(Transform target, float viewRadius, float viewAngle) { if (target == null) return false; // 先做距离粗筛,比角度判断便宜 Vector3 offset = target.position - transform.position; float sqrRadius = viewRadius * viewRadius; if (offset.sqrMagnitude > sqrRadius) return false; // 再做角度精确判断,视线方向与目标方向夹角 float angle = Vector3.Angle(transform.forward, offset.normalized); if (angle > viewAngle * 0.5f) return false; // 最后做遮挡检查,避免隔着墙也能看到 if (Physics.Raycast(transform.position + Vector3.up * 0.5f, offset.normalized, out RaycastHit hit, viewRadius)) { return hit.transform == target; } return false; }

先算模长的平方,减少开方调用;再用 Vector3.Angle 判断是否落在视野锥内;最后用射线过滤遮挡。这里把射线的起点抬高 0.5 米,避免从地面扫到自己的 Collider 导致误判。viewAngle 是锥体张角,传 120 就表示单侧 60 度。真正在实际调试里需要改的不是 viewRadius,而是这条射线起始高度,角色蹲姿和站姿对视野影响比想象大得多。

听觉判断相对简单:场景里放了一个事件源,当玩家跑动时触发一个带半径的 SphereCollider,AudioSensor 接到 OnTriggerEnter 事件后通知状态机进入搜索状态。这套实现的好处是听觉是事件驱动不是每帧轮询,不消耗 Update 开销。

2.3 有限状态机:巡逻、追踪、搜索与返回的切换逻辑

状态机是这部分的重头戏。状态定义在枚举里,当前状态由 NPC_Controller 统一管理。四个状态分别是:Patrol(巡逻)、Chase(追踪)、Search(搜索)、Return(返回巡逻起点)。每次 Update 都走一次状态机,但不允许同一个逻辑帧内连续切换两次状态,避免抖动。

public enum AIStateType { Patrol, Chase, Search, Return } private AIStateType currentState = AIStateType.Patrol; void Update() { switch (currentState) { case AIStateType.Patrol: PatrolUpdate(); break; case AIStateType.Chase: ChaseUpdate(); break; case AIStateType.Search: SearchUpdate(); break; } } void PatrolUpdate() { // 到达当前巡逻点后,切换到下一个点 if (agent.remainingDistance <= agent.stoppingDistance) { MoveToNextPatrolPoint(); } } void ChaseUpdate() { // 追踪优先级最高,目标丢失后不直接巡逻,而是先搜索 if (!visionSensor.IsTargetInView(player, viewRadius, viewAngle)) { currentState = AIStateType.Search; searchRemainTime = 3.0f; } else { agent.SetDestination(player.position); } }

关键在 ChaseUpdate 的丢失处理:没有立刻回到 Patrol,而是先进 Search 状态,站在最后看到玩家的位置上原地转 3 秒,再用 OnTriggerEnter 的听觉线索决定下一步。这个“丢失后先搜索”的设计,比直接掉头巡逻在真实感上提升很多,也是很多大厂 NPC 的标准做法。抄的时候要注意,状态机里所有状态必须有超时保护,比如 Search 状态倒计时跑完自动回 Patrol,而不是永远卡在一个死分支上。

3. 从零跑通 Demo:导入工程、烘焙导航网格与关键配置

这章能把前面讲的逻辑落到编辑器里。从导入到 Play,一共分三步:确认目录导入完整、烘焙导航网格、给角色挂组件。

3.1 导入后的目录结构:先把每个文件的功能对应上

导入完成后,你会看到以下几个关键目录。我一般建议先对照一遍,避免出现脚本丢失或场景引用断掉的情况:

目录或文件作用是否需要改
Assets/Scenes/AI_NPC_Demo.unity主演示场景,所有 AI 对象都在场景里直接改
Assets/Scripts/AI/NPC_Controller.cs状态机调度入口核心文件
Assets/Scripts/AI/VisionSensor.cs视野锥检测参数调整
Assets/Scripts/AI/AudioSensor.cs听觉触发检测一般不改
Assets/Scripts/Player/Player_Move.cs玩家控制,用于触发 AI 行为可换

导入后如果看到场景里的 AI 角色是粉红色,大多是材质或者着色器不兼容。这时候先别急着去装额外资源,在 Project 面板找到预设体材质,把着色器换成引擎内置的 Standard 即可。这个坑在 Demo 资源里出现概率极高,不是代码问题。

3.2 烘焙 NavMesh 的三个关键步骤

打开 Window -> AI -> Navigation,选择 Navmesh 烘焙页面。这个 Demo 已经提前把场景里的地面、墙壁、障碍物标记为 Navigation Static 了,所以你只需要做三步:

  1. 在 Bake 面板确认 Agent Radius 为 0.5,Agent Height 为 1.8(这个 Demo 的角色身高默认 1.8 米)。
  2. 把 Step Height 设为 0.3,意味着角色能正向上 30 厘米的台阶,不要设得太大,否则会爬上不该爬的桌子。
  3. 点击 Bake,看到场景上出现蓝色半透明区域就算成功。

这一步会出现一个常见误会:很多人以为调大了 Agent Radius 就能让 AI 走更宽的路,实际上 Agent Radius 是圆柱体的半径,调大了反而把窄通道堵死。你要理解这个值代表 AI 角色的“肚子”,不是路的宽度。数值变成 0.8 以上后,大多数门洞都会被判定不可通过,AI 会绕远路甚至卡在门口。

我给这个 Demo 调参数时的经验是:先按角色实际尺寸调 Radius 和 Height,再看看 Step Height 和爬坡角度,最后才考虑偏移。顺序最好不要反,不然你以为是路的问题,其实是角色尺寸把它堵住了。

3.3 配置 AI 角色:从空物体到完整状态机

场景中有一个名为 Enemy_Robot 的预设体,它挂在 NavMeshAgent、VisionSensor、NPC_Controller 三个核心组件。组件配置按下面的表走:

组件关键属性推荐值备注
NavMeshAgentSpeed / Angular Speed2.2 / 240追踪时可以动态改速度
NavMeshAgentStopping Distance0.4与攻击距离匹配
VisionSensorView Radius / View Angle6 米 / 120 度不要低于 5 米否则 AI 像瞎子
VisionSensorDetection Interval0.15 秒越低越吃性能
NPC_ControllerPatrol Points4~6 个点用空物体摆出路线

挂上去以后,把巡逻点拖进数组的顺序很关键。脚本是按下标顺序走通的,当前点完成后寻找下一点。摆点时不要用同一个平面高度,最好形成折线,AI 走起来才有“在巡逻”的感觉,而不是一条直线来回走。

提示:Play 之后如果发现 AI 往错误方向跑,先检查两个地方。第一,导航网格是否成功烘焙,蓝色区域是否覆盖巡逻点;第二,巡逻点是否放在了地上而不是在半空中。巡逻点悬空是新手最容易踩的问题,因为 NavMeshAgent 会找不到落点,原地旋转。

4. 调参落地套路:速度、视野与巡逻路线怎么改不翻车

在 Demo 能跑了之后,调试才是大头。这一章直接对着具体场景讲参数,避免你打开 Inspector 后不知道该动哪个。

4.1 速度与转向参数:移动的“手感”和“物理感”

先把角色速度拆成两类:移动速度和角速度。移动速度直接决定游戏难度,角速度则影响转向时是否会出现“漂移感”。

void UpdateChaseSpeed(bool isChasing) { // 追人时加速度快一点,让刺激感上来 if (isChasing) { agent.speed = Mathf.Lerp(agent.speed, 3.5f, Time.deltaTime * 2f); } else { agent.speed = Mathf.Lerp(agent.speed, 2.2f, Time.deltaTime * 2f); } }

这里用了 Mathf.Lerp 做平滑过渡,不至于让速度从 2.2 瞬间跳到 3.5 出现“瞬移”感。如果你觉得角色的表现像踩了冰面,主要查 angularSpeed,当角速度低于 120 时,角色走到目标点会经历夸张的“头先转向、身体再跟上”过程。调试小技巧:把 NavMeshAgent 的 AutoBraking 关掉,配合更大的 StoppingDistance,AI 会在到达目标点前减速,看起来更像人而不是撞墙的机器人。

速度参数不是越大越好。巡逻状态保持 2.2 左右,追踪状态 3.5 是大多数演示场景的合理区间。如果地图很小,3.5 会导致 AI 反复冲过目标点再回头,看起来非常笨。地图大就反过来,低速会让玩家觉得 NPC 在散步。

4.2 视野锥与感知半径:不要只调一个“视野距离”

视野锥有两根轴:半径和角度。追人时半径可以开大,但角度不要超过 150 度,因为背后还能感知到目标会显得很假。实战中我一般会额外加一个“警报延迟层”:目标进入视野后,需要连续多次确认才切换状态,避免因为目标瞬移到一个柱子后就立刻丢失追踪。

if (framesInView >= 3) { lastKnownPosition = player.position; currentState = AIStateType.Chase; } else { framesInView = visionSensor.IsTargetInView(player, viewRadius, viewAngle) ? framesInView + 1 : 0; }

这段代码解决的是视野判断的抖动问题。当目标在边缘或障碍物后面快速进进出出时,直接切状态会抖成“抽搐”,至少连续 3 帧确认是一个便宜且可靠的方案。代价是玩家在快速冲刺时,AI 的反应会慢一帧,实际感受基本无感。缺省情况 framesInView 初始化为 0。

感知半径和视野角度需要配合地图尺寸来定。6 米半径、120 度角在 10 米宽的房间里会太灵敏,AI 站门口就能看到房间里所有动静。我一般把半径设成地图最长边的四分之一,角度保持在 110 到 130 之间。

4.3 巡逻路线设计:路径点、死胡同和回归逻辑

巡逻路线是这个 Demo 里最具迷惑性的部分。因为看起来只是摆点,但点与点之间的距离、高度差和视线遮挡都影响最终表现。一个常见坑是把巡逻点摆成环形,AI 一路走到底后回到起点,形成单调循环。改成两三点折返式路线,比如点 A 到点 B 到点 C 再回到点 A,会让 NPC 有一种“正在检查区域”的感觉。

把巡逻点做成 GameObject,然后挂一个简单的 Gizmo 脚本,能在场景视图中画出连线:

void OnDrawGizmos() { Gizmos.color = Color.yellow; for (int i = 0; i < patrolPoints.Length; i++) { if (i + 1 < patrolPoints.Length) { Gizmos.DrawLine(patrolPoints[i].position, patrolPoints[i + 1].position); } } }

这个脚本本身不参与运行时逻辑,只提供可视化的调试。实际调路线时,我一般会把最后一个巡逻点和第一个点重合,避免 AI 巡完一圈后站在终点发呆。另一个小习惯:巡逻点的空物体位置不要放在碰撞体内部,不然导航网格会把它判定为不可达。

5. 避坑排查:五个最典型的 Unity AI 翻车点

任何 Demo 跑起来不难,难的是出了问题之后你知道去哪里查。这章记录我复盘这个 AI demo 时踩过的五个高频坑,每个都按现象、原因、解决来拆。

5.1 高频问题清单:现象、原因与对应解法

问题一:角色到达目标点后原地转圈,就是不进入下一状态。

现象:agent.remainingDistance 一直在 0.3 和 0.4 之间跳动,目标点看起来已经到了。 原因:stoppingDistance 设置得太小,角色还没走到判定区内就已经被物理碰撞挡住,系统反复尝试走最后一点距离。 解决:把 stoppingDistance 提高到 0.5 以上,或者直接用 Agent.Raycast 判断是否离目标点小于 0.3。不要在 remainingDistance 上做精确大于小于比较,浮点误差会让你怀疑人生。

问题二:视野锥能看到目标,但状态机不触发追踪。

现象:Gizmos 已经画出视野范围,目标也在扇形内,但 AI 没反应。 原因:射线遮挡检查把自己身上的 Collider 挡了,射线从角色中心发出时先撞到自己。 解决:射线起点改为头部高度,发射前用 Physics.QueriesHitTriggers 或者给自身 Collider 加忽略层。这个 Demo 里已经做了起点上移,但只要你自己改过角色尺寸就要重新调。

问题三:AI 在追踪和巡逻之间“震荡”。

现象:AI 刚跑两步就回到巡逻,过一秒又开始追。 原因:目标位置在导航网格边缘来回变化,或者是检测逻辑只有单帧确认。 解决:用连续几帧确认的模式,并且追踪状态下不要每帧更新 SetDestination,改成 0.2 秒更新一次。同时在目标离开视野后至少延迟 1 秒再回巡逻。

问题四:AI 在迷宫地图里绕远路,明显不聪明。

现象:明明有一条直线路径,AI 却绕了整个房间一圈。 原因:导航网格的 Agent Radius 太大,或者地图上的窄通道没有被正确标记为可行走。 解决:把 Agent Radius 降到 0.3 到 0.5,重新烘焙。再检查一些低矮障碍物是不是被误标成了静态遮挡物,把不必要的静态标记去掉。

问题五:加载场景后 AI 全部朝同一个方向跑。

现象:所有 NPC 开场直奔地图右上角,完全不理会巡逻点。 原因:巡逻点 GameObject 被误删,脚本的 patrolPoints 数组长度为 0,Start 里试图访问第一个元素导致初始化失败。 解决:让 Start 函数在数组为空时使用空物体占位,并用 Debug.Log 打开提示。这属于写代码时的防御性习惯,Demo 里特意把这个坑留出来了。

5.2 性能排查:NavMesh 帧消耗与脚本轮询的取舍

除了功能问题,性能是用户拿来就跑的第二个坎。NavMeshAgent 本身每帧都要做路径更新,如果场景里放 10 个以上 AI,物理引擎和寻路计算会同时吃帧。我在测试时习惯打开 Profiler,盯着两件事:第一,NavMeshQuery 的调用量;第二,Update 里的 SetDestination 调用频次。

如果 Profiler 显示 CPU 时间集中在 NavMeshQuery 上,优先考虑降低 SetDestination 更新频率,或者把部分 AI 放到协程里做间隔更新。视野检测的轮询最容易掩盖真实性能问题,实际上一秒 12 次足够,不需要每帧都扫。把这些扫描操作放到协程:

IEnumerator VisionLoop() { while (true) { // 每0.08秒做一次视野检测,降低更新频率 yield return new WaitForSeconds(0.08f); CheckVision(); } }

协程与每帧 Update 的区别在于可控性。把视野检测从 Update 中拆出去后,AI 角色数量多两倍也能保持稳定。记住,物理 Trigger 和 Raycast 是最贵的,能用距离判断的先做粗筛,能按帧率的才按帧率。

6. 进阶改造:用一棵行为树替换固定状态机

Demo 里的状态机适合作为模板,但它有个明显的边界:当 AI 新增要互动的行为(比如开箱子、与队友配合),状态机会膨胀成一张混乱的箭头网。那次我把巡逻、追踪、攻击、开箱四个行为加起来后,发现状态切换条件互相重叠,于是决定改成行为树。

6.1 状态机的扩展性弱点

状态机适合线性行为,而当你有“先看有没有敌人,再看有没有障碍,然后决定绕路还是攻击”这类嵌套逻辑时,行为树能让优先级看得一清二楚。改动时可以保留原来的 NavMeshAgent 和 VisionSensor,只替换决策层。

6.2 最小行为树脚本

public enum NodeState { Success, Failure, Running } public abstract class BTNode { public abstract NodeState Execute(); } public class SequenceNode : BTNode { private BTNode[] children; public SequenceNode(BTNode[] nodes) { children = nodes; } public override NodeState Execute() { // 顺序执行,一旦失败就返回失败,全部成功才成功 foreach (var node in children) { var state = node.Execute(); if (state != NodeState.Success) return state; } return NodeState.Success; } }

SequenceNode 按顺序执行所有子节点,遇到失败就提前返回。改造时把“巡逻到目标点”当成叶子节点,把“判断视野内是否有玩家”当成条件节点,根节点是一个并行或选择节点。这些最小节点段可以用 100 行内实现,整个 AI 行为就完全变成了可配置的数据,而不是写在状态机里的分支判断。

6.3 验证改造结果

改完后验证方法很简单:把原来跑到场景里手动测试的时间换成在固定帧里打印状态机增量日志,看每个行为的进入次数。如果一道门让 AI 绕路了 5 次,但日志只有 2 次记录,说明行为树的条件判断不够严。

从那以后我每次进一个新的 AI Demo,都会先看它是状态机还是行为树,再看感知层和决策层有没有分开。不是状态机不好,而是在真正要长期维护的项目里,行为树的数据化表达能让我少撑很多夜。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进