Unity ML-Agents实战:从零构建强化学习驱动的游戏AI

Unity ML-Agents实战:从零构建强化学习驱动的游戏AI

1. 项目概述:当游戏角色开始“自学成才”

如果你还在用一长串的if-else或者复杂的状态机来驱动你的NPC,是时候换个思路了。想象一下,你设计的敌人不再只是按照预设的路径巡逻,而是会观察你的走位,预判你的攻击,甚至学会利用地形进行伏击;你训练的宠物不再只会执行几个固定的指令,而是能根据环境和你互动的历史,发展出独特的“性格”和行为模式。这听起来像是遥远的未来?不,这就是 Unity ML-Agents 工具箱正在让开发者触手可及的现实。

ML-Agents 本质上是一个桥梁,它将游戏开发世界与机器学习(特别是强化学习)世界连接了起来。它把 Unity 编辑器变成了一个功能强大的“虚拟生物实验室”,而你,就是那个设定进化规则的设计师。你不再需要手把手地教角色每一个动作,而是通过定义“奖励”和“惩罚”,让角色在虚拟环境中通过数百万次的试错,自己找到达成目标的最优策略。这个过程,我们称之为“训练”。训练完成后,你会得到一个神经网络模型(一个.nn文件),这就是角色的“大脑”。把这个大脑装回你的游戏,一个能自主“思考”的智能体就诞生了。

这个工具箱的价值,远不止于让方块学会走路。它适用于任何需要复杂决策、适应性行为或玩家-like AI 的场景:从 RTS 游戏中的单位微操,到开放世界 NPC 的拟真反应,再到解谜游戏中能自适应玩家策略的对手。它降低了机器学习的门槛,让你能用熟悉的 Unity 工作流,创造出真正拥有“学习能力”的游戏内容。

2. 核心原理拆解:奖励驱动的试错学习

要玩转 ML-Agents,你必须理解其核心——强化学习。你可以把它想象成训练一只小狗。小狗不知道“握手”是什么,但你可以在它抬起爪子时给它一块零食(正向奖励),在其他时候不给(或给予轻微惩罚)。经过多次重复,小狗就把“抬爪”和“获得零食”联系了起来。ML-Agents 中的智能体(Agent)就是这只“小狗”,而你就是那个手握零食的训练师。

2.1 强化学习的关键要素

在这个框架里,有几个核心概念构成了智能体与环境的交互循环:

  • 智能体 (Agent):你需要训练的对象,即游戏中的角色。它身上挂载着继承自Agent基类的脚本。
  • 环境 (Environment):你的 Unity 场景。它包含了地面、障碍物、目标等一切外部元素。
  • 状态/观察 (State/Observation):智能体感知环境的方式。在 ML-Agents 中,你通过CollectObservations方法,将环境信息(如自身位置、目标位置、生命值、敌人距离等)转化为一组数字(向量),输入给神经网络。这是智能体的“眼睛”和“耳朵”。
  • 动作 (Action):智能体可以做的事情。在OnActionReceived方法中,你将神经网络输出的数字(例如,一个代表“向前移动力”的 -1 到 1 的值)转化为实际的行为(如给刚体施加一个力)。动作可以是连续的(如转向角度),也可以是离散的(如跳跃、下蹲、开枪)。
  • 奖励 (Reward):驱动学习的核心。你需要在代码中 strategically 地调用AddReward()函数。奖励信号告诉智能体“刚才那一步做得好不好”。达成目标给一个大额正奖励(+10),犯错给一个惩罚(-1),甚至为了鼓励效率,每一步都可以给一个微小的负奖励(-0.001)以激励其尽快完成任务。
  • 策略 (Policy):即神经网络模型。它根据当前的“观察”,决定采取哪个“动作”,以最大化未来累积的奖励期望。训练的过程,就是不断优化这个策略的过程。

2.2 训练流程:PPO 算法与课程学习

ML-Agents 默认使用 PPO(近端策略优化)算法,这是一种非常稳健的强化学习算法。你可以把它理解为一个既鼓励探索(尝试新动作)又防止“学坏”(动作变得太随机)的教练。在后台,Unity 会启动一个 Python 进程,运行训练算法,而你的 Unity 编辑器则作为“环境模拟器”在运行。

