
简介一份面向Unity开发者的AI实现参考Demo通过可运行项目演示导航网格、行为树、ML-Agents强化学习等核心AI技术的落地方式解决游戏角色自动寻路、行为决策与智能交互等常见开发难题适合初中级开发者系统学习。资源共178个文件主要包含C#脚本、场景asset、Unity配置与meta元数据以及少量dll和工程文件RAR压缩包仅1.35MB轻量便于解压后直接分析。目前已有266人学习。Demo实际覆盖NavMesh烘焙与Agent参数调节、行为树决策节点设计、动画状态机切换、物理碰撞与传感器感知并展示了多智能体协作、粒子特效触发和性能优化技巧例如利用LateUpdate减少不必要的计算。打开工程即可对照脚本与场景看清AI从感知、决策到动作执行的完整链路有助于在自有项目中快速落地智能玩法减少从零摸索的成本。1. 一个能跑起来的 Unity AI Demo从追踪到决策的完整链路如果你在 Unity 里做过第三人称或俯视角游戏一定遇到过这样的场景角色躲在掩体后面AI 敌人却傻乎乎地直线冲过来甚至卡在墙角。这个 unity AI demo 解决的问题很直接——用一个轻量级状态机把巡逻、追踪、攻击三套行为串起来让 AI 根据玩家位置、距离和视野角度自动切换行为同时把动画、寻路和技能帧协调到同一节奏上。它不依赖重型行为树插件纯 C# 脚本加 Unity 自带 NavMesh 就能跑适合刚开始做游戏 AI 的开发者当成可改的起点。你要做的只是打开场景、烘焙寻路网格、点一下 Play。下面我会按结构、代码、运行、避坑、改造的顺序把这个 Demo 拆给你看。2. 项目结构和 AI 模块设计三个核心层怎么搭2.1 目录结构与场景组织这个 Demo 的目录并不复杂但分层很明确。我把 Asset 下按功能拆成五个文件夹避免所有脚本堆在一起变成黑匣子Assets/ ├── Scenes/ │ └── AIDemo_Main.unity ├── Scripts/ │ ├── Core/ │ │ ├── StateMachine.cs │ │ ├── AIAgent.cs │ │ └── AIPathController.cs │ ├── Perception/ │ │ ├── VisionSensor.cs │ │ └── HearingSensor.cs │ ├── Combat/ │ │ ├── AttackAction.cs │ │ └── WeaponArm.cs │ ├── Animation/ │ │ └── AIAnimatorBridge.cs │ └── Utils/ │ └── AiDebugDrawer.cs ├── Prefabs/ │ ├── AIEnemy.prefab │ └── PlayerTarget.prefab └── NavMesh/ └── NavMeshConfigured.asset每个脚本只干一件事VisionSensor负责告诉 AI“我看到了谁”StateMachine负责状态切换AttackAction负责伤害判定。这样改造时不需要重写整套逻辑。比如你想把巡逻路径改成随机点只需要动AIPathController其他模块完全不用管。场景里只有两个预制体一个玩家目标角色一个 AI 敌人。AI 敌人身上挂了 NavMeshAgent、Animator、VisionSensor、HearingSensor、StateMachine 和 AttackAction。这里有个关键点StateMachine 本身不是 MonoBehaviour它是一个纯 C# 类由AIAgent这个 MonoBehaviour 驱动。这么做好处是方便单元测试也方便在编辑器里可视化调试状态变化。搭建步骤也不复杂。先创建一个平面放上几个立方体当障碍物把 AI 和玩家拖进场景然后进 Window - AI - Navigation 面板把地形和障碍物设为静态Bake 一次。如果不做这一步NavMeshAgent 跑不起来AI 会原地打转。2.2 感知层视野、听觉与简单的状态输入感知层是 AI 的输入来源。我最开始做这个 Demo 的时候偷懒直接在每个帧里判断玩家和 AI 的直线距离导致 AI 隔着墙就能看见人毫无游戏体验。后来改成两个独立传感器视觉和听觉并且用 LayerMask 过滤射线检测目标。VisionSensor的核心字段包括视场角viewAngle默认 120 度、视距viewRange默认 12 米、检测层级targetLayer。每帧只做三件事计算 AI 到玩家的方向与 AI 正前方的夹角如果夹角小于视场角一半发射一条射线射线命中目标后将目标引用写入visibleTarget属性。听觉传感器更简单它监听一个全局的HearingEvent带有一个位置和音量值。玩家奔跑时发出音量 1.0 的事件正常走路音量 0.3。AI 会优先处理最近一次音量大于阈值的事件。感知结果不会直接驱动行为而是写入AIAgent的几个公共属性HasVisibleTarget、LastKnownPosition、HearingAlertLevel。这样决策层只读取这些属性不关心感知具体怎么实现的。以后你想加一个嗅觉传感器只要在AIAgent里加一个HasSmellTarget决策层加一个对应转移条件就行。感知层还需要注意性能。我一般会在传感器的Update里加一个checkInterval比如每 0.2 秒检测一次视觉而不是每帧都发射射线。对于单个 AI 无所谓但如果场景里有几十个 AI每帧都做射线检测会在手机端掉到 30 帧以下。这个 Demo 里我把检测间隔写成了可配置字段方便你做压力测试。2.3 决策层状态机与行为树的取舍决策层是这个 Demo 的核心也是很多入门开发者容易纠结的地方用状态机还是行为树我的取舍是——状态机适合行为数量少、切换条件明确的项目行为树适合行为层级深、带优先级抢占的项目。这个 Demo 只有巡逻、追踪、攻击三个状态用状态机是成本最低的解法。状态机定义如下Idle - 初始状态AI 原地等待 Patrol - 按路径点循环移动 Chase - 追踪玩家同时更新最近已知位置 Attack - 停在攻击距离内执行攻击动作状态切换条件写在StateMachine.Update()里。这里有个容易翻车的点状态切换条件不能只在状态内部判断必须由状态机统一控制否则会出现在多个状态同时满足条件时的混乱。我的做法是给每个状态配置优先级比如 Attack 优先级最高只要CanAttack为真强制切到攻击其次是 Chase只要HasVisibleTarget为真就切到追踪最后才是 Patrol 和 Idle。我把状态机实现为泛型类StateMachineTT 是状态枚举。每个状态做三件事OnEnter、OnUpdate、OnExit。StateMachine内部只维护当前状态和状态转移表转移表是一个从状态到条件函数的列表。这样添加一个新状态时只需要在枚举里加一项在转移表里注册条件不需要改其他状态的逻辑。对应到工程实践AIAgent在Update中每帧调用stateMachine.Tick()StateMachine根据当前状态执行对应的OnUpdate并扫描所有可能的转移条件。一旦条件满足就调用当前状态的OnExit再调用新状态的OnEnter。这个模式很老套但非常稳定我在两个模拟项目里都验证过。3. 核心功能拆解追踪、巡逻、攻击的代码实现3.1 巡逻与追踪的切换逻辑先看巡逻状态的代码。这里我用了最常见的路径点数组方式AI 按顺序走向每个路径点到达后转向下一个public class PatrolState : IAIState { private readonly AIAgent _agent; private readonly AIPathController _pathController; private int _currentPointIndex; public PatrolState(AIAgent agent, AIPathController pathController) { _agent agent; _pathController pathController; } public void OnEnter() { _currentPointIndex 0; _agent.SetSpeed(2f); // 巡逻速度慢 _agent.MoveTo(_pathController.GetPoint(_currentPointIndex)); } public void Update() { if (_agent.ReachedDestination()) { _currentPointIndex (_currentPointIndex 1) % _pathController.PointCount; _agent.MoveTo(_pathController.GetPoint(_currentPointIndex)); _agent.SetSpeed(2f); } } public void OnExit() { _agent.StopMoving(); } }逻辑说明OnEnter里把 AI 移到第一个路径点Update里检查是否到达到达就切换下一个路径点。这里用了取模运算实现循环巡逻。AIPathController管理路径点数组你可以直接在 Inspector 里拖一组空物体作为巡逻点。参数说明SetSpeed(2f)是给 NavMeshAgent 的speed属性赋值。巡逻速度设得慢一点能让玩家有反应时间chase 状态我一般设 5.5 到 6.5比玩家跑步速度快一点否则追不上会显得很蠢。ReachedDestination()判断的是NavMeshAgent.remainingDistance baseOffset 0.05f别用distance 0因为 NavMeshAgent 在接近目标时可能会有微小抖动永远到不了 0。追踪状态稍微复杂一点。它要持续更新最近已知位置同时保持速度public class ChaseState : IAIState { private readonly AIAgent _agent; public ChaseState(AIAgent agent) { _agent agent; } public void OnEnter() { _agent.SetSpeed(6f); _agent.UpdateDestination(_agent.LastKnownPosition); } public void Update() { if (_agent.HasVisibleTarget) { // 玩家可见时把玩家位置作为追踪目标 _agent.LastKnownPosition _agent.TargetTransform.position; _agent.UpdateDestination(_agent.LastKnownPosition); } } }这里的核心设计是当玩家从视野中消失AI 不会立刻回到巡逻状态而是先向LastKnownPosition走一段。很多 Demo 只判断当前是否可见导致玩家一晃到掩体后面 AI 就放弃这很出戏。改进方式是让StateMachine的转移条件里记录上一个可见时间超过 3 秒还没看到玩家才切回巡逻。3.2 攻击与技能释放的定时与范围判定攻击状态是游戏逻辑里最容易出 bug 的地方因为动画和伤害判定不同步。这个 Demo 里我采用了一个简单的两段式实现动画事件触发攻击而不是在代码里硬编码等待时间。public class AttackState : IAIState { private float _attackCooldownTimer; private readonly float _attackRange 2.5f; private readonly float _attackCooldown 1.2f; public void OnEnter() { _agent.SetSpeed(0f); _agent.PlayAnimation(Attack); } public void Update() { _attackCooldownTimer - Time.deltaTime; // 如果玩家跑出攻击范围立即退出攻击 if (Vector3.Distance(_agent.transform.position, _agent.TargetTransform.position) _attackRange) { _agent.TriggerStateChange(AIState.Chase); return; } // 冷却结束后再次播放攻击动画 if (_attackCooldownTimer 0f) { _agent.PlayAnimation(Attack); _attackCooldownTimer _attackCooldown; } } // 这个函数由 Animator 的 Animation Event 调用 public void OnAttackHit() { float distance Vector3.Distance(_agent.transform.position, _agent.TargetTransform.position); if (distance _attackRange) { _agent.DamageTarget(10); } } }逻辑说明OnAttackHit是挂在动画剪辑上的事件动画播放到“挥砍”这一帧时引擎会自动调用。这样伤害判定和动画严格同步不会出现在动画还要 0.3 秒才砍到人、伤害却提前打出来的情况。参数说明攻击范围_attackRange我根据模型大小调到了 2.5因为武器臂长大约 1.8 米再加上前后摇视觉上 2.5 比较合理。如果你用远程攻击攻击范围要改成射线检测不能只用距离判断。_attackCooldownTimer是计时器用来控制攻击频率。这里有个细节攻击冷却计时只在攻击状态内累加如果你在冷却还没结束时切到了追踪回来后会立即执行攻击这可能让玩家觉得 AI 反应过快。我一般会在OnExit里把计时器保留而不是重置这样更自然。3.3 动画与 AI 状态同步Animator 参数桥接动画和 AI 状态不同步是常见翻车点。比如 AI 已经切到 Attack 状态了动画还停留在运行中画面就会变成“滑步砍人”。我的做法是建一个AIAnimatorBridge它只负责监听状态机变化然后写好 Animator 参数。public class AIAnimatorBridge : MonoBehaviour { [SerializeField] private Animator _animator; [SerializeField] private AIAgent _agent; private void Update() { float speed _agent.NavAgent.velocity.magnitude; _animator.SetFloat(MoveSpeed, speed); int currentState _agent.GetCurrentState(); _animator.SetBool(IsChase, currentState AIState.Chase); _animator.SetBool(IsAttack, currentState AIState.Attack); } }这里的MoveSpeed是 Animator Controller 里的 Float 参数控制 Run 动画的混合权重。IsChase和IsAttack是布尔参数用于触发状态切换。因为Animator的 CrossFade 需要时间我通常会把“动画切到攻击”的过渡时长设置为 0.1 秒这样不会出现瞬爆。有一个值得注意的点MoveSpeed直接用NavMeshAgent.velocity.magnitude。如果 AI 被击退或者被推挤这个值会有波动但视觉上反而是合理的。如果要精确匹配动画的 root motion建议把applyRootMotion关闭用代码控制实际位移否则会出现 AI 一边被 NavMeshAgent 控制一边又被动画移动拖拽的冲突。这个冲突很难排查我自己第一次做的时候整整调了一个晚上。4. 跑起来的关键步骤场景搭建、参数配置与调试4.1 从零搭一个测试场景打开 Unity 后先创建一个空场景然后把下面的东西按顺序放好创建地面GameObject - 3D Object - Plane尺寸默认就行。把 Transform 的 Scale 设为 (10, 1, 10)这样地面变大。创建障碍物放三四个 Cube随意错开摆放模拟掩体。导入预制体把AIEnemy.prefab和PlayerTarget.prefab拖进场景。如果没有预制体可以手动创建AIEnemy新建一个 Capsule加上NavMeshAgent组件把AIAgent、VisionSensor、HearingSensor、StateMachine、AttackAction等脚本挂上。PlayerTarget新建一个 Capsule挂一个简单的PlayerController脚本用 WASD 控制移动方便测试 AI 追踪。烘焙寻路Window - AI - Navigation选择所有地面和障碍物勾选 Navigation Static点 Bake。这里最容易出的问题是没有地面静态标记导致 Bake 出来的 NavMesh 是空的。如果你按下 Play 后 AI 没有走向玩家先检查NavMeshAgent在 Scene 视图里有没有显示蓝色路径没有就必须重新烘焙。4.2 关键组件参数说明我把 AI 预制体上需要手动设置的参数整理成一个表方便你对照检查组件参数推荐值说明NavMeshAgentSpeed6追踪速度比玩家快一点NavMeshAgentAngular Speed360转身速度低了会导致 AI 走弧线NavMeshAgentStopping Distance2.2停在攻击范围边缘VisionSensorView Angle120视场角长条形敌人可以调到 90VisionSensorView Range12视觉距离场景小就调成 8VisionSensorCheck Interval0.2检测间隔性能优化时调大HearingSensorAlert Threshold0.6超过这个音量的声音才会被记录StateMachineLose Target Time3玩家丢失后多久才放弃追踪AttackActionAttack Damage10每次攻击伤害值AttackActionCooldown1.2攻击间隙设置这些参数时注意StoppingDistance要小于攻击范围否则 AI 永远抬不起手。如果你发现 AI 在攻击时反复横跳多半是 stopping distance 和 attack range 设置反了。4.3 调试技巧用 Gizmos 画出 AI 感知范围感知层是黑匣子你看不到 AI 眼里有什么。最简单的办法是写一个专门的 Gizmos 脚本把视觉范围、听觉范围和当前状态画在 Scene 视图里public class AiDebugDrawer : MonoBehaviour { [SerializeField] private VisionSensor _vision; [SerializeField] private AIAgent _agent; private void OnDrawGizmosSelected() { if (_vision null) return; Gizmos.color Color.yellow; Vector3 forward transform.forward * _vision.ViewRange; Vector3 rightBoundary Quaternion.Euler(0, _vision.ViewAngle / 2, 0) * forward; Vector3 leftBoundary Quaternion.Euler(0, -_vision.ViewAngle / 2, 0) * forward; Gizmos.DrawRay(transform.position, rightBoundary); Gizmos.DrawRay(transform.position, leftBoundary); if (_agent ! null) { string stateName _agent.GetCurrentState().ToString(); UnityEditor.Handles.Label(transform.position Vector3.up * 2f, stateName); } } }这个脚本只会在编辑器里绘制不会打进发布包。OnDrawGizmosSelected在选中物体时绘制避免所有 AI 都画线导致画面太花。用Handles.Label可以把当前状态直接显示在 AI 头顶跑起来就能看到状态切换的时机比打印日志更直观。调试时我一般把Time.timeScale调到 0.2让 AI 慢动作运行同时观察状态变化和动画是否同步。如果状态已经变为 Chase 但动画还在跑巡逻问题多半是 Animator Controller 缺少对IsChase参数的过渡检查一下 Animator 的状态转移条件即可。5. 避坑指南Unity AI Demo 的 5 个常见问题5.1 AI 卡在墙角或障碍物前现象AI 一直在原地抖动路径计划失败。原因NavMeshAgent 的radius太大无法通过狭窄缝隙或者目标点定位在 NavMesh 之外。解决检查 Agent 的Radius默认 0.5如果障碍物之间的距离小于 1 米把 Radius 调小到 0.3。另外UpdateDestination时目标点最好通过NavMesh.SamplePosition投影到网格上避免目标悬空。5.2 状态机频繁切换导致 AI 神经质现象AI 在 Chase 和 Attack 之间疯狂切换动画不停被打断。原因进入攻击的条件是“距离小于范围”退出攻击的条件也是“距离大于范围”。如果范围阈值只有一个值而玩家站在边界附近反复横跳状态就会震荡。解决给攻击进入和退出设置两个不同阈值专业说法叫“迟滞区间”。比如进入攻击需要距离小于 2.2退出攻击需要距离大于 2.8。在StateMachine里写成两条独立条件就能避免抖动。5.3 动画事件调用不到脚本方法现象攻击动画播放了但OnAttackHit没有执行。原因Animation Event 指定的函数名拼写错误或者脚本挂在了子物体而非动画播放的同一个 GameObject 上。解决确保AttackState挂载的 GameObject 上有某个脚本包含OnAttackHit方法。更稳妥的做法是挂在 AI 根节点然后在动画事件面板里把接收者设为同名的公共方法不需要管是哪个脚本Unity 会往根物体的所有组件上找这个方法。5.4 烘焙后 NavMesh 不完整现象场景一部分区域 AI 无法进入哪怕地面是平的。原因地面和障碍物的Navigation Static标记没勾选或者烘焙分辨率太低导致斜面被忽略。解决在 Navigation 窗口的 Object 面板里选中所有地形勾选Navigation Static然后 Bake。调整 Bake 参数里的Voxel Size默认 1 太大改成 0.1 能让网格更精细但烘焙时间会变长。5.5 多 AI 同时运行时性能突然下降现象场景里放了 20 个 AI帧率从 60 掉到 20。原因每个 AI 每帧都在检测视觉和路径更新还可能有大量Animator动画在同步状态。解决把VisionSensor的检测间隔调大到 0.3 秒并且用协程或者定时器而不是Update。另外可以关闭没有播放动画的 AI 的Animator组件的enabled直到玩家接近 15 米才开启。这个技巧在移动端特别有效。6. 二次开发怎么把这个 Demo 改成你自己的游戏 AI改这个 Demo 之前先想清楚你要加哪种行为。如果是想加一个新的攻击方式比如远程射击只需要改AttackAction把距离判定改成一条射线把OnAttackHit里的伤害改成调用投射物生成。不需要动状态机因为攻击状态仍然是攻击状态。如果你想加一个“潜行”状态那就要动状态机了。我会这样做先在AIState枚举加一项Sneak再在StateMachine的转移表里增加一个条件当玩家没有发现 AI 且 AI 与玩家距离小于 5 时从Patrol切到Sneak。这一步的关键是让感知层暴露更多的数据比如PlayerAwareness字段。我一般会在VisionSensor里加一个累计值玩家视角朝向 AI 的时间越长awareness 越高。只要 awareness 大于阈值AI 就进入潜行状态。改造时有一个习惯值得养成每次加新状态先写一个最小化场景验证不要在原来的场景里直接改。比如你想测潜行就新建一个空场景只放一个敌人和一个玩家手动把玩家藏到墙后看 AI 是否进入潜行。如果验证通过再把改动合入主场景。这个流程能帮你把翻车的范围控制在最小。另外这个 Demo 还可以再叠加行为树。当你发现状态机的转移条件超过 10 条或者出现了“当前状态需要知道上一个状态”的需求时状态机就开始变难维护了。这时可以把决策层换成行为树的根节点把感知结果作为黑板数据行为树叶子节点直接复用PatrolState、ChaseState的逻辑。我做过一个类似改造大概花了一天时间但换来的是新增行为时不用再改转移表。如果你需要把这个 Demo 用在自己项目里别忘了改LayerMask。我最初写的检测层级是Player很多新手在别人的项目里没建这个 Layer结果 AI 什么也看不见。把玩家预制体放到Player层再确认 VisionSensor 的目标层级包含那一层基本就不会有问题了。从那以后我每次拿到一个新的 AI 项目都会先检查这两个地方状态切换的转移表是否清晰、动画事件接收者是否在根节点。确认无误再跑 Play。这两个点排查完80% 的跑不起来问题都能在五分钟内解决。希望帮到你。本文还有配套的精品资源点击获取