尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Unity AI Demo实战指南:从状态机到行为树与避坑要点

发布时间:2026/9/29 2:05:08

资讯中心
01
ARTICLE

Unity AI Demo实战指南:从状态机到行为树与避坑要点

Unity AI Demo实战指南:从状态机到行为树与避坑要点
简介一份聚焦Unity引擎AI集成的演示项目面向游戏开发者与Unity学习者。项目通过具体示例展示NavMesh导航网格、行为树决策、ML-Agents机器学习、物理碰撞感知、C#脚本控制等关键知识点同时覆盖动画状态机、粒子系统触发及性能优化思路适合希望从零搭建角色智能行为的开发者参考。压缩包共178个文件以meta场景配置、asset资源、cs脚本、csproj工程文件、dll库及sln解决方案等类型为主整体体积仅1.35MB目录结构精简便于快速查看核心代码与场景设置。已有265人学习可用作Unity AI入门与进阶的轻量参考资料。对照Demo中的代码与配置读者可理解导航网格烘焙、行为树节点设计、ML-Agents训练流程、多智能体协作以及动画与AI状态联动等实现细节获得可直接借鉴到实际项目的AI模块搭建思路同时资源还展示了如何通过物理模拟提升交互真实性并提供了若干脚本层面的性能优化示意帮助开发者在学习AI的同时兼顾运行效率。1. 接了个 AI demo 需求先别急着写代码拿到“unity AIdemo”这个需求时很多人第一反应是“要不上个大模型 API”先冷静三秒。在 Unity 里做 AI demo绝大多数场景不是让 NPC 学会聊天而是让 NPC“看起来有脑子”——会巡逻、会追玩家、会躲障碍、会根据血量切换行为。这才是团队验收时真正会盯着看的东西。本文要讲的就是一条从零把 AI demo 跑通的完整路径行为树怎么搭、状态机怎么写、ML-Agents 什么时候才值得引以及我踩过的那些编辑器里不报错、一打包就翻车的坑。适合刚被安排做 AI 原型验证的 Unity 开发者也适合想把手游 NPC 行为从“脚本堆 if-else”升级到可维护架构的人。2. 先想清楚 AI demo 要哪种“智能”贴图还是真逻辑2.1 三种常见方案的选型边界Unity 生态里做 AI绕不开三件事自写状态机、行为树可视化方案、ML-Agents 训练。它们各占一个生态位选错了后面全是血泪。状态机适合流程固定、状态少、每个状态行为简单的场景。比如一个只会站桩、受击、攻击三态转换的怪物。好处是零依赖、好调试、性能开销可忽略。缺点是状态一多状态转移图变成蜘蛛网加一个状态要检查所有旧状态的影响。行为树适合 NPC 需要“决策”的场景——巡逻、警戒、追踪、攻击、逃跑这些行为要能组合、能打断、能复用。它把“决策”和“执行”拆开了Selector 决定下一步走哪条分支Sequence 要求一串动作全部完成才继续。调试时能清楚看到当前走到哪个节点策划也看得懂。ML-Agents 是最容易被高估的方案。它适合的是“行为无法手工编写”的场景比如人形角色的平衡控制、群体对抗策略。代价是训练时间长、需要调 reward 函数、结果不可解释。做一个 demo 级的 AI除非你确实在验证强化学习本身否则别碰——这属于给自己挖坑。2.2 选型判据看决策复杂度不看项目大小我用过一条经验来快速定方案把 NPC 需要响应的外部事件列出来超过 8 个且存在优先级冲突比如“血量低时即使看到玩家也要先跑”直接上行为树少于 6 个且行为是线性串行状态机足够如果行为连规则都描述不清才考虑 ML-Agents。另一个判据是团队结构。有策划愿意一起调行为行为树的优势会被放大纯程序项目状态机反而更快落地。记住一点demo 的目的是验证“这玩法行不行”不是炫技。最快跑通的那条路才是好路。常见做法是先用状态机把 demo 跑通然后在下一轮迭代里换成行为树。这样做的额外好处是你能在对比中直观感受到两种架构的差异而不是背概念。3. 用状态机在 Unity 里跑通最小 AI三态怪物的完整实现3.1 搭建最小场景从空场景到可追踪的 NPC目标很明确一个怪物能在空闲、巡逻、追击三个状态间切换。把场景搭出来只要三步建一个 Plane 当地面给怪物放一个 Capsule带 Animator 更好没有也不阻塞逻辑。给怪物挂 NavMeshAgent 组件这是 Unity 内置的寻路方案能自动绕障。在场景里放一个空物体当巡逻点数组的父节点用来标记巡逻路径。注意一个容易漏的配置地面需要先勾选 Navigation 窗口里的 Navigation Static再点击 Bake 按钮否则 NavMeshAgent 根本没有可行走区域agent 会原地打转。代码结构上我一般把状态机拆成三块状态枚举、状态基类、状态实例。先定义枚举和基类public enum AIState { Idle, Patrol, Chase } public abstract class AIStateBase { protected MonsterController controller; public AIStateBase(MonsterController ctrl) { controller ctrl; } public abstract void OnEnter(); public abstract void OnUpdate(); public abstract void OnExit(); }基类做的事情是统一状态生命周期。每个状态只在进入时拿一次需要的组件引用Update 里只做本状态的事退出时清理状态。这个设计能避免后面最常见的“状态切换后上一状态的协程还在跑”的坑。3.2 三个状态的实现与切换条件接下来写三个状态。Idle 最简单进入时记录时间超过指定秒数后切到 PatrolPatrol 让 NavMeshAgent 走向下一个巡逻点到达后换点并切回 IdleChase 负责设置 agent 的目标为玩家超出距离后回到 Patrol。public class MonsterController : MonoBehaviour { public Transform player; public Transform[] patrolPoints; public float chaseRange 8f; public float idleDuration 3f; private NavMeshAgent agent; private AIState currentState; private AIStateBase[] states; void Start() { agent GetComponentNavMeshAgent(); states new AIStateBase[] { new IdleState(this), new PatrolState(this), new ChaseState(this) }; TransitionTo(AIState.Idle); } void Update() { currentState?.OnUpdate(); CheckStateChange(); } void CheckStateChange() { float distToPlayer Vector3.Distance(transform.position, player.position); if (currentState ! AIState.Chase distToPlayer chaseRange) { TransitionTo(AIState.Chase); } else if (currentState AIState.Chase distToPlayer chaseRange * 1.2f) { TransitionTo(AIState.Patrol); } } public void TransitionTo(AIState newState) { currentState?.OnExit(); currentState newState; states[(int)newState].OnEnter(); } public void MoveTo(Vector3 destination) { agent.isStopped false; agent.SetDestination(destination); } }两个关键参数要说明chaseRange是进入追击的触发距离chaseRange * 1.2f是退出追击的滞回距离。为什么要区别对待因为如果进出都用一个值NPC 会在边界来回抖动看起来像抽风。这是状态机调参里最容易忽略的“滞回区间”概念。PatrolState 的到达判断不要用agent.isStopped推荐用剩余距离if (!agent.pathPending agent.remainingDistance 0.5f) { // 到达当前巡逻点切换到下一个 }pathPending这个属性非常关键。SetDestination 之后路径计算是异步的立刻读remainingDistance得到的是 0直接判断到达会导致 NPC 根本没动就去下一个点了。这个坑至少坑了我两小时。4. 从状态机到行为树用 Behavior Tree 重构同一个 NPC4.1 行为树的核心节点Sequence、Selector、Decorator状态机版本跑通后你会立刻碰到一个问题需求方说“能不能让 NPC 血量低于 30% 时先跑不追了”。在状态机里这意味着一组新状态和状态转移条件代码开始膨胀。这时就该考虑行为树了。行为树的核心认知是它不是在“描述状态”而是在“描述决策流程”。每个 tick 从根节点往下走遇到条件不满足就返回失败让父节点换一条分支。三个基础节点要理解透Sequence序列从左到右执行子节点全部成功才返回成功任何一个失败就中断并返回失败。适合“先看有没有目标再走过去再攻击”这类必须全中的流程。Selector选择从左到右尝试子节点任何一个成功就返回成功全部失败才返回失败。适合“能打就打不能打就巡逻”这类优先级选择。Decorator装饰包在子节点外层用来反转结果、限流、加冷却时间。市面上有 Behavior Designer、NodeCanvas 这类插件但 demo 阶段我更建议先手写一个极简树。因为插件有学习成本而且 debug 时你不知道插件内部怎么跑的。手写版 200 行内能完成逻辑全在掌控中。4.2 手写极简行为树的核心框架先定义节点接口和组合节点public enum BTResult { Success, Failure, Running } public abstract class BTNode { public abstract BTResult Execute(); } public class BTSequence : BTNode { private BTNode[] children; private int currentIndex 0; public BTSequence(params BTNode[] nodes) { children nodes; } public override BTResult Execute() { while (currentIndex children.Length) { var result children[currentIndex].Execute(); if (result BTResult.Running) return BTResult.Running; if (result BTResult.Failure) { currentIndex 0; return BTResult.Failure; } currentIndex; } currentIndex 0; return BTResult.Success; } }这里最核心的设计是Running状态。一个巡逻行为是持续性的不能一帧就返回 Success。Running告诉父节点“这事还没干完下帧继续”。不用协程来跑行为树是关键——协程和 Update 的生命周期管理混在一起很容易出现“NPC 死了树还在跑”的玄学问题。行为树的 tick 应该放在 Update 里每帧一次节点内部用时间累计来判断“是否持续了足够久”而不是用 yield 等。4.3 把三态怪物改成行为树版本代码对比与决策逻辑迁移用行为树重写三态怪物逻辑变成这样根节点是 Selector两个子分支——第一条是“追击分支”Sequence检测玩家是否在范围内 → 追向玩家第二条是“巡逻分支”Sequence走到下一个巡逻点 → 等待 → 换下一个点。运行逻辑变成每帧先问“玩家在范围内吗”——在就追不在就继续巡逻。不需要状态机里那种刻意的 TransitionTo行为树天然在每一帧做“决策”。public class BTMonsterController : MonoBehaviour { private BTNode rootNode; void Start() { var chaseBranch new BTSequence( new BTCondition_HasTargetInRange(transform, player, chaseRange), new BTAction_MoveToTarget(agent, player) ); var patrolBranch new BTSequence( new BTAction_MoveToNextPatrolPoint(agent, patrolPoints), new BTAction_Wait(waitDuration) ); rootNode new BTSelector(chaseBranch, patrolBranch); } void Update() { rootNode.Execute(); } }这个重构的价值在于后续要加“血量低就跑”的需求不需要改任何旧节点只需要在 Selector 最前面加一个“逃跑分支”var fleeBranch new BTSequence( new BTCondition_HealthBelow(health, 30f), new BTAction_FleeFromPlayer(agent, player) );这就是行为树的真正优势——新行为对旧行为零侵入。状态机版本做同样的事得改状态枚举、加转移条件、加新状态类至少动五个地方。demo 阶段如果需求变更频繁这个优势能帮你省下大量改 bug 的时间。5. 踩坑记录Demo 能跑只是错觉这些坑真会咬人5.1 NavMeshAgent 不动但 isStopped 已经是 false现象agent 的 destination 已经设置isStopped false但 NPC 站在原地不动。检查 Animator 没有异常没有报错。原因最常见的是地面没烘焙 NavMesh或者烘焙时选错了 agent 类型。另一个高频原因是 agent 的 Radius 或 Height 配置比通道宽/矮导致没有可行路径。还有个隐蔽的NavMeshAgent 的updatePosition被改成 false但代码里没有自己同步 transform 位置。解决先看 Scene 视图里 agent 周围有没有浅蓝色网格。没有就重新 Bake。有的话把 Agent 的 Radius 调小到 0.3 以下再试。最后检查updatePosition是否被某段代码动过——我遇到过一次是团队里有人为了做移动平滑把它关了结果寻路和位移完全脱节。5.2 编辑器里表现正常打包后 AI 完全不动现象在 Editor 里运行NPC 巡逻、追击全正常打成 Windows 包或 Android 包后NPC 像被定身一样。原因这是 NavMesh 烘焙数据没进包。场景里的 NavMesh 是运行时生成的如果代码里没有在运行时动态烘焙或者 Build Settings 里没把包含 NavMesh 数据的场景勾选打包后自然没寻路数据。解决确认 Navigation 窗口里 Bake 过后还必须在 Build Settings 里检查场景是否被打进包。建议再做一层兜底——在目标平台上跑一次运行时烘焙var navMeshData NavMeshBuilder.BuildNavMeshData( NavMesh.GetSettingsByIndex(0), UnityEngine.AI.NavMesh.GetSettingsByIndex(0).agentTypeID, transform.position Vector3.zero, new Vector3(50, 50, 50), Vector3.one, new Bounds(Vector3.zero, new Vector3(10, 10, 10)) );运行时烘焙能解决“发布后没数据”的问题但只推荐在 demo 阶段用性能开销不值得长期保留。5.3 状态切换后上一状态的协程还在跑现象NPC 从 Chase 切到 Idle 后原本的追踪协程依然在修改 agent 的 destinationNPC 反复抖动。Debug 后发现协程还在跑。原因协程一旦 Start只有 StopCoroutine 或所在组件销毁才会停。状态机里 OnExit 只写了状态逻辑清理忘了 Stop 协程。解决在状态基类 OnExit 统一做一次协程清理。或者在切换状态前先遍历当前状态持有的所有协程引用逐个 Stoppublic override void OnExit() { StopCoroutine(chaseRoutine); }同时养一个好习惯所有协程的引用都存字段不直接StartCoroutine(ChaseLoop)用字符串启动。字符串启动的方式在协程重名时是静默失败的排查起来非常玄学。5.4 Time.timeScale 不归一时AI 表现完全不可用现象UI 面板弹出来暂停游戏时 AI 也在动或者把 timeScale 调慢做慢动作AI 的巡逻和追击节奏跟着变快/变慢完全不像设定好的表现。原因协程里的WaitForSeconds受 timeScale 影响。AI 逻辑设计上应该“AI 自己决定什么时候决策”而不是“全部被 timeScale 绑架”。解决行为树节点里不要用WaitForSeconds做延时改用手动计时public class BTAction_Wait : BTNode { private float duration; private float elapsed 0f; public BTAction_Wait(float duration) { this.duration duration; } public override BTResult Execute() { elapsed Time.deltaTime; if (elapsed duration) { elapsed 0f; return BTResult.Success; } return BTResult.Running; } }这样 timeScale 变化时 AI 计时稳定不被系统缩放影响UI 暂停时 AI 也能真正停下来。5.5 AI 用 Transform 移动和 NavMeshAgent 打架现象NPC 用 NavMeshAgent 寻路但又用transform.Translate做朝向或小位移修正结果 NPC 在移动时呈现卡顿感甚至不停抖动。原因NavMeshAgent 每帧会强制执行自己的位置和旋转外部用 Transform 做的修改会被下一帧覆盖产生视觉上的抖动和位移回退。解决所有移动、朝向都交给 NavMeshAgent。需要做朝向修正时设置agent.updateRotation false然后自己用Quaternion.Slerp处理但此时位置的更新依然由 agent 负责。另一个做法是把需要“自己控制移动”的行为比如逃跑也用 NavMeshAgent 的velocity属性来施加方向而不是直接改 transform。6. 收尾技巧一把 Profiler 教你判断 AI 值不值得优化Demo 能跑之后验收时最常被问的是“性能行不行”。先把 Profiler 打开Window → Analysis → Profiler选择 CPU Usage跑五分钟玩法流程然后看两件事。第一看NavMeshAgent在 Update 里的耗时占比。如果占了主线程 15% 以上且 NPC 数量超过 20 个你就有充分理由做分层更新把 NPC 按距离玩家远近分两批近距离每帧更新寻路远距离每 0.5 秒更新一次。实现方式很简单每个 NPC 记录一个下一帧更新时间只有到达时间才调用SetDestination或执行行为树 tick。第二看Animator的耗时。AI demo 里动画状态机常常比 AI 逻辑本身更费性能。一个典型情况是30 个 NPC 每个都有三层动画层每层都在计算 blend。处理方式是把真正需要流畅动画的 NPC 放在近距离远距离 NPC 直接播放预设动画或者降低 Animator 更新频率。用animator.updateMode AnimatorUpdateMode.AnimatePhysics降低动画频率效果立竿见影。最后一招用OnDrawGizmos把 AI 的决策过程画出来。在 Scene 视图里画出玩家检测范围、当前位置的目标点、行为树当前执行的节点名。这招帮我省掉的 debug 时间比什么插件都多void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawWireSphere(transform.position, chaseRange); if (agent ! null agent.hasPath) { Gizmos.color Color.green; Gizmos.DrawLine(transform.position, agent.destination); } }我一直保持的习惯是AI demo 做出来之后先在地面画几条路径线让策划看再拿 Profiler 截图对比最后才谈优化。很多团队在 AI 上翻车不是逻辑写不对而是表现不对、性能说不清。先把这两件事落地AI demo 就算真正站稳了。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。