
简介这是一份面向Unity初中级开发者的AI功能演示项目以实际场景串联导航寻路、行为树决策、ML-Agents强化学习、动画状态机等关键模块帮助理解游戏角色智能行为从感知到响应的完整链路。资源包共178个文件压缩后约1.35MB以cs脚本、asset资源、dll库、meta元数据及工程配置文件为主便于直接导入Unity查看工程结构与脚本绑定关系。目前已获得266人学习下载适合正在做NPC角色、敌人AI或自动化决策系统的开发者参考。项目内容涵盖NavMesh烘焙与NavMeshAgent参数调整、行为树节点设计、基于物理碰撞的避障、动画与AI状态联动以及多智能体协作等具体案例可从中提取可直接复用的脚本片段和设计思路。通过拆解Demo还能熟悉Unity AI从环境感知、逻辑决策到动画表现和性能优化的标准工作流。1. Unity AI demo 的三种做法决定了你后面是省心还是返工我第一次做 Unity AI demo 的时候犯过一个不太聪明的错误在场景里摆了十几个 NPC全部挂上巡游脚本装好行为树以为这就是 AI 了。结果产品一句话让整个方案推倒重来——他要的是 NPC 能听懂玩家提问再走过去做点事。于是我发现这个标题背后其实压着三条完全不同的技术路线NavMesh 加状态机的传统游戏 AI解决寻路和决策ML-Agents 强化学习解决策略训练大模型接入解决语言交互。选错路线的返工成本比把 demo 写出来高得多。这篇就以一个能对话、会行动的 NPC demo 为终点把选型、实现、参数和坑位依次讲清楚。适合正在准备 AI 展示需求、且 Unity 基础已经过关的开发者。2. 开局先选路线行为树、强化学习还是大模型驱动2.1 三条路线的适用边界和踩坑点NavMesh 加状态机是最“古早但稳定”的方案先烘焙导航网格AI 角色通过 NavMeshAgent 寻路再用有限状态机或行为树决定“现在该做什么”。它的突出优点就是确定性同样的输入永远有同样的行为评审现场不会因为一次随机数让 NPC 卡进墙角。用它做巡逻、追击、警戒这类传统怪最合适数字孪生项目里设备按工序启停这种刚需场景也基本靠行为树而不是大模型。缺点也很直接——所有决策都是人写好的NPC 没有任何“理解”能力。我一般在行为树里习惯把移动、转向、等待封装成共享节点后面接到大模型时这些节点就是现成的动作插槽。ML-Agents 路线是 Unity 官方的强化学习训练框架训练脚本用 Python训练一个会玩障碍跳的 Agent 要跑几千回合。它适合的问题是“我们知道好结果但写不出规则”的连续决策。调 reward 是出了名的玄学训练出来的策略是个黑匣子它为什么选择绕左边而不是跳过去没人能解释。如果你的 demo 定位是展示“自适应策略”这条路线值得投入如果只是为了交互展示它的人力成本往往超过收益。大模型驱动路线是把对话、指令理解这类任务交给大模型 APINPC 第一次能“听懂人话”。这种 demo 的震撼力最强客户端包体几乎不涨因为模型不在本地。但它同时把延迟、内容失控和调用成本系统性地带进了游戏里需要专门的参数调配和失败兜底。做这类演示最好是 Unity 端只保留最基本的动作系统把 AI 请求统一收敛到一个封装层里后面换模型、加缓存、做队列都不至于伤筋动骨。2.2 怎么选用一张决策表把成本和体验对齐demo 的第一目标推荐路线主要成本巡逻、追击、状态切换NavMesh 行为树网格烘焙 1 小时脚本当天出自适应策略学会跑跳避障ML-Agents环境建模 训练时间 12 天起步reward 调多久看运气语言对话、指令理解大模型接入请求封装半天参数调试一周性能预算要留好要对话又要行动行为树 大模型混合先做行为树打底再让大模型输出动作指令混合方案是我现在做 AI demo 的默认答案NavMesh 负责移动状态机负责动作切换大模型只做“决策输入”。这样即使大模型断网NPC 也能退回巡逻状态演示永远不会全崩。选型一旦定了后面章节的代码就是往这个框架里填空。2.3 最小验证用 NavMeshAgent 把第一个 AI 角色跑起来无论后面接什么方案先把移动底座跑通。在场景里建一个地面和几个方块选中所有静态地面在 Inspector 里勾选 Navigation Static。烘焙面板在 Window AI Navigation点底部的 Bake 后Scene 视图里会出现蓝色网格。新手最常见的问题就是地台没勾 Navigation StaticBake 结束后蓝色网格是空的或整片缺失。烘焙完成后在一个 Capsule 上挂 NavMeshAgent然后挂这个巡逻脚本using UnityEngine; using UnityEngine.AI; public class PatrolAI : MonoBehaviour { [SerializeField] private Transform[] waypoints; // 巡逻点 [SerializeField] private float waitSeconds 2f; // 到点后停留时间 private NavMeshAgent agent; private int waypointIndex; private float waitTimer; private void Start() { agent GetComponentNavMeshAgent(); MoveToNextWaypoint(); } private void Update() { // remainingDistance 只对当前路径有效pathPending 表示还在计算路径两个要一起判 if (!agent.pathPending agent.remainingDistance agent.stoppingDistance) { waitTimer Time.deltaTime; if (waitTimer waitSeconds) { waitTimer 0f; MoveToNextWaypoint(); } } } private void MoveToNextWaypoint() { agent.destination waypoints[waypointIndex].position; waypointIndex (waypointIndex 1) % waypoints.Length; } }这段代码里最容易写错的是到达判定。不要用agent.remainingDistance 0路径最后一次采样点往往停在 stoppingDistance 之外永远等不到 0也不要在没有判pathPending的情况下直接读 remainingDistance首次寻路时路径还没算完remainingDistance 可能短暂返回 0NPC 会在起点原地罚站。把这两个条件一起判再叠加目标点停留时间就是一个能直接用于演示的巡逻 AI。2.4 烘焙后先查三样东西能省你一下午第一NavMeshAgent 挂上后没有自动吸附到地面说明目标点不可达或该区域被烘焙层排除了。选中地面看 Navigation Static 是否已勾选以及 Area 类型是不是 Walkable。第二路径明显穿了墙多半是墙的厚度比 Agent 的 radius 还小寻路认为穿不过去。把墙加厚或把 Agent radius 调小我一般优先加厚墙体而不是改 radius因为改半径会连带影响整个场景的寻路结果。第三烘焙出来的网格覆盖了玩家不该走的地方比如演示用的水面或玻璃地面。把对应物件标成 Not Walkable 后重新烘焙记得确认层级里没有残留的旧网格。3. 把大模型接进 Unity做一个会对话也会行动的 AI NPC3.1 用 UnityWebRequest 封装一次对话请求先让 NPC 开口Unity 里最稳妥的 HTTP 请求方式是 UnityWebRequest。我习惯把它封装成一个协程方法而不是用 async/await——协程不需要额外引入 Task 库兼容旧版本也不会因为忘记 ConfigureAwait 而出诡异问题。下面这个类只做一件事发一次对话请求把返回文本通过回调交给外层。using System; using System.Collections; using System.Text; using UnityEngine; using UnityEngine.Networking; [Serializable] public class ChatRequest { public string model; public Msg[] messages; public float temperature; public int max_tokens; } [Serializable] public class Msg { public string role; public string content; } [Serializable] public class ChatResponse { public Choice[] choices; } [Serializable] public class Choice { public Message message; } [Serializable] public class Message { public string content; } public class LLMRequester : MonoBehaviour { [SerializeField] private string endpoint http://127.0.0.1:8000/v1/chat/completions; [SerializeField] private string apiKey demo-key; [SerializeField] private string model demo-model; public IEnumerator Ask(string userText, Actionstring onReply) { ChatRequest payload new ChatRequest { model model, messages new[] { new Msg { role system, content 你是游戏中负责引路的NPC。每次回复不超过30字。 }, new Msg { role user, content userText } }, temperature 0.3f, max_tokens 128 }; string json JsonUtility.ToJson(payload); using (UnityWebRequest req new UnityWebRequest(endpoint, POST)) { req.uploadHandler new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)); req.downloadHandler new DownloadHandlerBuffer(); req.SetRequestHeader(Content-Type, application/json); req.timeout 15; yield return req.SendWebRequest(); if (req.result UnityWebRequest.Result.Success) { ChatResponse data JsonUtility.FromJsonChatResponse(req.downloadHandler.text); string reply data.choices[0].message.content; onReply?.Invoke(reply); } else { Debug.LogError($request failed: code{req.responseCode} err{req.error}); onReply?.Invoke(string.Empty); } } } }这段代码有四个值得注意的地方。一是协程里 yield 的是SendWebRequest()网络 IO 不会阻塞主线程一旦有人图省事改成Task.Wait()演示时按下按钮就会白屏。二是 JsonUtility 解析数组必须写强类型字段上面用Choice[]而不是 ArrayList因为 Unity 序列化器不认泛型集合。三是system prompt里写“不超过30字”是因为游戏 NPC 说话简练远比说全重要而且它直接约束了 max_tokens 的消耗。四是 timeout 设 15 秒超时后走 else 分支onReply 收到空字符串外层逻辑可以做兜底。3.2 把回复文本变成动作指令用前缀路由拆解 AI 输出大模型天然会输出多余内容比如“好的我这就去门口”但程序不知道“门口”是哪个对象。所以我在 system prompt 里明确要求输出格式只允许三种[Speak] 说话内容、[MoveTo] 目标名称、[Idle]。然后写一个专门解析动作的路由器public class NpcActionRouter : MonoBehaviour { [SerializeField] private NpcBrain brain; public void Execute(string aiText) { string text aiText.Trim(); if (text.StartsWith([MoveTo])) { string target text.Replace([MoveTo], ).Trim(); brain.MoveTo(target); } else if (text.StartsWith([Speak])) { string dialog text.Replace([Speak], ).Trim(); brain.Speak(dialog); } else if (text.StartsWith([Idle])) { brain.EnterIdle(); } else { // 兜底识别失败就站在原地不让 NPC 乱跑 brain.EnterIdle(); Debug.LogWarning($unrecognized AI output: {aiText}); } } }前缀路由的好处是只匹配开头几个字符不依赖 JSON 结构完整性。很多本地模型输出的 JSON 不一定合法但前缀文本往往稳定。兜底分支必须有AI 输出永远可能不符合格式进入 Idle 比“NPC 突然向一个不存在的坐标跑过去”安全得多。如果要做更严格的方案可以要求模型输出{action:move_to,target:gate}这样的 JSON但解析失败时要做好容错演示环境下我宁可选择前缀路由这种皮实写法。3.3 从“会说话”到“会行动”行为状态机接管决策现在让 NPC 真正动起来。大模型只负责给出意图状态机负责把意图变成可执行动作同时管理动画、移动和对话 UI 的组合。这里一个最简单的三态大脑public enum NpcState { Idle, Moving, Speaking } public class NpcBrain : MonoBehaviour { public NpcState state NpcState.Idle; [SerializeField] private NavMeshAgent agent; [SerializeField] private Animator animator; public void MoveTo(Transform target) { if (target null) { EnterIdle(); return; } state NpcState.Moving; agent.isStopped false; agent.destination target.position; animator.SetBool(walking, true); } public void Speak(string dialog) { state NpcState.Speaking; agent.isStopped true; // 说话期间不允许继续移动 DialogBoard.Instance.Show(dialog); animator.SetBool(talking, true); } public void EnterIdle() { state NpcState.Idle; agent.isStopped true; animator.SetBool(walking, false); animator.SetBool(talking, false); } }这就是一个完整的 AI Agent 闭环感知玩家提问→ 决策大模型返回前缀指令→ 行动状态机驱动 NavMeshAgent 和动画。状态机这一层很关键它把不可控的模型输出转成了可控的游戏表现。比如 Speak 的时候强制agent.isStopped true避免 NPC 一边说话一边飘向目标点MoveTo 的时候先判目标是否为空为空就回 Idle。这些细节不写AI 再聪明表现层也会在演示现场露怯。4. 参数与性能预算让 AI demo 在演示时不掉链子4.1 NavMeshAgent 的五个必调参数与调法NavMeshAgent 默认参数能跑但手感很差。下面这五个是我每次做 demo 必调的参数常见起始值影响与调法speed3.5数值低让 NPC 显得木讷过高会漂移演示场景建议 34angularSpeed120转向速度过低时 NPC 会“画龙”过高看起来像瞬移acceleration8加速过猛时启动瞬间镜头能看到顿挫和 speed 配合调stoppingDistance0.2必须大于 0路径终点和该值之间会有“够不着”现象radius / height0.5 / 2.0标准人形radius 大于墙缝会绕路小于墙角会导致贴墙抖动调参顺序建议先 speed 和 angularSpeed 确定手感再调 stoppingDistance 控制到点精度最后才动 radius 和 height。手动改 radius 会影响整个场景的寻路结果不要用“每只怪各调一个 radius”的方式去修抖动那是治标不治本。4.2 大模型请求的四个边界参数大模型接入之后真正决定 demo 成败的是请求参数不是模型选型。参数推荐值为什么temperature0.20.4游戏 NPC 要稳定需要可复现太高会输出随机行为max_tokens64128生成长度直接决定延迟演示场合越短越好timeout15 秒太长演示画面会停滞太短本地模型第一轮容易失败请求频率每个 NPC 至少间隔 0.5 秒多个 AI 同时提问时靠队列控制不能让一帧打出十几个请求提示不要在客户端写死真实 API Key。内部 demo 图省事把 Key 放在 App 里发布版会被抓包提取。规范做法是 Unity 端请求走自己的后端代理Key 只留在服务端。4.3 速度获取与 Time.timeScale 的配合如果 AI 角色需要读速度做表现脚步声、受击反馈、动画混合你会发现从 Rigidbody.velocity 读出来是 0——因为 NavMeshAgent 的移动不走物理系统。正确做法是直接读 agent.velocityVector3 navVelocity agent.velocity; // 导航计算的速度方向大小 float speed01 navVelocity.magnitude / agent.speed; // 归一化直接喂给动画混合树再注意 Time.timeScale。暂停菜单把 timeScale 设为 0 时状态机的 Update 依然会跑但 Time.deltaTime 是 0agent.isStopped 不会自动替你停住 NavMeshAgentAI 在暂停状态下可能继续移动。我习惯在暂停时显式调agent.isStopped true恢复后再放开。另外协程不受 timeScale 影响大模型请求在暂停时依然会回来并触发回调所以 onReply 里要判断当前是否处于暂停态避免出现 NPC 在暂停画面里说话的穿帮。5. AI demo 避坑实录五个最容易翻车的现场以下五条都是真机演示时踩出来的血泪经验。提前看完至少能让你避开 80% 的返工。5.1 NPC 全程抖动离墙越近越明显现象角色在接近目的地或沿墙走时出现高频原地踏步像贴图抖动。 原因NavMeshAgent 与 Rigidbody 同时控制位置。Rigidbody 每物理帧把位置拉回原处NavMeshAgent 又按路径把位置推走两套坐标互相打架。 解决两选一。如果角色不需要物理碰撞把 Rigidbody 设成 Kinematic如果必须保留物理效果关闭 NavMeshAgent 的自动移动在 FixedUpdate 里自己同步 transform。关键是别让两套系统都写 transform.position。5.2 演示中画面白屏一秒然后恢复现象按下“问 NPC”按钮画面卡死鼠标转圈过一两秒恢复。 原因图省事把协程改成 async 方法后用 Task.Wait() 等结果主线程被网络 IO 阻塞到返回。 解决回到上一章的协程写法。同时做一个演示前的预防动作进入展示场景时预发一条“ping”消息提前建立连接、验证服务端在线现场不会因为首次请求超时冷场。5.3 AI 说了一长段话NPC 却站在原地现象对话气泡显示一大段文字NPC 的走路动画、寻路全没触发。 原因行为树或状态机没有从这一大段文本里提取动作意图AI 输出被当成纯文本渲染了执行层没有消费到指令。 解决把提示词改成“先给动作前缀再给一句话”用前缀路由消费同时不管识别到什么动作都必须有兜底。更稳妥的是把 max_tokens 压小抑制长篇输出的概率。5.4 ML-Agents 训练的模型发布版里动作崩坏现象编辑器里跑得好好的 AgentBuild 到 Windows 或 Android 后路线乱走、动作抽搐。 原因决策频率不一致。训练时编辑器帧率不固定Decision Interval 对应的实际步长和发布版不一致发布版开了垂直同步后物理步长与 Agent 决策步长错位。 解决在训练和发布环境里统一 Time.fixedDeltaTime 与 Application.targetFrameRate把决策频率固定为每秒 1015 次而不是每 N 帧一次。真机验证时先开 Profiler 看推理耗时不要只看画面。5.5 内存只增不减粒子特效和热更资源一起泄漏现象演示跑到三四十分钟后帧率下降内存曲线一路往上重启恢复。常见于带了粒子特效和热更资源的大 demo。 原因ParticleSystem 循环播放但没人 Stop用协程开的特效在 NPC 回收后协程一直挂Addressables 加载的 Prefab 只 Load 不 ReleaseUnity 6 的 GPU skinning 虽把蒙皮计算挪到 GPU但显存里每一帧提交的骨架数据如果没随资源卸载同样会累积。 解决特效播放完毕显式 Stop 并销毁循环粒子在 NPC 切状态时主动停Addressables 对应引用计数减一不要“Load 完就不管”发布前跑 30 分钟 CPU/GPU 内存曲线重点看代际曲线是否逐级抬高。6. 进阶把单个 AI 扩成多 Agent 协作的中控调度层如果只做一个 NPC协程直接调就行。但 demo 做到第二个、第三个 NPC 时每个都独立发请求现场就会出现接口限流和回复错位。把决策请求从“每个 NPC 自己发”收归到全局队列里是投入产出比最高的一步。6.1 事件总线NPC 之间不直接互相调用A 角色走到 B 面前B 该怎么知道直接在 A 脚本里引用 B写死引用会让演示场景改动成本变高。我一般在场景里放一个静态事件总线A 只发一条OnNpcArrived事件B 如果关心就订阅。事件总线让 AI Agent 之间的协作解耦这也是后面多 AI 编排的地基。6.2 全局请求队列多 AI 不把接口打爆public class LLMRequestQueue : MonoBehaviour { private readonly QueueSystem.FuncSystem.Collections.IEnumerator tasks new QueueSystem.FuncSystem.Collections.IEnumerator(); private bool running; public void Enqueue(System.FuncSystem.Collections.IEnumerator task) { tasks.Enqueue(task); if (!running) { StartCoroutine(Pump()); } } private System.Collections.IEnumerator Pump() { running true; while (tasks.Count 0) { System.FuncSystem.Collections.IEnumerator task tasks.Dequeue(); yield return task(); yield return new WaitForSeconds(0.3f); } running false; } }队列里放的是协程工厂方法不直接放协程对象避免在入队时就把请求发出去。串行间隔 0.3 秒是为了让本地模型和远端接口都喘口气。相同问题命中本地缓存后直接返回不再入队现场演示时多个 NPC 同时问同一件事不会重复烧 token。我最早给六个 NPC 各写了独立的 LLMRequester第一次评审演示六个角色同时追问接口 429画面卡了两秒那个尴尬我记得很清楚。从那以后凡是 AI demo 里的决策请求我先过队列再谈并发凡是 AI 输出我先做兜底再谈效果。这两条习惯救了我很多次希望帮到你。本文还有配套的精品资源点击获取