简介:这是一份Unity AI演示项目压缩包,面向希望快速上手Unity人工智能开发的游戏开发者、毕业设计学生与进阶学习者。资源以角色智能行为为主线,覆盖NavMesh导航网格烘焙与Agent自动寻路、行为树决策流程搭建、ML-Agents机器学习训练示例、多智能体协作与竞争、物理碰撞检测、传感器感知、动画状态机切换及粒子特效触发等关键知识点,并配套C#脚本实现,帮助读者理解AI从环境感知、逻辑决策到角色表现的完整链路。压缩包共178个文件,约1.35MB,以cs脚本、asset场景资源、meta配置为主,另有sln解决方案、csproj项目文件及少量DLL依赖,目录结构清晰,便于直接打开工程对照脚本与场景学习。目前已有266人学习下载,适合有一定Unity基础、想系统了解AI模块落地方式的开发者。通过研读Demo中的脚本与场景组织,可掌握NavMeshAgent参数调节、行为树节点设计、动画状态机与AI行为联动等实操方法,也能借鉴其在性能优化、对象管理和资源组织上的实用经验,适合用于课程设计或快速验证AI玩法。
1. unity AIdemo 到底是什么:先看清四层结构再动手
初次拿到这份 unity AIdemo 工程时,我以为它只是给新手练手用的巡逻加追人小样。把工程塞进 Unity 后仔细拆了一遍才发现,它的价值不在那几张占位用的胶囊体和平面,而在一条从感知、决策、移动一路贯通到动画的 AI 主回路。项目用 C# 实现了一个带视野锥形检测、听觉事件、状态机切换、NavMesh 寻路和 Animator 联动的完整敌人 AI,适合两类人:一类是刚写完基础组件、想看看 NPC 完整行为怎么组织的开发者;另一类是想把 Demo 里的敌人逻辑抄进自己游戏、却担心看不懂源码的熟手。它真正的门槛不是算法本身,而是感知、决策、移动、动画四层之间在 Update 里的衔接位置,这也是本文想拆给你看的东西。
2. 工程结构与主回路:AIController 如何把感知、决策、移动串起来
2.1 先看目录:脚本按职责分层而不是按功能堆文件
打开 Assets 目录,第一感觉是脚本没有全部塞进一个文件夹,而是按 AI 内部职责拆成了 Perception、StateMachine、Movement 三层,外加一个 AIController 做入口。这种组织方式算不上花哨,但对拆项目的人来说非常友好,拿到手五分钟就能判断哪段代码管什么。
Assets/ ├─ Scenes/ │ └─ DemoScene.unity ├─ Scripts/ │ ├─ AI/ │ │ ├─ AIController.cs │ │ ├─ Perception/ │ │ │ ├─ BasePerception.cs │ │ │ ├─ VisionPerception.cs │ │ │ └─ HearingPerception.cs │ │ ├─ StateMachine/ │ │ │ ├─ AIState.cs │ │ │ ├─ AIBlackboard.cs │ │ │ ├─ PatrolState.cs │ │ │ ├─ ChaseState.cs │ │ │ └─ AttackState.cs │ │ └─ Movement/ │ │ └─ AINavMeshMover.cs │ └─ UI/ │ └─ RuntimeDebugPanel.cs └─ README.mdPerception 只负责采集信息,StateMachine 负责状态切换,Movement 封装 NavMeshAgent 的调用,AIController 是唯一知道其他三层存在的脚本。这样有几个好处:状态脚本之间互相不知道对方的存在,切换状态时不会出现一个状态在改目标、另一个状态在改速度的竞争;移动和感知被隔离之后,后续想换寻路方案或者加一个新的感知器,不会牵连决策层。
2.2 技术选型:为什么状态机当主干、NavMesh 当轮子
这个 Demo 的决策主干是状态机,Patrol、Chase、Attack 三个状态依次切换;感知数据写入一个共享的 AIBlackboard,移动全部交给内置 NavMeshAgent。选这套组合的核心理由有三个。
状态机对“当前行为”的展示最直观,看到 Enter、Tick、Exit 三个方法,就能明白这个单位此刻在干什么,遇到问题也好定位。NavMeshAgent 承担了寻路、避障、局部移动,开发者不需要手写 A*,也不用自己去处理“撞墙后沿着墙滑动”这种血泪问题。行为树在这个工程里没有抢状态机的位置,而是可以作为单个状态内部的任务编排器存在——比如追人状态下用 Sequence 依次执行“接近目标、瞄准、开火”。也就是说,状态机决定“现在该干什么”,行为树决定“这件事内部怎么拆”,两层不打架。
如果你拿到的是衍生版本,里面状态机和行为树同时在写同一个 Agent 的目标,就会发现 AI 反复横跳。解决办法是明确一条主线:状态机拥有最终决策权,行为树只产出建议,二者不要在同一个字段上直接赋值。
2.3 主回路入口:AIController 的一次 Tick
using UnityEngine; // 每个 AI 单位的“大脑”入口,负责串联四层 public class AIController : MonoBehaviour { [Header("引用")] public VisionPerception vision; // 视觉感知 public HearingPerception hearing; // 听觉感知 public AIStateMachine machine; // 状态机 public AINavMeshMover mover; // 移动执行 public Animator anim; // 表现层 [Header("参数")] [Range(0.05f, 1f)] public float senseInterval = 0.2f; // 感知采样间隔 private void Start() { // 初始进入巡逻状态 machine.ChangeState(new PatrolState(this)); // 感知使用间歇采样,不需要逐帧执行 InvokeRepeating(nameof(ExecuteSense), 0f, senseInterval); } private void ExecuteSense() { // 感知结果写入黑板,决策层统一读取 machine.blackboard.target = vision.NearestVisibleTarget(); machine.blackboard.heardNoise = hearing.DetectNoise(); } private void Update() { // 决策与移动逐帧驱动;表现层读取移动速度 machine.Tick(); mover.Tick(machine.currentState.MoveVector()); anim.SetFloat("Speed", mover.NormalizedSpeed()); } }这段代码展示了整个 AI 的调度节奏:感知是间歇的,决策和移动是逐帧的,动画只是结果的消费者。senseInterval 参数非常关键,调到 0.05 会让 AI 对目标出现反应极快,但 CPU 开销直线上升;调到 0.5 会看到明显的迟钝感,适合节省性能。把感知和决策分开,是我认为这个 Demo 最值得借鉴的设计。
3. 核心模块拆解:从视野锥形检测到动画落地的完整链路
3.1 感知层:Vector3.Angle 与 Physics.Raycast 的组合
视野检测是 AI 行为的第一入口。这段代码实现了三个条件的同时判断:距离在视线范围内、目标处于锥形视野夹角内、目标没有被障碍物遮挡。
// VisionPerception.cs —— 视野锥形 + 遮挡检测 public Transform NearestVisibleTarget() { Transform result = null; float nearestSqr = float.MaxValue; for (int i = 0; i < candidates.Count; i++) { Transform candidate = candidates[i]; Vector3 toTarget = candidate.position - transform.position; float sqrDist = toTarget.sqrMagnitude; // 1) 超出最大视野距离:跳过 if (sqrDist > maxRange * maxRange) continue; // 2) 不在锥形角度内:跳过 float angle = Vector3.Angle(transform.forward, toTarget.normalized); if (angle > viewHalfAngle) continue; // 3) 被其他物体遮挡:跳过 if (Physics.Raycast(transform.position, toTarget.normalized, out RaycastHit hit, maxRange)) { if (hit.transform != candidate) continue; } // 取最近可见目标 if (sqrDist < nearestSqr) { nearestSqr = sqrDist; result = candidate; } } return result; }距离判断用 sqrMagnitude 而不是 Vector3.Distance,因为平方距离省了一次开根号,目标多的时候能省下不少 CPU。Vector3.Angle 返回的是两个向量之间的夹角,这里传的是 forward 与目标方向的归一化向量,所以 viewHalfAngle 代表的是“半角”。如果你把半角设成 60 度,实际视野是正面 120 度的扇形。遮挡检测用的是 Physics.Raycast,坑在于射线会命中目标自己,所以判断条件是 hit.transform != candidate,而不是 hit.collider 是否为 null。
候选目标列表由外部注入,而不是每个 AI 都去遍历全场景。这是性能设计上很关键的一步,后面避坑章节会具体聊。
3.2 决策层:状态机用 Enter、Tick、Exit 保持行为清晰
决策层是 AI 的核心,它的可读性决定了二次开发时改代码的难度。下面先看状态基类的定义,再看一个具体的 ChaseState:
// AIState.cs —— 所有状态的基类 public abstract class AIState { public virtual void Enter(AIController ctx) { } public virtual void Tick(AIController ctx) { } public virtual void Exit(AIController ctx) { } } // ChaseState.cs —— 追击状态 public class ChaseState : AIState { public override void Enter(AIController ctx) { ctx.anim.SetBool("Running", true); // 进入追击时打开跑步动画 } public override void Tick(AIController ctx) { Transform target = ctx.machine.blackboard.target; if (target == null) { // 目标丢失,回到巡逻 ctx.machine.ChangeState(new PatrolState(ctx)); return; } float dist = Vector3.Distance(ctx.transform.position, target.position); if (dist <= ctx.attackRange) { // 距离足够近,切换攻击 ctx.machine.ChangeState(new AttackState(ctx)); return; } } public override void Exit(AIController ctx) { ctx.anim.SetBool("Running", false); // 离开追击时复位动画 } }状态切换逻辑写在 Tick 里,ChangeState 内部会先调用当前状态的 Exit,再调用新状态的 Enter。这样保证动画状态、速度参数、目标引用都不残留上一次的痕迹。attackRange 这个参数必须和 AttackState 里的实际攻击距离保持一致,否则会出现“AI 一直追到贴脸却切不进攻击状态”的尴尬。黑板的 target 字段由 AIController 在 ExecuteSense 中写入,决策层不直接调感知层,数据流是单向的。
3.3 移动层:NavMeshAgent 只接收方向意图
移动层是这个 Demo 里最容易翻车的地方,原因是很多人直接把“目标 Transform”塞给 NavMeshAgent,让它在每帧都重算路径。好的做法是状态机只给移动层一个方向意图:
// AINavMeshMover.cs public class AINavMeshMover : MonoBehaviour { [SerializeField] private NavMeshAgent agent; public void Tick(Vector3 direction) { // direction 是状态机给出的移动意图,零向量代表原地不动 if (direction.sqrMagnitude > 0.001f) { agent.SetDestination(transform.position + direction.normalized * followDistance); } } }这里把目标位置换算成“当前坐标加一个方向向量”,本质上是让 NavMesh 自己决定最终路径,而不是我们替它定终点。followDistance 表示移动的终点偏移量,避免目标点是动态移动物体时,Agent 因为终点不断变化而抖动。ChaseState 的 MoveVector 返回目标方向,PatrolState 返回下一个巡逻点的方向,移动层不关心决策层选的是谁。避障由 NavMeshAgent 的 radius 和 obstacle avoidance 参数处理,不需要手动写碰撞规避。
3.4 表现层:Animator 只读状态,不参与决策
动画联动其实是整个链路里最容易被误解的环节。很多新手试图让 Animator 知道“当前是追击还是巡逻”,然后在动画状态机里做切换,结果动画状态和逻辑状态各切各的,角色会做出“追人时播放走路动画”的怪事。
正确的联动方式是 Animator 只消费两个变量:Speed 表示移动快慢,Running 表示是否在追击。在这个 Demo 里,AIController 在 Update 中把 mover.NormalizedSpeed() 传给动画参数,Animator 的 Blend Tree 根据 Speed 在 Idle、Walk、Run 之间自动混合。ChaseState 的 Enter/Exit 负责设置 Running 布尔量,AttackState 触发攻击动画时改的是 Trigger。
注意 Animator 组件上的 Apply Root Motion 必须关闭,否则动画自带的位移会与 NavMeshAgent 的位移叠加,导致滑步、抖动甚至穿墙。转向由 Agent 的 angularSpeed 控制,这与动画播放的朝向无关。
4. 跑通它:从打开场景到验证 AI 行为的实战步骤
4.1 先把场景和引用关系检查一遍
拿到工程后,我习惯不直接点 Play。先打开 Scenes/DemoScene,选中场景中挂 AIController 的对象,检查 Inspector 上的 vision、hearing、machine、mover、anim 五个引用是否为空。脚本文件如果移动过目录,Unity 的序列化引用会失效,表现为运行时不报错,但 AI 完全不动。
检查完引用,再确认 Agent 是否挂载了 NavMeshAgent 组件,以及场景里有没有 NavMesh 数据。如果都正常,按下 Play 应该能看到角色在巡逻点之间循环移动。这里有个很容易踩的坑:如果场景里没有烘焙数据,Scene 视图里看不到蓝色网格,AI 就算挂了 Agent 也寸步难行。
4.2 NavMesh 烘焙:AI 卡住的九成原因
烘焙是跑通这个 Demo 的关键动作,步骤如下:
- 打开菜单 Window > AI > Navigation,打开 Navigation 窗口。
- 在 Hierarchy 中选中地面和所有障碍物。
- 在 Inspector 右上角勾选 Navigation Static,并确保 Area 设置为 Walkable。
- 回到 Navigation 窗口,点击右下角 Bake 按钮。
- 等待烘焙完成,Scene 视图里会出现蓝色覆盖层,代表 AI 能行走的区域。
几个参数需要留意。Agent Radius 决定 AI 能挤过的通道宽度,如果你在场景里搭了窄门,半径过大时会绕路;Agent Height 决定低矮障碍物是否会被跨过;Max Slope 控制能爬的坡度,超过角度会被视为不可通行。烘焙完成后再 Play,AI 通常会立刻开始巡逻。如果你改过场景的几何结构,记得要重新 Bake,这是最容易被忽略的一步。
4.3 用 Gizmos 和日志验证感知是否正常
AI 不按预期行动时,最难判断的是“它到底有没有看到目标”。我的习惯是给 VisionPerception 加一段 OnDrawGizmosSelected,把视野范围画出来:
// VisionPerception.cs 附加调试代码 private void OnDrawGizmosSelected() { // 画视野左右边界 Vector3 leftDir = Quaternion.Euler(0, -viewHalfAngle, 0) * transform.forward; Vector3 rightDir = Quaternion.Euler(0, viewHalfAngle, 0) * transform.forward; Gizmos.color = Color.yellow; Gizmos.DrawRay(transform.position, leftDir * maxRange); Gizmos.DrawRay(transform.position, rightDir * maxRange); // 画最近目标的连线 Transform t = NearestVisibleTarget(); if (t != null) { Gizmos.color = Color.red; Gizmos.DrawLine(transform.position, t.position); } }OnDrawGizmosSelected 只在选中对象时绘制,平时不耗性能。验证方法是把一个可移动的物体放进目标列表,拖着它在 AI 周围绕圈,观察黄色扇形是否覆盖目标,红色连线是否在目标进入墙体后消失。如果 AI 在目标进入视野后没有追击,打开 AIController 的日志开关,看状态机是否真的切换到了 ChaseState。
4.4 把玩家当成移动目标来验证追击
Demo 场景里的目标往往是个静止的胶囊体,静止物体验证不了追击路径的实时性。改造方法很简单:建一个挂 CharacterController 或 Rigidbody 的移动物体,把它加入 VisionPerception 的候选列表,运行时用键盘控制它移动。验证标准有三个:AI 是否在目标进入视野后切换追击;追击路径是否会避开障碍物;目标脱离视野后 AI 是停在原地还是回到巡逻状态。
这一步跑通之后,这个 Demo 的核心能力基本就掌握了,后面再做二次开发心里就有底了。
5. 避坑指南:跑通这个 AI Demo 最容易翻车的五个细节
5.1 现象一:AI 站在原地一动不动
现象:Play 后 AI 既不巡逻也不追人,日志干净得像没挂脚本。
原因:最常见的是 NavMesh 没有烘焙,Agent 没有可走的地面;其次是 AIController 的引用在 Inspector 里丢失,导致 machine、mover 调用的是空引用。还有一种不常见但存在的情况:AI 对象挂载了 CharacterController,和 NavMeshAgent 冲突。
解决:先在 Scene 视图确认蓝色导航网格存在,不存在就按第 4.2 节重新 Bake。引用丢失则重新拖拽对应组件。如果你用的是旧版 Unity,检查是否忘了给地面设置 Navigation Static。验证方式是选中 AI,看 Navigation 面板中它是否显示 “On NavMesh”。
5.2 现象二:AI 隔着墙也能看见玩家并穿墙追击
现象:AI 能隔着厚墙准确追踪玩家,甚至穿墙移动。
原因:距离和角度检测都通过了,但缺少 Physics.Raycast 的遮挡检测;或者 Raycast 的 layerMask 没有包含墙体,射线穿过了物体。
解决:在 NearestVisibleTarget 中补上射线检测,墙体统一放在一个命名明确的 Layer 上。检查方法是在 Gizmos 里画一条射线,移动目标穿过墙体时,看红色连线是否立刻断开。如果你用的是多个碰撞体,注意 Raycast 命中的可能是子物体,hit.transform 对比需要用递归找到根节点。
5.3 现象三:动画方向与移动方向不一致
现象:AI 正常转弯时,模型出现滑步,或者向前走的动画和实际移动方向偏移 90 度。
原因:Animator 勾选了 Apply Root Motion,动画位移与 NavMeshAgent 位移叠加;另一个常见原因是模型部件没有统一朝向前方,角色的 forward 指向了侧边。
解决:在 Animator 组件上取消 Apply Root Motion;把 NavMeshAgent 的 angularSpeed 设为 360 到 720 的数值,转向会更平滑;确认模型骨骼根节点的 forward 与世界坐标 Z 轴正向一致。如果模型是下载的,直接在模型导入面板调整 Rotation 让正面朝前,不要在运行后再做补偿。
5.4 现象四:状态机和行为树同时工作导致决策打架
现象:AI 在追击敌人时偶尔往回走,状态日志反复切换。
原因:工程里某个衍生版本同时采用了状态机和行为树两套决策系统,两套系统都在写同一个 NavMeshAgent 的目标点,每帧互相覆盖。
解决:定一条主线。状态机负责决定“当前行为目标是什么”,行为树只负责在单个状态内部做任务编排。如果两套系统已经并存,临时排查时先把行为树脚本禁用,跑一次确认问题是否消失,再决定是否保留。我在这个 Demo 中没有让行为树抢状态机的活,就是为了保持这套主回路的简洁。
5.5 现象五:加入多个 AI 后帧率断崖式下降
现象:一个 AI 时帧率正常,放进八个 AI 后帧率直接对半砍。
原因:每个 AI 每帧都对自己视野内的所有候选目标做 Raycast,候选目标几十个时,O(N*M) 的射线检测直接压垮主线程。感知采样间隔设成 0 或者过小,也会让 CPU 开销成倍增加。
解决:把 senseInterval 设在 0.2 到 0.5 秒之间;候选目标按空间分区投喂,而不是把全场景物体都加入列表;更激进的做法是“懒惰感知”——先通过听觉事件激活视觉,再开始做 Raycast。性能优化不是写完再调,而是从一开始就把采样频率和候选列表暴露成可调参数。
我这里列一个实际运行时的参考参数表,刚跑通 Demo 时可以直接抄这个区间:
| 参数 | 参考区间 | 说明 |
|---|---|---|
| senseInterval | 0.1 ~ 0.5 秒 | 感知采样间隔,越小反应越快 |
| attackRange | 1.5 ~ 3.0 | 攻击触发距离,需小于视野距离 |
| angularSpeed | 360 ~ 720 | 转向速度,太低会有“漂移”感 |
| maxRange | 8 ~ 15 | 视野半径,数值越大 Raycast 开销越高 |
6. 二次开发技巧:把这个 AI Demo 改造成你自己的敌人 AI
6.1 把固定巡逻点改成运行时可调的巡逻路线
PatrolState 里通常写死了一组 Transform,想改成“巡逻途中随时调整路线”,最省事的做法是把巡逻点列表提取到 Inspector,并给状态脚本加一个可调参数。这样不用每改一次数值就重新编译:
public class PatrolState : AIState { [Range(0.5f, 10f)] public float stopDistance = 2f; public List<Transform> patrolPoints; public override void Tick(AIController ctx) { if (ctx.mover.Arrived()) { nextIndex = (nextIndex + 1) % patrolPoints.Count; ctx.mover.MoveTo(patrolPoints[nextIndex].position); } } }stopDistance 设置为 0 时,Agent 会一直试图精确停在巡逻点上,遇到不可行走区域边缘就会反复重算路径,表现为原地抖动。设一个容忍距离,到达判定会更稳定。把视野半径、攻击距离这些参数全部暴露到 Inspector 之后,运行时当场改数、当场看表现,比一遍遍改代码重新 Play 高效得多。
6.2 加一个“逃离”状态来验证状态机扩展性
复制一份 ChaseState 改成 FleeState,Enter 时打开 Running 动画,Tick 里向远离目标的方向移动,再把切换条件放在 ChaseState 里:当目标距离过近且触发威胁时,切换到逃离。这个改造验证的不只是代码能跑,而是状态机主干的扩展点是否干净——只需要新增一个类并改切换条件,不碰移动层和动画层,就能加一种完整行为。
有一次我改造这个 Demo 时,忘了在 FleeState 的 Exit 里把 Running 动画关掉,结果角色追完敌人后继续摆着跑步姿势散步。从那以后我每次新增状态,都强制先检查 Enter 和 Exit 是否配对。希望帮到你。拿到这套 Demo 源码的话,直接下载工程包打开,先按第 4 章把 NavMesh 烘焙好,再跑通主回路,后面改起来会顺畅很多。
本文还有配套的精品资源,点击获取