更强大的是课程学习 (Curriculum Learning)功能。就像教小孩先学爬再学走一样,你可以把复杂任务分解成多个阶段。在配置文件中,你可以定义:当智能体在简单环境(如无障碍)中的成功率超过 90% 后,自动切换到更难的环境(如加入移动障碍)。这能极大地解决稀疏奖励问题(即智能体在成功前几乎得不到任何有效反馈,导致它无从学起),是训练复杂行为的利器。

注意:奖励函数的设计是整个项目成败的关键,也是最容易“踩坑”的地方。一个设计不当的奖励函数,会导致智能体学会各种意想不到的“作弊”行为。例如,如果你的惩罚是“每存活一秒扣 0.1 分”,而“到达终点加 100 分”,智能体可能会发现,与其冒险冲向终点,不如找个角落躲起来“苟活”,因为扣分速度很慢。这被称为“奖励黑客”。

3. 环境搭建与项目初始化实战

理论说再多,不如动手搭一个。我们从零开始,构建一个经典案例:训练一个立方体 Agent 找到场景中随机位置的目标球。

3.1 基础环境配置

首先,确保你有一个较新版本的 Unity Hub 和 Unity Editor(推荐 2021 LTS 或更新版本)。新建一个 3D 项目,命名为MLAgents_Playground

安装 ML-Agents 包是第一步。打开Window -> Package Manager。点击左上角的+号,选择Add package from git URL...。输入官方包地址:com.unity.ml-agents。点击Add,Unity 会自动下载并安装。这个过程可能会花费几分钟,取决于你的网络。

安装完成后,强烈建议导入示例项目。在 Package Manager 中找到已安装的ML-Agents包,在详情页切换到Samples选项卡,点击Import按钮导入Getting Started示例。这里面包含了多个预制场景和脚本,是极佳的学习参考,能帮你省去大量搭建基础组件的时间。

3.2 构建第一个训练场景

现在,让我们搭建一个最简单的训练场:

  1. 创建地面:在 Hierarchy 窗口右键 ->3D Object -> Plane,重命名为Ground,缩放至合适大小(如 Scale 设为 10, 1, 10)。
  2. 创建智能体:右键 ->3D Object -> Cube,重命名为Agent。将其 Y 轴位置设为 0.5,使其刚好立在地面上。
  3. 创建目标:右键 ->3D Object -> Sphere,重命名为Target。将其放置在离 Agent 一段距离的位置(如 X=5, Y=0.5, Z=5)。
  4. 添加物理与标签
    • 选中Agent,点击Add Component,添加Rigidbody(刚体)。为了简化初期训练,可以暂时取消勾选Use Gravity,避免它因摔倒而增加学习难度。
    • 确保AgentBox Collider
    • 选中Target,确保其有Sphere Collider,并勾选Is Trigger,这样 Agent 可以穿过它并触发事件。
    • Target的 Tag 下拉列表中,点击Add Tag...,新建一个名为Target的标签,并赋予给Target物体。
  5. (可选)创建训练边界:复制几个Cube,缩放成围墙,摆放在Ground边缘,防止 Agent 在训练初期跑出视野。为它们创建一个新 Tag,如Wall

至此,一个最基础的训练环境就搭建好了。接下来,我们要赋予 Agent “灵魂”。

4. 编写智能体脚本:定义观察、动作与奖励

在 Project 窗口右键 ->Create -> C# Script,命名为MoveToTargetAgent。双击打开,我们将逐步实现一个完整 Agent。

4.1 脚本骨架与初始化

using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Sensors; using Unity.MLAgents.Actuators; public class MoveToTargetAgent : Agent // 必须继承自 Agent 类 { // 关键引用 public Transform targetTransform; // 在 Inspector 中拖拽赋值 private Rigidbody agentRigidbody; // 初始化,类似于 Start() public override void Initialize() { agentRigidbody = GetComponent<Rigidbody>(); // 确保刚体存在,如果忘记添加,这里可以自动添加 if (agentRigidbody == null) { agentRigidbody = gameObject.AddComponent<Rigidbody>(); agentRigidbody.useGravity = false; // 按需设置 } } }

4.2 定义观察:告诉大脑“世界是什么样子”

