简介:这是一份面向Unity初学者与AI开发者的实战型学习资源,聚焦游戏开发中人工智能技术的集成与应用,帮助开发者掌握从基础寻路到复杂决策系统的完整实现路径。资源包含178个文件,以17个C#脚本(cs)、12个工程配置文件(csproj)、6个解决方案(sln)和35个Unity元数据(meta)为主干,辅以asset资源、dll插件及json配置等,全面支撑NavMesh自动寻路、行为树逻辑编排、ML-Agents强化学习训练、传感器感知系统及多智能体协作等核心功能演示,压缩包仅1.35MB,轻量易解压。已有268人学习下载,适合希望快速上手Unity AI模块、理解AI与动画、物理、粒子系统协同机制的学习者。读者可直接运行项目观察AI行为响应,深入源码学习C#控制逻辑,并参考结构化目录快速定位各AI子系统实现细节。
1. Unity AI Demo:不是“AI插件合集”,而是可调试、可替换、可部署的最小闭环验证体
你在网上搜“Unity AI Demo”,十有八九点开是几个悬浮球自动绕圈、NPC原地转头、或者用Behavior Tree拖出三节点就标榜“智能”的工程。但真正压到项目一线的,从来不是“看起来像”,而是“改一行参数就能切算法”“换一个模型文件不崩场景”“在真机上跑满30帧还能吐出决策日志”。这个 Unity AI Demo 就是冲着这个来的——它不封装黑匣子,不隐藏推理链路,整个工程里所有 AI 相关模块(行为决策、状态评估、动作生成)都暴露在 Assets/Scripts/AI/ 下,每个脚本顶部注释写明输入/输出契约,所有模型加载路径用 ScriptableObject 统一管理,连 ONNX Runtime 的 C# binding 都打了 patch 适配 Unity 2021.3 LTS 的 IL2CPP 构建链。它适合两类人:一是刚学完强化学习或行为树理论、手痒想塞进 3D 场景看反馈的开发者;二是正为某跨平台系统做 AI 模块预研,需要快速验证策略泛化性、延迟敏感度、资源占用边界的工程师。别被名字骗了,“Demo”在这里是“可拆解验证单元”的意思,不是“演示用完即弃”。
2. 从空场景起步:构建可复现的 AI 行为验证环境
这个 Demo 的核心价值不在炫技,而在“可控性”。它默认不依赖任何第三方 Asset Store 插件,所有 AI 逻辑走纯 C# 实现 + 可选 ONNX 模型接入,避免了常见陷阱:比如 Behavior Designer 的序列器在 IL2CPP 下序列化失败、ML-Agents 的 Python 依赖导致打包卡死、或自定义 DLL 在 Android 上 ABI 不匹配。我们从零搭起这个验证环境,重点不是“怎么让角色动起来”,而是“怎么确保每次运行输入一致、输出可比、修改可追溯”。
2.1 创建最小化 AI 控制器架构
先新建一个空 Unity 项目(推荐 2021.3.30f1 或 2022.3.25f1,已验证 IL2CPP 兼容性),然后导入 Demo 包中的Core/文件夹。关键结构如下:
Assets/ ├── Core/ │ ├── Scripts/ │ │ ├── AI/ // 所有 AI 相关逻辑 │ │ │ ├── Decision/ // 决策层:BehaviorTree / RuleEngine / ONNXInference │ │ │ ├── State/ // 状态层:SensorManager / PerceptionSystem │ │ │ └── Action/ // 执行层:MotionController / AnimationBlender │ │ └── Utils/ // 工具类:DebugDrawer / ProfilerHook │ └── Resources/ │ └── AIConfig.asset // 全局配置:启用哪套决策器、传感器范围、帧率采样间隔提示:
AIConfig.asset是 ScriptableObject,双击可编辑。它不存硬编码路径,所有模型路径、权重阈值、超参都通过该对象注入,避免改代码才能调参。
2.2 加载并验证基础决策器:RuleEngine 作为基线
RuleEngine 是 Demo 中最轻量、最透明的决策器,用纯 C# 规则链实现,无外部依赖,适合做 baseline 对比。在场景中创建一个空 GameObject,挂载Decision/RuleEngineController.cs脚本:
// Assets/Core/Scripts/AI/Decision/RuleEngineController.cs public class RuleEngineController : MonoBehaviour { [Header("Rule Configuration")] public float detectionRadius = 5f; // 检测半径(米) public LayerMask targetLayer = 1 << 8; // 只检测 Layer 8(Target) [Header("Debug & Output")] public bool logDecisionSteps = true; // 是否打印每步规则触发详情 public TextMeshProUGUI debugText; // UI 文本显示当前状态 private void Update() { var targets = Physics.OverlapSphere(transform.position, detectionRadius, targetLayer); var closest = GetClosestTarget(targets); if (closest != null) { // 规则链:距离 < 2m → 攻击;2~4m → 追踪;>4m → 巡逻 float dist = Vector3.Distance(transform.position, closest.position); string state = dist < 2f ? "ATTACK" : dist < 4f ? "CHASE" : "PATROL"; if (logDecisionSteps) Debug.Log($"[RuleEngine] {state} (dist={dist:F2}m)"); if (debugText) debugText.text = $"State: {state}\nDist: {dist:F2}m"; ExecuteAction(state, closest); } } private void ExecuteAction(string state, Transform target) { switch (state) { case "ATTACK": GetComponent<MotionController>().Attack(target); break; case "CHASE": GetComponent<MotionController>().MoveTo(target.position); break; case "PATROL": GetComponent<MotionController>().Patrol(); break; } } }这段代码的关键不在功能,而在契约清晰:
- 输入:
detectionRadius、targetLayer、transform.position—— 全部可调、可测、可 mock; - 输出:
state字符串 +ExecuteAction()调用 —— 后续替换成 Behavior Tree 或 ONNX 推理时,只需重写Update()中的决策逻辑,ExecuteAction()接口完全不变; - 日志:
logDecisionSteps开关控制是否输出每步判断依据,方便回溯“为什么它突然切到 PATROL”。
2.3 替换为 Behavior Tree:用 Blackboard 解耦状态与行为
当 RuleEngine 无法满足复杂状态流转(如“受伤→寻找掩体→回血→再进攻”)时,Behavior Tree 是更健壮的选择。Demo 中的 BT 实现基于BTNode抽象类,所有节点(Selector、Sequence、Condition、Action)均继承自它,并通过Blackboard共享数据:
// Assets/Core/Scripts/AI/Decision/BehaviorTree/Blackboard.cs public class Blackboard : ScriptableObject { public float health = 100f; public float stamina = 100f; public Vector3 lastKnownEnemyPos; public bool isUnderFire = false; public float timeSinceLastHit = 0f; public void UpdateFromSensor(SensorData data) { health = Mathf.Clamp(data.health, 0f, 100f); stamina = Mathf.Clamp(data.stamina, 0f, 100f); lastKnownEnemyPos = data.enemyPos; isUnderFire = data.isUnderFire; timeSinceLastHit = data.timeSinceLastHit; } }挂载BTController.cs到角色后,它会在Start()中加载Assets/Resources/BT/CombatTree.asset(一个 ScriptableObject 树结构),并在Update()中调用root.Tick()。重点在于:所有条件节点(如IsHealthLowCondition)只读Blackboard,不访问GameObject或Component。这意味着你可以完全在编辑器里模拟Blackboard数据,测试整棵树在不同状态下的执行路径,而无需启动游戏。
参数说明:
CombatTree.asset中每个节点的priority值决定 Selector 并行执行时的抢占顺序;timeoutSeconds控制 Sequence 节点内子节点的最大执行时间,防止单个动作卡死整棵树。
3. ONNX 模型接入实战:把训练好的 PyTorch 策略部署进 Unity
Unity 原生不支持 PyTorch/TensorFlow,但 ONNX 是工业级中间表示标准。这个 Demo 的 ONNX 接入不是“能跑就行”,而是做了三件事:模型输入标准化、推理耗时监控、失败降级兜底。我们以一个简化版的“移动决策模型”为例(输入:自身坐标、敌人坐标、血量、弹药量;输出:x/z 移动方向 + 攻击概率)。
3.1 模型导出与预处理对齐
PyTorch 训练脚本导出 ONNX 时,必须与 Unity 端预处理严格一致。常见翻车点是归一化参数错位:
# train.py - 导出前务必确认 model.eval() dummy_input = torch.randn(1, 6) # [x_self, z_self, x_enemy, z_enemy, hp, ammo] torch.onnx.export( model, dummy_input, "unity_policy.onnx", input_names=["input_state"], output_names=["action_dir", "attack_prob"], dynamic_axes={"input_state": {0: "batch"}, "action_dir": {0: "batch"}, "attack_prob": {0: "batch"}}, opset_version=12 )注意:
opset_version=12是 Unity ONNX Runtime for Unity 插件(v1.12.0)的最高兼容版本,用 14 会报Unsupported opset;dynamic_axes必须声明 batch 维度,否则 Unity 加载时报Shape inference error。
3.2 Unity 端 ONNX 加载与推理封装
Demo 中ONNXInferenceController.cs封装了完整生命周期:
// Assets/Core/Scripts/AI/Decision/ONNXInferenceController.cs public class ONNXInferenceController : MonoBehaviour { [Header("Model Settings")] public TextAsset onnxModel; // 拖入 .onnx 文件(需设为 TextAsset) public string inputName = "input_state"; // ONNX 模型输入名,必须与导出时一致 public string[] outputNames = { "action_dir", "attack_prob" }; [Header("Runtime Control")] public bool enableInference = true; // 运行时开关,便于对比 RuleEngine/ONNX 效果 public float inferenceInterval = 0.1f; // 最小推理间隔(秒),防止单帧多次调用 private InferenceSession session; private float lastInferenceTime = 0f; private Tensor<float> inputTensor; private void Start() { if (onnxModel == null) return; try { session = new InferenceSession(onnxModel.bytes); Debug.Log($"[ONNX] Loaded {onnxModel.name}, inputs: {session.InputMetadata.Count}"); } catch (Exception e) { Debug.LogError($"[ONNX] Load failed: {e.Message}"); enableInference = false; // 自动降级 } } private void Update() { if (!enableInference || Time.time - lastInferenceTime < inferenceInterval) return; // 构造输入:[x_self, z_self, x_enemy, z_enemy, hp, ammo] var state = new float[6]; state[0] = transform.position.x; state[1] = transform.position.z; state[2] = GetClosestEnemyXZ(); // 实际项目中应从 SensorManager 获取 state[3] = GetClosestEnemyXZ(); state[4] = blackboard.health / 100f; // 归一化!必须与训练时一致 state[5] = blackboard.ammo / 30f; // 弹药上限 30 发 inputTensor = new DenseTensor<float>(state, new int[] { 1, 6 }); try { var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(inputName, inputTensor) }; using var outputs = session.Run(inputs); var dirOutput = outputs.First(o => o.Name == outputNames[0]).AsEnumerable<float>().ToArray(); var probOutput = outputs.First(o => o.Name == outputNames[1]).AsEnumerable<float>().ToArray(); // 解析输出:dirOutput[0]=x, dirOutput[1]=z, probOutput[0]=attack prob HandleONNXOutput(dirOutput[0], dirOutput[1], probOutput[0]); lastInferenceTime = Time.time; } catch (OnnxRuntimeException e) { Debug.LogError($"[ONNX] Inference failed: {e.Message}"); // 降级到 RuleEngine FallbackToRuleEngine(); } } }这段代码的血泪经验在于:
inputTensor必须是DenseTensor<float>,不能用float[]直接传,否则 ONNX Runtime 报Invalid tensor type;GetClosestEnemyXZ()返回的是世界坐标,但模型训练时用的是局部坐标?那就要在构造state前做transform.InverseTransformPoint(enemyPos);HandleONNXOutput()函数里必须做后处理:比如attack_prob > 0.7f才触发攻击,而不是直接用原始浮点值。
3.3 性能监控与热更新支持
ONNX 推理耗时是移动端瓶颈。Demo 在Utils/ProfilerHook.cs中埋点:
// Assets/Core/Scripts/Utils/ProfilerHook.cs public static class ProfilerHook { public static void LogONNXInferenceTime(float ms) { if (ms > 16f) // 超过 1 帧(60fps) Debug.LogWarning($"[ONNX Perf] Inference took {ms:F2}ms (>16ms)"); // 记录到 CSV 供后续分析 File.AppendAllText("Logs/onnx_perf.csv", $"{Time.realtimeSinceStartup},{ms}\n"); } }调用位置在ONNXInferenceController.Update()的try块末尾:ProfilerHook.LogONNXInferenceTime((Time.realtimeSinceStartup - startTime) * 1000);。这样你能在真机上跑 5 分钟,导出 CSV 用 Excel 画出耗时分布图,判断是否要量化模型或降低输入维度。
提示:热更新时,只需替换
Resources/AIModels/policy_v2.onnx文件,重启游戏即可加载新模型,无需重新打包 APK/IPA。这是靠Resources.Load<TextAsset>("AIModels/policy_v2")实现的,路径硬编码在脚本里,但实际项目建议用 Addressables。
4. 避坑指南:五个真实踩过的坑与当场解决法
这个 Demo 在多个模拟项目 X 和某跨平台系统中实测过,以下问题不是“理论上可能”,而是“我们真在凌晨三点遇到并修好的”。
4.1 现象:Android 构建后 ONNX 推理返回全零向量,Editor 中正常
原因:IL2CPP 剥离了 ONNX Runtime 的反射调用所需类型。InferenceSession构造时依赖System.Reflection加载算子,但默认剥离规则会删掉Microsoft.ML.OnnxRuntime命名空间下的非公开类。
解决:在Assets/Plugins/下新建link.xml文件,强制保留:
<linker> <assembly fullname="Microsoft.ML.OnnxRuntime" preserve="all"/> <assembly fullname="Microsoft.ML.OnnxRuntime.Managed" preserve="all"/> </linker>注意:
link.xml必须放在Plugins文件夹下,且Build Settings > Player Settings > Other Settings > Managed Stripping Level设为Medium或Low,High会忽略link.xml。
4.2 现象:Behavior Tree 在 Editor 中运行正常,Build 后Blackboard值始终为默认值(0/0/false)
原因:Blackboard是ScriptableObject,其字段在 Build 时若未被任何脚本显式引用(如public Blackboard bb;),Unity 的脚本剥离器会认为它是“未使用资源”而丢弃。
解决:在BTController.cs中添加[SerializeField] private Blackboard _blackboard;并在 Inspector 中手动拖入Assets/Resources/AI/DefaultBlackboard.asset;或在Awake()中强制Resources.Load<Blackboard>("AI/DefaultBlackboard")。
4.3 现象:RuleEngine 的Physics.OverlapSphere在真机上检测不到目标,Editor 中正常
原因:Android 设备的物理引擎默认使用Fast Approximation模式,OverlapSphere的精度下降,尤其当目标 Collider 是MeshCollider且未勾选Convex时。
解决:在Edit > Project Settings > Physics中,将Default Contact Offset从 0.01 降到 0.005,Solver Iteration Count从 6 提到 12;或改用Physics.SphereCast(精度更高但开销略大)。
4.4 现象:切换 ONNX 模型后,第一次推理极慢(>500ms),后续正常
原因:ONNX Runtime 首次加载模型时会进行图优化(Graph Optimization)和算子编译(Kernel Compilation),这是正常行为,但会卡住主线程。
解决:在Start()中异步预热:
private void Start() { StartCoroutine(PreheatONNX()); } private IEnumerator PreheatONNX() { yield return new WaitForSeconds(0.1f); // 确保场景初始化完成 var dummy = new float[1, 6]; // 构造假输入 var tensor = new DenseTensor<float>(dummy, new int[] { 1, 6 }); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(inputName, tensor) }; using var _ = session.Run(inputs); // 丢弃结果,只触发预热 Debug.Log("[ONNX] Preheated"); }4.5 现象:Debug.Log在真机上不输出,导致无法排查 ONNX 失败原因
原因:Android Logcat 默认过滤级别为Warning,而Debug.Log是Log级别,被过滤。
解决:在Player Settings > Other Settings > Logging中,勾选Script Debugging和Development Build;或改用Debug.LogWarning/Debug.LogError输出关键路径;终极方案是用Application.logMessageReceived拦截所有日志写入本地文件。
5. 真机性能压测与多策略 A/B 测试框架
压测不是“跑一遍看卡不卡”,而是建立可量化的指标体系。这个 Demo 内置了一套轻量 A/B 测试框架,让你在一台设备上并行跑 RuleEngine、Behavior Tree、ONNX 三套策略,用同一套传感器输入驱动,对比决策延迟、CPU 占用、内存峰值。这才是工程落地的核心能力。
5.1 构建多策略并行测试场景
在Assets/Scenes/TestAB.unity中,放置三个相同配置的 AI 角色(Prefab 名:AI_Rule、AI_BT、AI_ONNX),每个挂载对应控制器,并共享同一个SensorManager实例:
// Assets/Core/Scripts/AI/State/SensorManager.cs public class SensorManager : MonoBehaviour { public static SensorManager Instance; private List<ISensorInput> _inputs = new List<ISensorInput>(); private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } // 所有 AI 控制器在 Update() 开头调用此方法获取统一感知数据 public SensorData GetLatestPerception() { var data = new SensorData(); foreach (var input in _inputs) { input.UpdateSensor(ref data); // 如 EnemyDetector.Update(ref data) } return data; } }SensorData是一个 struct,包含Vector3 enemyPos、float health等字段,所有策略读取的是同一份快照,消除“时间差”干扰。
5.2 量化指标采集与可视化
Demo 在Utils/PerformanceMonitor.cs中定义了三类指标:
| 指标类型 | 采集方式 | 存储位置 | 查看方式 |
|---|---|---|---|
| 决策延迟 | Stopwatch.StartNew()在Update()开头,Stop()在决策逻辑结束 | List<float>滑动窗口(默认存最近 100 帧) | DebugText显示Avg: 8.2ms ± 1.1ms |
| CPU 占用 | System.Diagnostics.Process.GetCurrentProcess().TotalProcessorTime.TotalMilliseconds | 每秒采样一次,存List<float> | 导出 CSV 后用 Python 画折线图 |
| 内存峰值 | GC.GetTotalMemory(false)+Profiler.GetMonoUsedSizeLong() | 每 5 秒记录一次 | Debug.Log($"[MEM] {usedMB:F1}MB") |
关键代码段(PerformanceMonitor.cs):
public class PerformanceMonitor : MonoBehaviour { [Header("Metrics")] public float decisionLatencyAvg = 0f; public float cpuUsageAvg = 0f; public float memoryPeakMB = 0f; private Stopwatch _decisionStopwatch = new Stopwatch(); private List<float> _latencyHistory = new List<float>(100); private float _lastCpuCheck = 0f; private long _lastGCMem = 0L; private void Update() { // 1. 决策延迟:由各 AI 控制器在决策前/后调用 Start/Stop if (_decisionStopwatch.IsRunning) _decisionStopwatch.Stop(); if (_decisionStopwatch.ElapsedMilliseconds > 0) { _latencyHistory.Add((float)_decisionStopwatch.ElapsedMilliseconds); if (_latencyHistory.Count > 100) _latencyHistory.RemoveAt(0); decisionLatencyAvg = _latencyHistory.Average(); } _decisionStopwatch.Restart(); // 2. CPU 使用率(粗略估算) if (Time.time - _lastCpuCheck > 1f) { var proc = Process.GetCurrentProcess(); cpuUsageAvg = (proc.TotalProcessorTime.TotalMilliseconds - _lastCpuCheck * 1000) / (Time.deltaTime * 1000) * 100f; _lastCpuCheck = Time.time; } // 3. 内存峰值 long currentMem = GC.GetTotalMemory(false) + Profiler.GetMonoUsedSizeLong(); if (currentMem > _lastGCMem) _lastGCMem = currentMem; memoryPeakMB = _lastGCMem / (1024f * 1024f); } }提示:
decisionLatencyAvg的计算逻辑必须放在PerformanceMonitor.Update(),而非各 AI 控制器内部,否则多策略并行时会互相干扰计时。
5.3 A/B 测试报告生成与决策
压测结束后,一键生成 HTML 报告(Tools/GenerateABReport.cs):
// Tools/GenerateABReport.cs - Editor script [MenuItem("Tools/Generate A-B Report")] public static void GenerateReport() { var report = new StringBuilder(); report.AppendLine("<html><body><h1>AI Strategy A/B Test Report</h1>"); report.AppendLine("<table border='1'><tr><th>Metric</th><th>RuleEngine</th><th>BehaviorTree</th><th>ONNX</th></tr>"); var metrics = new[] { ("Decision Latency (ms)", GetAvgLatency("Rule")), ("CPU Usage (%)", GetAvgCpu("BT")), ("Memory Peak (MB)", GetPeakMem("ONNX")), ("Frame Drop Rate", GetDropRate()) }; foreach (var (name, values) in metrics) { report.AppendLine($"<tr><td>{name}</td>" + $"<td>{values[0]:F2}</td><td>{values[1]:F2}</td><td>{values[2]:F2}</td></tr>"); } report.AppendLine("</table></body></html>"); File.WriteAllText($"{Application.dataPath}/Reports/AB_Report_{DateTime.Now:yyyyMMdd_HHmmss}.html", report.ToString()); Debug.Log("A/B Report generated!"); }报告里最关键的不是“哪个最快”,而是看波动性:比如 ONNX 平均延迟 12ms,但 P95 达到 45ms,说明存在长尾抖动,不适合实时战斗;而 RuleEngine 平均 8ms,P95 仅 10ms,稳定性碾压。这时候你就知道:高动态场景用 RuleEngine 做主控,ONNX 只用于非关键路径(如对话情绪生成)。
从那以后我每次接到新 AI 模块需求,都强制走一遍这个流程:先用 RuleEngine 写 baseline,再用 Behavior Tree 做状态扩展,最后只把最耗计算的子模块(如路径重规划)替换成 ONNX。不是为了炫技,而是让每个技术选型都有数据支撑,而不是靠“感觉”。希望帮到你。
本文还有配套的精品资源,点击获取