在本系列的第 1 部分中,我们提供了 Arm 博士的 Boss 战斗演示的总体概述。第 2 部分更深入地了解游戏 AI 代理的设计方式以及生成的神经网络 (NN) 模型的外观。
代理设计
一旦决定了boss战的策略,下一步就是设计代理。设计代理主要需要明确四项。更多关于代理设计的信息可以在这里找到。
1、代理输入:代理需要什么信息?
我们首先必须考虑代理需要哪些信息来完成目标任务。在我们的演示中,输入包括统计数据、动作事件以及目标和代理本身的位置。统计数据是生命值、法力值和耐力值。动作事件是攻击、滚动和射击。我们通过两种方式收集此类信息。
一种方法是从代理 C# 代码向代理提供信息。主要是通过这种方式传递stats和action事件,如下:
// Customized class to manage a character's state
public PlayerManager _manager;
public PlayerManager _enemyManager;
public override void CollectObservations(VectorSensor sensor)
{
// Collect my state and add them to observations
// Normalize a value to [0, 1] by dividing its max value
sensor.AddObservation(_manager.Stats.CurrentHealth / _manager.Stats.MaxHealth);
sensor.AddObservation(_manager.Stats.CurrentStamina / _manager.Stats.MaxStanima);
sensor.AddObservation(_manager.Stats.CurrentMana / _manager.Stats.MaxMana);
sensor.AddObservation(_manager.RollFlag);
sensor.AddObservation(_manager.IsInteracting);
sensor.AddObservation(_manager.throwFire);
sensor.AddObservation(_manager.posFire); // Vector3 type
// Collect enemy's state and add them to observations
sensor.AddObservation(_enemyManager.Stats.CurrentHealth / _enemyManager.Stats.MaxHealth);
sensor.AddObservation(_enemyManager.Stats.CurrentStamina / _enemyManager.Stats.MaxStanima);
sensor.AddObservation(_enemyManager.Stats.CurrentMana / _enemyManager.Stats.MaxMana);
sensor.AddObservation(_enemyManager.RollFlag);
sensor.AddObservation(_enemyManager.IsInteracting);
sensor.AddObservation(_enemyManager.throwFire);
sensor.AddObservation(_enemyManager.posFire); // Vector3 type
int isEnemyFacingMe = (Vector3.Dot(_manager.transform.localPosition - _enemyManager.transform.localPosition, _enemyManager.transform.forward)) > 0 ? 1 : 0;
sensor.AddObservation(isEnemyFacingMe);
}
目标和代理本身的生命值、法力和耐力是至关重要的信息。健康是赢得战斗的直接指标,因此必须提供。根据法力和耐力采取行动对于成功也很重要。动作事件允许代理知道目标是否即将攻击。我们还根据其方向提供目标是否正在查看代理。此信息允许代理在目标背后进行更有效的攻击。
将信息提供给代理的另一种方法是使用Raycast。您可以将它们视为检测物体视线的激光。这用于通过将 RayPerceptionSensor3D 组件添加到代理来检测墙壁和目标的位置。
输入 NN 模型的输入数据的数量是上述两种方式定义的输入的总和。C# 代码中的输入数为 57。这可以使用以下公式计算:(空间大小)(堆叠向量)。在这种情况下,Space Size 是 AddObservation 方法收集的观察数据的数量,而 Stacked Vectors 是一次馈送到 NN 模型的输入的帧数。Stacked Vectors 可以在 Unity 的 UI 中设置,如图 2 所示。您必须将参数与您在代码中定义的观察结果相匹配。光线投射的输入数量为 492。该数量可以使用以下公式计算:(堆叠光线投射)(1 + 2 * 每个方向的光线)*(可检测标签数量 + 2)。这些也可以在 Unity 的 UI 中设置。当然,光线和标签的数量越少,
图 2. 代理行为参数(左)和 Ray Perception Sensor 3D 组件(右)
2、代理的输出:完成目标任务需要采取哪些行动?
接下来是定义代理的可能输出。我们将代理的输出与角色的独特动作一对一地映射。在这个游戏演示中,Arm 博士和 Knight 可以采取的行动是相同的。
图 3. 角色动作(左:Dr Arm,右:Knight,下:可能的动作)
角色可以在水平和垂直两个轴上移动,每个轴取一个从 -1 到 1 的连续值。它还取四个独立的离散值作为一个动作。每个值都分配给一个动作:挥动剑的攻击,投掷火球的火,躲避攻击的滚动和无动作。这些可以如以下示例代码所示实现:
// Called every time the agent receives an action to take from Agent.OnActionReceived()
public void ActAgent(ActionBuffers actionBuffers)
{
// Joystick movement
var actionZ = Mathf.Clamp(actionBuffers.ContinuousActions[0], -1f, 1f);
var actionX = Mathf.Clamp(actionBuffers.ContinuousActions[1], -1f, 1f);
Vector2 moveVector = new Vector2(actionZ, actionX);
_inputController.Move(moveVector);
// Discrete actions
if (actionBuffers.DiscreteActions[0] == 1)
{
_inputController.Attack();
}
else if (actionBuffers.DiscreteActions[0] == 2)
{
_inputController.Roll();
}
else if (actionBuffers.DiscreteActions[0] == 3)
{
_inputController.Fire();
}
}
// Heuristic convers the controller inputs into actions.
// If the agent has a Model file, it will use the NN Model to take actions instead.
public override void Heuristic(in ActionBuffers actionsOut)
{
var continuousActionsOut = actionsOut.ContinuousActions;
var discreteActionsOut = actionsOut.DiscreteActions;
continuousActionsOut[0] = Input.GetAxis("Horizontal");
continuousActionsOut[1] = Input.GetAxis("Vertical");
if (Input.GetKey(KeyCode.Joystick1Button0))
{
discreteActionsOut[0] = 1;
}
else if (Input.GetKey(KeyCode.Joystick1Button2))
{
discreteActionsOut[0] = 2;
}
else if (Input.GetKey(KeyCode.Joystick1Button1))
{
discreteActionsOut[0] = 3;
}
else
{
// do nothing
discreteActionsOut[0] = 0;
}
}
如图 3 所述,有 2 个连续动作(水平和垂直移动)和 1 个离散动作。对于代理的输出,有 4 个可能的值。这些值的总和等于 NN 模型输出层中的节点数。同样,您必须将下面显示的 Unity UI 中的参数与您在代码中定义的操作数相匹配。
图 4. 行为参数应与输出配置匹配
3、NN模型结构:大脑应该如何处理信息?
接下来,考虑决策者的大脑。应该是哪种NN模型结构?是否需要历史信息?需要相机输入吗?
默认情况下,ML-Agents 使用多层感知器 (MLP) 结构。MLP 是神经网络的最基本结构,每个神经元的连接如图 5 所示。网络的输入层和输出层由上述设计第 1 节和第 2 节中定义的输入和输出决定。此外,ML-Agents 提供了几个参数来改变中间层的数量和大小等等。
图 5. MLP NN 模型结构
游戏开发者可以更改三个参数:
• 堆叠向量:一次输入到 NN 模型的输入数据的帧数
• 层数:NN 模型中的中间层数
• 隐藏单元:每层的神经元数
游戏开发者必须根据任务的复杂程度设置合适的参数值。在我们的演示中,NN 模型有 3 个堆叠向量、2 个中间层和每层 128 个神经元。NN 模型的大小并没有那么大,但这是强化学习中比较常见的大小。正如上面设计部分 1 中提到的,可以在 Unity 的 UI 中设置 Stacked Vectors。其他两个参数应在传递给训练命令的 YAML 脚本中指定。可以在此处找到有关网络设置的更多信息。
ML-Agents 将 NN 模型生成为ONNX格式。下面是生成的NN模型的结构。如您所见,定义的输入和输出反映在模型中。在右上角,已创建名为 action_masks 的输入。这可用于在某个时间点禁用特定操作。例如,当法力不足时,您可以显式屏蔽 FIRE 动作,但我们在演示中没有使用此功能。
图 6. 生成的 NN 模型
4、奖励功能:如何训练?
最后,考虑应该给予什么奖励。奖励在设定代理人的目标方面起着关键作用。在这个演示中,根据状态,给予代理三个主要奖励。
图 7. 奖励函数
在我们的案例中,第一个是当智能体实现其目标并击败目标时给予的巨大积极奖励。在这种情况下给予 +1 奖励。相反,当代理被目标击败时,会给予很大的负奖励。被击败是必须避免的事件,因此在这种情况下给予 -1 奖励。最后,每一步都会继续给予小的负奖励。这样可以激励代理尽快击败目标。此外,在训练期间,会在一段时间后定义超时。在我们的例子中是 2500 步。当超时发生时,这个累积的惩罚变为 -1。这意味着超时平局与被目标击败是相同的负面奖励。
原作者:三海幸树