CollectObservations方法负责收集环境信息。我们决定让 Agent 知道三件事:自己在哪、目标在哪、自己跑得多快。

public override void CollectObservations(VectorSensor sensor) { // 观察1: Agent自身的位置 (3个浮点数: x, y, z) sensor.AddObservation(transform.localPosition); // 观察2: 目标点的位置 (3个浮点数) sensor.AddObservation(targetTransform.localPosition); // 观察3: Agent当前的速度 (3个浮点数) sensor.AddObservation(agentRigidbody.velocity); // 总观察值数量 = 3 + 3 + 3 = 9 // 这个数字必须与 Behavior Parameters 组件中的 Vector Observation Space Size 严格一致! }

为什么是这些观察值?位置信息是导航的基础。速度信息则至关重要,它让 Agent 能感知自己的运动状态(是静止、加速还是减速),没有速度信息,Agent 很难学会平稳地启动和停止,容易产生振荡。

4.3 定义动作:大脑决策后“该怎么做”

OnActionReceived是执行指令的地方。我们假设大脑输出两个连续值,分别控制 X 轴和 Z 轴的力。

public override void OnActionReceived(ActionBuffers actions) { // 从大脑的输出缓冲区获取连续动作 float moveX = actions.ContinuousActions[0]; // 范围通常在 [-1, 1] float moveZ = actions.ContinuousActions[1]; // 将输出值转换为力。系数 5f 需要根据你的 Agent 质量和场景大小调整 Vector3 controlSignal = new Vector3(moveX, 0, moveZ); Vector3 force = controlSignal * 5f; // 使用 VelocityChange 力模式,忽略质量,使控制更直接 agentRigidbody.AddForce(force, ForceMode.VelocityChange); // 基于距离的奖励/惩罚设计 float distanceToTarget = Vector3.Distance(transform.localPosition, targetTransform.localPosition); // 核心奖励1:每一步的小惩罚,鼓励快速完成任务(时间惩罚) AddReward(-0.001f); // 核心奖励2:越靠近目标,给予的即时奖励越多(稠密奖励,解决稀疏性问题) // 计算一个基于距离的奖励,距离越近奖励越高 float proximityReward = 0.01f * (1f / (distanceToTarget + 1f)); // 防止除零 AddReward(proximityReward); }

这里引入了“稠密奖励”的概念。如果只在碰到目标时给一个大奖励(稀疏奖励),Agent 在探索初期几乎得不到任何正向反馈,学习会非常缓慢甚至失败。我们通过proximityReward提供了一个连续的、与距离成反比的奖励信号,像一块磁铁,始终将 Agent 引向目标,大大加速了学习过程。

4.4 处理回合与碰撞事件

// 每个训练回合(Episode)开始时调用,用于重置状态 public override void OnEpisodeBegin() { // 随机重置Agent的位置,增加训练的泛化能力 float randomX = Random.Range(-4f, 4f); float randomZ = Random.Range(-4f, 4f); transform.localPosition = new Vector3(randomX, 0.5f, randomZ); // 重置物理状态 agentRigidbody.velocity = Vector3.zero; agentRigidbody.angularVelocity = Vector3.zero; transform.rotation = Quaternion.identity; // 也可以随机重置目标的位置,让学习更具挑战性 // targetTransform.localPosition = new Vector3(Random.Range(-4f, 4f), 0.5f, Random.Range(-4f, 4f)); } // 碰撞检测,用于判断成功或失败 private void OnTriggerEnter(Collider other) { if (other.CompareTag("Target")) { // 成功抵达目标!给予巨额奖励 AddReward(10.0f); // 可选:播放成功音效或粒子效果 // Debug.Log("Target Reached!"); EndEpisode(); // 结束本轮,触发 OnEpisodeBegin } else if (other.CompareTag("Wall")) { // 撞墙了,给予惩罚并结束本轮 AddReward(-2.0f); EndEpisode(); } }

4.5 实现手动控制(Heuristic)用于调试

这是一个极其重要但常被忽略的步骤。它让你可以用键盘控制 Agent,验证你的动作逻辑(移动、转向)是否正常,同时也是调试奖励函数的绝佳工具。

public override void Heuristic(in ActionBuffers actionsOut) { var continuousActions = actionsOut.ContinuousActions; // 将键盘输入映射到动作输出 continuousActions[0] = Input.GetAxis("Horizontal"); // A/D 键对应 X 轴力 continuousActions[1] = Input.GetAxis("Vertical"); // W/S 键对应 Z 轴力 }

5. 编辑器配置与训练启动

脚本写好后,回到 Unity 编辑器进行配置。

5.1 组件挂载与参数设置

  1. MoveToTargetAgent脚本拖到Agent立方体上。
  2. 在 Inspector 中,将场景中的Target球体拖拽到脚本的Target Transform字段。
  3. Agent添加Behavior Parameters组件。这是连接 Agent 脚本与 ML-Agents 训练后端的关键。
    • Behavior Name: 填写MoveToTarget这个名字必须与后续配置文件中的行为名严格一致
    • Vector Observation > Space Size: 填入9。这就是我们CollectObservations方法中AddObservation调用次数的总和。
    • Actions > Continuous Actions: 填入2。对应OnActionReceived中我们期望的两个连续动作值(moveX, moveZ)。
    • Model: 暂时留空。训练完成后,我们会把生成的.nn模型文件拖到这里。
  4. Behavior Parameters组件下方,确保Agent脚本的Use Heuristic在训练时是取消勾选的(我们让算法控制),但在手动测试时可以勾选。

5.2 编写训练配置文件

在项目根目录(与Assets同级)下创建一个新文件夹config。在里面创建一个文本文件,重命名为move_to_target.yaml。用文本编辑器(如 VSCode)打开,输入以下内容:

behaviors: MoveToTarget: # 必须与 Behavior Name 完全一致! trainer_type: ppo # 使用PPO算法 hyperparameters: batch_size: 64 # 每次参数更新使用的经验样本数 buffer_size: 1024 # 经验回放缓冲区大小 learning_rate: 3.0e-4 # 学习率,太大不稳定,太小学得慢 beta: 5.0e-4 # 熵系数,鼓励探索 epsilon: 0.2 # PPO裁剪参数 lambd: 0.95 # GAE参数,权衡偏差与方差 num_epoch: 3 # 每次更新时对数据进行几轮优化 learning_rate_schedule: linear # 学习率随训练步数线性衰减 network_settings: normalize: false # 是否归一化输入,简单任务可关闭 hidden_units: 128 # 神经网络隐藏层神经元数量 num_layers: 2 # 神经网络隐藏层层数 reward_signals: extrinsic: gamma: 0.99 # 折扣因子,越接近1表示越重视远期奖励 strength: 1.0 # 外部奖励的权重 max_steps: 500000 # 最大训练步数 time_horizon: 64 # 每个Agent收集多少步经验后进行一次更新 summary_freq: 10000 # 每多少步输出一次训练摘要到TensorBoard

5.3 启动训练流程

  1. 打开命令行终端(Windows 的 CMD/PowerShell,Mac/Linux 的 Terminal)。
  2. 使用cd命令导航到你的 Unity 项目根目录。
  3. 输入训练命令:
    mlagents-learn config/move_to_target.yaml --run-id=Run_01 --force
    • mlagents-learn: 训练命令。
    • config/move_to_target.yaml: 你的配置文件路径。
    • --run-id=Run_01: 为本次训练运行指定一个名称,用于区分不同实验。
    • --force: 如果同名 run-id 已存在,则覆盖。
  4. 命令行会显示Start training by pressing the Play button in the Unity Editor.
  5. 切回 Unity 编辑器,点击 Play 按钮。此时,场景中可能会出现多个 Agent 的副本(取决于配置),它们会同时开始探索。
  6. 观察命令行窗口,你会看到奖励(Cumulative Reward)等指标在滚动。同时,你可以直接在 Game 视图中观察 Agent 们从“无头苍蝇”到“精准导航”的进化过程。
  7. 训练达到最大步数或你手动Ctrl+C停止后,在项目根目录下的results/Run_01文件夹中,会找到生成的MoveToTarget.nn模型文件。
  8. 将此.nn文件拖入 Unity 项目的Assets文件夹。然后选中Agent,在Behavior Parameters组件的Model字段中,拖入这个模型文件。
  9. 取消勾选Agent脚本上的Use Heuristic,再次点击 Play。现在,你的立方体将使用训练好的 AI 大脑自主导航到目标点!

6. 实战进阶:从走到跑,解锁复杂行为

基础移动只是开始。ML-Agents 的真正威力在于构建复杂、智能的行为。

6.1 添加视觉观察:让 Agent “看见”世界

除了向量观察,ML-Agents 支持基于摄像头的视觉观察。这适用于需要识别形状、颜色或复杂空间关系的任务。

  1. 给 Agent 添加一个子物体作为“眼睛”,比如一个Camera
  2. 在 Agent 脚本中,声明一个CameraSensorComponent类型的公共变量,并在 Inspector 中赋值。
  3. CollectObservations中,你不再需要手动添加目标位置等,神经网络会直接从摄像头画面中学习特征。但请注意,视觉训练对算力要求高,训练时间更长,且需要更精细的奖励设计。

6.2 实现离散动作与组合动作

很多游戏行为是离散的,比如“跳跃”、“开枪”、“切换武器”。ML-Agents 支持离散动作空间。

// 在 Behavior Parameters 中,设置 Discrete Actions 分支,例如定义两个分支: // Branch 0: 3个动作 (0: 不动,1: 左转,2: 右转) // Branch 1: 2个动作 (0: 不跳,1: 跳) public override void OnActionReceived(ActionBuffers actions) { // 处理离散动作 int turnAction = actions.DiscreteActions[0]; // 取值 0, 1, 2 int jumpAction = actions.DiscreteActions[1]; // 取值 0, 1 float turnStrength = 0f; switch (turnAction) { case 1: turnStrength = -1f; break; // 左转 case 2: turnStrength = 1f; break; // 右转 } // 应用转向... if (jumpAction == 1 && IsGrounded()) { // 执行跳跃 agentRigidbody.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } // 同时也可以处理连续动作(如移动) float moveZ = actions.ContinuousActions[0]; // ... 应用移动 }

6.3 多智能体与竞争/协作

在同一个场景中放置多个具有相同Behavior Name的 Agent,它们会共享同一个大脑进行训练。你可以设计奖励机制让它们竞争(如对抗游戏)或协作(如共同搬运物体)。

  • 竞争:给击败对手的 Agent 正奖励,给被击败的 Agent 负奖励。
  • 协作:给共同完成任务的群体一个共享的大奖励。这里需要注意“信用分配”问题,即如何将群体奖励合理地分配给个体。ML-Agents 提供了Group Reward等机制来辅助处理。

6.4 利用课程学习攻克难关

对于非常困难的任务,直接训练可能无法收敛。课程学习允许你定义多个关卡,逐步提升难度。

behaviors: MoveToTarget: trainer_type: ppo # ... 其他超参数 curriculum: - name: EasyLevel # 第一阶段课程 completion_criteria: measure: reward # 以奖励为衡量标准 behavior: MoveToTarget threshold: 1.0 # 当平均奖励超过1.0时,进入下一阶段 min_lesson_length: 1000 # 本阶段至少训练1000步 value: 0.0 # 这个参数会传递给环境,用于控制难度 parameters: obstacle_count: 0 # 无障碍物 target_range: 5.0 # 目标出现范围较小 - name: HardLevel completion_criteria: measure: reward behavior: MoveToTarget threshold: 5.0 min_lesson_length: 5000 value: 1.0 parameters: obstacle_count: 5 # 增加5个障碍物 target_range: 10.0 # 目标出现范围变大

在你的 Agent 脚本中,可以通过Academy实例读取这些课程参数,并动态调整环境(如生成障碍物)。

7. 血泪踩坑指南与调优心法

这部分是无数小时调试换来的经验,能帮你避开最常见的陷阱。

7.1 奖励函数设计:艺术与科学的结合

  • 坑1:奖励稀疏。Agent 在成功前得不到任何反馈。解决:设计“塑形奖励”。例如,在走迷宫时,不仅给最终出口奖励,还给每一步更靠近出口一个微小正奖励。
  • 坑2:奖励冲突。例如,同时奖励“收集金币”和“保持高速”,Agent 可能会为了高速而绕过金币。解决:仔细权衡不同奖励的权重(strength),或者设计更高级的奖励结构(如分层奖励)。
  • 坑3:局部最优与奖励黑客。Agent 找到了一个刷分但不合你意的漏洞。例如,在一个需要跳跃的平台游戏中,如果“存活”有微小正奖励,而“坠落”有大惩罚,Agent 可能学会了一动不动。解决:增加负奖励(惩罚)要非常小心。更好的办法是修改环境或观察空间,堵上漏洞。
  • 实操心得大量使用Heuristic模式进行手动测试。你亲自操作 Agent,感受当前的奖励设置是否合理。你觉得别扭或不合理的地方,AI 学起来也一定困难。

7.2 观察空间:给大脑恰到好处的信息

  • 坑1:信息不足。没给速度,Agent 学不会平稳移动;没给相对角度,Agent 学不会有效转向。解决:从第一性原理思考,完成这个任务,一个智能体最少需要知道哪些信息?
  • 坑2:信息冗余或噪声。提供了无关信息(如天空盒颜色),或包含大量噪声,会干扰学习。解决:保持观察空间简洁、干净、归一化。例如,位置坐标可以相对于某个参考点,或者进行缩放。
  • 坑3:观察值尺度不一。位置坐标可能是几十个单位,而速度值可能是个位数。这会导致神经网络难以处理。解决:在脚本中对观察值进行归一化,或者启用Behavior Parameters中的Normalize选项(对于简单任务可能不需要)。

7.3 超参数调优:没有银弹,只有耐心

配置文件中的超参数没有绝对的最优值。但有一些启发性原则:

  • batch_sizebuffer_size:通常保持默认比例(buffer_sizebatch_size的 10-20 倍)。如果任务复杂,可以适当增大。
  • learning_rate:最常见的调整参数。如果奖励曲线震荡剧烈(上蹿下跳),尝试调低(如1.0e-4)。如果学习速度太慢,曲线几乎不动,尝试调高(如5.0e-4)。使用linear衰减通常是好选择。
  • beta(熵系数):鼓励探索。如果 Agent 过早地陷入单一行为模式,可以适当增加beta值。
  • hidden_unitsnum_layers:对于简单任务(如走到点),1282足够。对于复杂任务(如视觉输入、多智能体),可以尝试增大网络规模(如256512个单元,3层)。

7.4 训练过程监控与调试

  • 使用 TensorBoard:ML-Agents 训练时会自动生成 TensorBoard 日志。在命令行中,进入results文件夹下的具体运行目录,运行tensorboard --logdir .,然后在浏览器打开提示的地址。你可以看到奖励曲线、 episode 长度、熵值等关键指标的可视化图表。奖励曲线稳步上升是健康的;如果剧烈波动或下降,说明有问题。
  • 在 Unity 中实时观察:训练时,多看看 Game 视图。Agent 们是不是在“抽风”?是不是卡在某个角落?这些直观现象能给你最直接的调试线索。
  • 打印日志:在 Agent 脚本的关键位置(如获得大奖励或惩罚时)使用Debug.Log,可以帮助你理解 Agent 在何时因何故受到了奖励。

7.5 环境与物理的坑

  • 物理不稳定:训练中如果大量使用物理(如刚体碰撞),可能会导致模拟不一致,影响学习。解决:尽量简化碰撞体,使用ForceMode.VelocityChange替代ForceMode.Force以获得更直接的控制,或者考虑在训练时固定时间步长(Time.fixedDeltaTime)。
  • Episode 长度失控:如果 Agent 永远无法成功或失败,EndEpisode()不被调用,会导致一个 Episode 无限长。解决:在Agent脚本的FixedUpdate中设置一个最大步数计数器,超时则给予惩罚并EndEpisode()

训练一个强大的游戏 AI 并非一蹴而就,它更像是一场与算法的对话。你通过奖励函数和观察空间提出“问题”,算法通过试错给出“答案”。这个过程需要反复迭代、观察和调整。但当你在游戏中看到那个由你亲手“培育”出来的智能体,做出超出你预设的精彩行为时,那种成就感是无与伦比的。ML-Agents 打开了一扇新的大门,门后是让游戏角色真正“活”过来的无限可能。