
简介这是一份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 的一次 Tickusing 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 控制能爬的坡度超过角度会被视为不可通行。烘焙完成后再 PlayAI 通常会立刻开始巡逻。如果你改过场景的几何结构记得要重新 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 时可以直接抄这个区间参数参考区间说明senseInterval0.1 ~ 0.5 秒感知采样间隔越小反应越快attackRange1.5 ~ 3.0攻击触发距离需小于视野距离angularSpeed360 ~ 720转向速度太低会有“漂移”感maxRange8 ~ 15视野半径数值越大 Raycast 开销越高6. 二次开发技巧把这个 AI Demo 改造成你自己的敌人 AI6.1 把固定巡逻点改成运行时可调的巡逻路线PatrolState 里通常写死了一组 Transform想改成“巡逻途中随时调整路线”最省事的做法是把巡逻点列表提取到 Inspector并给状态脚本加一个可调参数。这样不用每改一次数值就重新编译public class PatrolState : AIState { [Range(0.5f, 10f)] public float stopDistance 2f; public ListTransform 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 改成 FleeStateEnter 时打开 Running 动画Tick 里向远离目标的方向移动再把切换条件放在 ChaseState 里当目标距离过近且触发威胁时切换到逃离。这个改造验证的不只是代码能跑而是状态机主干的扩展点是否干净——只需要新增一个类并改切换条件不碰移动层和动画层就能加一种完整行为。有一次我改造这个 Demo 时忘了在 FleeState 的 Exit 里把 Running 动画关掉结果角色追完敌人后继续摆着跑步姿势散步。从那以后我每次新增状态都强制先检查 Enter 和 Exit 是否配对。希望帮到你。拿到这套 Demo 源码的话直接下载工程包打开先按第 4 章把 NavMesh 烘焙好再跑通主回路后面改起来会顺畅很多。本文还有配套的精品资源点击获取