跳至内容

构建球体射击游戏

本指南将引导您在 Unreal Engine 中创建一个简单的射击场环境,并训练一个 Agent 来射击移动目标。目标是在射击前正确瞄准,并逐个击破目标。

在此示例中,我们将创建一个动态射击场环境,其中包含一个 Agent,它将使用强化学习来学习射击移动目标。Agent 通过观察目标的移动并通过执行器执行旋转和射击操作来与环境进行交互。

我们将通过让 Agent 反复尝试击中移动目标来训练它。每次尝试称为一个回合,当 Agent 成功击中目标三次或用完时间时,回合结束。

Agent 将会定期回顾其在之前回合中的表现,并更新其策略以进一步提高。为了量化 Agent 的表现,我们定义了一个奖励函数:击中目标会获得奖励,未击中目标会受到惩罚,而每走一步未击中目标会受到小额惩罚。然后,Agent 可以使用学到的策略来决定在游戏过程中采取哪些操作。

Unreal Engine 中的环境结构

为了构建 Agent 将要学习射击移动目标的(以下称为环境的)游戏,我们需要在 Unreal Engine 项目中具备以下条件:

  • 地图:游戏地图包括地面、四个墙壁、Agent 和环境定义。

  • 球体蓝图:Agent 射击的弹丸,由 Agent 在执行射击操作时生成。

  • 目标蓝图:Agent 将要射击的对象,它会在地图中随机移动,并在被球体击中三次后被销毁。

  • Agent 蓝图Pawn 的子类,包含 Agent 的形状和外观。

  • 射击执行器:一个自定义的离散 Actuator,允许 Agent 射击球体。

  • 离散旋转执行器:一个自定义的离散 Actuator,允许 Agent 进行旋转。

  • 训练器蓝图BlueprintTrainer 的子类,包含计算训练的 rewardstatus 的逻辑,以及 Sensors Actuators

  • 环境定义BlueprintStaticScholaEnvironment 的子类,包含在不同训练回合之间 初始化重置 环境的逻辑。

  • 注册 Agent:将 Agent 连接到环境定义和训练器。

初始设置

  1. 创建一个具有所需名称和位置的新空白项目。

  2. 使用 /guides/setup_schola 指南将 Schola 插件安装到项目中。

  3. 转到 Edit → Project Settings,然后向下滚动找到 Schola。

  4. 对于 Gym Connector Class,选择 Python Gym Connector

创建地图

  1. 在地图中创建一个带有地面和四个墙壁的射击场。
  2. 对于墙壁,在 DetailsTags 中,添加一个新元素,并将其值设置为 wall。该标签用于 RayCastObserver 检测不同的对象。

创建球体

球体类是 Agent 射击的弹丸。球体由 Agent 在执行射击操作时生成,并在撞到墙壁或目标时被销毁。

  1. 创建一个父类为 Actor 的新蓝图类,并将其命名为 BallShooterBall

  2. 向蓝图中添加一个球体 Static Mesh Component,并可选地选择一个外观不错的材质。

    1. 启用 DetailsPhysicsSimulate Physics
    2. 启用 DetailsCollisionSimulation Generates Hit Events
    3. 启用 DetailsCollisionGenerate Overlap Events
    4. DetailsCollisionCollision Presets 设置为 Custom
    5. DetailsCollisionCollision PresetsCollision Enabled 设置为 Probe Only。这可以防止球体阻挡 Agent 的 Ray Cast Observer 视觉。
  3. 添加一个球体 Collision Component,使其比球体稍大。

  4. DefaultSceneRoot 缩放到 0.5x0.5x0.5。

创建目标

目标是 Agent 将要射击的对象。目标会在地图中随机移动,并在被球体击中三次后被销毁。Event Tick 将对目标应用一个随机力,使其在地图中移动。OnTakeAnyDamage_Event 将在被球体击中时触发,调整目标的生命值,并在生命值达到零时销毁目标。

  1. 创建一个父类为 Actor 的新蓝图类,并将其命名为 BallShooterTarget

  2. 向蓝图中添加一个球体 Static Mesh Component,并可选地选择一个外观不错的材质。

    1. 启用 DetailsPhysicsSimulate Physics
    2. 启用 DetailsCollisionSimulation Generates Hit Events
    3. 启用 DetailsCollisionGenerate Overlap Events
    4. DetailsCollisionCollision Presets 设置为 PhysicsActor
  3. 添加一个球体 Collision Component,使其比球体稍大。

  4. DefaultSceneRoot 缩放到 3x3x3。

  5. 添加一个新的布尔变量。将其命名为 isHit。它存储 Agent 在当前步是否被球体击中。

  6. 添加一个新的 Transform 变量。将其命名为 initialTransform。它存储回合开始时目标的初始变换。

  7. 添加一个新的整数变量。将其命名为 hitPoint,并将默认值设置为 3。它存储目标被球体击中的次数。当 hitPoint 达到零时,目标将被销毁。

  8. 添加一个新的浮点变量。将其命名为 forceMagnitude,并将默认值设置为 50。它存储每次 tick 应用于目标的随机力的幅度。

  9. 创建一个名为 teleportToRandomLocation 的新函数,如下图所示,并将 Make Vector 节点的随机范围设置为射击场的范围。此函数将目标传送到射击场内的随机位置。

  10. 将事件图设置为如下图所示。

    1. Event Begin Play 将保存目标的初始变换并绑定 OnTakeAnyDamage_Event 一次。
    2. OnTakeAnyDamage_Event 将在被球体击中时触发,调整目标的 hitPoint,并在 hitPoint 达到零时销毁目标。
    3. Event Tick 将对目标应用一个随机力,使其在射击场中移动。
  11. DetailsTags 中,添加一个新元素,并将其值设置为 target。该标签用于 RayCastObserver 检测不同的对象。

创建代理

  1. 创建一个父类为 Pawn 的新蓝图类,并将其命名为 BallShooterAgent
  2. 将任何你想要的 静态网格体 添加为代理的身体,并可选地选择好看的材质。
  3. 添加一个 箭头组件,并将其设置为代理的前进方向。将其命名为 Projectile Indicator
  4. 保存并关闭蓝图,然后在地图中心放置一个 BallShooterAgent

创建射击执行器

Schola 提供了多种内置的执行器类,例如 TeleportActuatorMovementInputActuator。但是,有些游戏可能需要自定义执行器。在本例中,我们将创建一个自定义的 BlueprintDiscreteActuatorDiscreteActuator 的子类)来发射球。该执行器有两个可能的动作:发射球或什么都不做。GetActionSpace 函数将返回动作空间,而 TakeAction 函数将执行动作。我们还将创建两个辅助函数 getInitialVelocity()getInitialLocation(),以获取生成球的初始速度和位置。

  1. 创建一个父类为 BlueprintDiscreteActuator 的新蓝图类,并将其命名为 BallShooterShootingActuator
  2. 添加一个新的浮点变量。将其命名为 ballSpawnSpeed,并将默认值设置为 2000。此变量存储球被发射时的速度。
  3. 添加一个新的 Rotator 变量。将其命名为 projectileSpawnDirection。此变量存储球将要生成的方向。调整值以确保球在正确的方向生成。
  4. 添加一个新的浮点变量。将其命名为 ballSpawnOffset。此变量存储球将要生成的相对于代理位置的偏移量。将其默认值设置为 200,并根据需要进行调整,以确保球在代理前面生成,而不是在代理内部生成。
  5. 添加一个新的整数变量。将其命名为 countOfBallsShot。它存储代理在当前时间步长中发射的球的数量。
  6. 添加一个新的 Actor 变量。将其命名为 Agent。此变量存储拥有执行器的代理。
  7. TakeAction 函数转换为一个 事件。这允许我们将 Ball Hit Event 绑定到生成的球。
  8. 按照下面的图示设置 getInitialVelocity()getInitialLocation()GetActionSpaceTakeAction 蓝图。

创建离散旋转执行器

虽然 Schola 中存在 RotationActuator 并且可以用来持续旋转代理,但我们将创建另一个自定义的 BlueprintDiscreteActuatorDiscreteActuator 的子类)来旋转代理。该执行器有三种可能的动作:左转、右转或不执行任何操作。

  1. 创建一个新的蓝图类,父类为 BlueprintDiscreteActuator,并将其命名为 BallShooterDiscreteRotationActuator
  2. 添加一个新的浮点型变量。将其命名为 rotationMagnitude,并将默认值设置为 2。这用于存储代理旋转时的旋转幅度。
  3. 按照下图所示设置 GetActionSpaceTakeAction 蓝图。

创建 Trainer

要在 Schola 中训练一个代理(agent),代理必须由一个 AbstractTrainer 控制,该类定义了 ComputeRewardComputeStatus 函数。在本教程中,我们将创建一个 BlueprintTrainerAbstractTrainer 的子类)。

  1. 创建一个新的蓝图类,父类为 BlueprintTrainer,并将其命名为 BallShooterTrainer
  2. 添加一个新的整数变量。将其命名为 maxNumberOfHitsPerEpisode。它存储了代理在每个回合(episode)中可以击中目标的次数上限,即目标的数量乘以每个目标的命中点数。此值由 环境定义 蓝图设置。
  3. 添加一个新的整数变量。将其命名为 numOfHitsThisEpisode。它存储了代理在当前回合中击中目标的次数。用于确定回合何时结束。
  4. 添加一个新的整数变量。将其命名为 numOfTargetHits。它存储了代理在当前步骤中击中目标的次数。
  5. 添加一个 Actuator 组件,并将 DetailsActuator ComponentActuator 设置为 BallShooterShootingActuator
  6. 将事件图(Event Graph)设置为如下所示。这将 On Ball Hit 事件绑定到代理的执行器(actuator)生成的任何球上,使训练器能够检测到代理是否击中或错过了目标。

附加执行器和观察器

示例 1 不同,执行器(actuators)和观察器(observers)将不会附加到代理蓝图。相反,它们将被附加到 Trainer 蓝图中。这种方法简化了变量的传递,因为 TrainerComputeRewardComputeStatus 逻辑依赖于 BallShooterDiscreteRotationActuator 中的变量。

附加射球器(Ball Shooter Shooting Actuator)

  1. 添加一个 Actuator 组件。
  2. DetailsActuator ComponentActuator 中,选择 BallShooterDiscreteRotationActuator

附加球射手离散旋转执行器(Ball Shooter Discrete Rotation Actuator)

  1. 添加一个 Actuator 组件。
  2. DetailsActuator ComponentActuator 中,选择 BallShooterDiscreteRotationActuator

附加射线投射观察器(Ray Cast Observer)

  1. 添加一个Sensor 组件。
  2. DetailsSensorObserver 中,选择Ray Cast Observer
  3. DetailsSensorObserverSensor propertiesNumRays 中,输入 10。
  4. DetailsSensorObserverSensor propertiesRayDegrees 中,输入 120。
  5. DetailsSensorObserver``→ ``Sensor properties 中,勾选 DrawDebugLines 复选框。
  6. DetailsSensorObserver``→ ``Sensor propertiesTrackedTags 中,添加一个新元素,并将标签设置为 target

定义奖励函数

在本教程中,击中目标奖励 1 分,错过目标惩罚 -0.01 分。此外,代理每走一步会受到 -0.05 分的惩罚,以鼓励代理销毁所有目标并尽快结束回合。每一步的奖励计算如下:(1.01*numOfTargetHits - 0.01*countOfBallsShot) - 0.05

  1. 添加一个新的浮点型变量。将其命名为 reward。它用于存储当前步骤的奖励。
  2. 按照下图设置 ComputeReward 函数。

定义状态函数

每个时间步有三种可能的状态:

  1. 运行中:回合仍在进行中,代理会继续与环境互动。
  2. 已完成:代理已成功到达终止状态,回合完成。
  3. 已截断:回合被强制结束,通常是由于时间步限制或手动干预等外部因素,而未达到终止状态。

在本教程中,当代理销毁所有目标时,回合将达到终止状态,此时 numOfTargetHits 等于 maxNumberOfHitsPerEpisode。我们还设置了最大步数,以防止回合无限期运行。

  1. 添加一个新的整数变量。将其命名为 maxStep,并将默认值设置为 1000。这意味着如果回合在达到 1000 个时间步之前未完成,则会被截断。您可以根据环境大小或代理速度等因素调整此数字,以允许更长或更短的回合。
  2. 如下所示设置 ComputeStatus

创建环境定义

要在 Schola 中训练代理,游戏必须有一个 StaticScholaEnvironment Unreal 对象,其中包含代理以及初始化或重置游戏环境的逻辑。在本教程中,我们将创建一个 Blueprint EnvironmentStaticScholaEnvironment 的子类)作为环境。 InitializeEnvironment 函数在游戏开始时调用,并设置环境的初始状态。在本教程中,我们保存代理的初始变换(位置和旋转)。 ResetEnvironment 函数在每个新回合开始前调用。在本教程中,我们将代理重置到其初始变换,清理任何剩余的球和目标,生成三个新目标,计算该回合的 TotalHitPoints,并重置训练器中的变量。

  1. 创建一个父类为 BlueprintStaticScholaEnvironment 的新蓝图类,并将其命名为 BallShooterEnvironment

  2. 添加一个名为 agentArray 的新变量,类型为 Pawn (Object Reference) 数组。它用于跟踪属于此环境定义的已注册代理。

    1. 使此变量可公开编辑(通过单击眼睛图标切换可见性)。
  3. 添加一个名为 agentInitialLocation 的新 Transform 变量。用于存储代理的初始位置和旋转,以便在重置时恢复。

  4. 添加一个名为 numberOfTargets 的新整数变量,并将默认值设置为 3。这存储了要在环境中生成的对象的数量。

  5. 添加一个名为 totalHitPoints 的新整数变量。这存储了该回合的总命中点数,即目标数量乘以每个目标的命中点数。

  6. 添加一个名为 Targets 的新变量,类型为 Ball Shooter Target (Object Reference) 数组。它存储环境中生成的对象。

  7. 创建 saveAgentInitialTransformplaceAgentToInitialTransform 函数,如下所示。这会在回合开始时保存代理的初始变换并将代理放置到其初始变换。

  8. 如下所示设置 Event Graph 和 RegisterAgents 函数。

  9. 保存并关闭蓝图,然后在地图中的任何位置放置一个 BallShooterEnvironment。位置无关紧要。

注册 Agent

  1. 在地图中点击 BallShooterEnvironment

    1. 转到 Details 面板 → DefaultAgent Array
    2. 添加一个新元素。
    3. 在下拉菜单中选择 BallShooterAgent
  2. 在蓝图编辑器中打开 BallShooterAgent 类。

    1. 转到 Details Panel。
    2. 搜索 AIController
    3. 在下拉菜单中,选择 BallShooterTrainer

开始训练

我们将使用 Proximal Policy Optimization (PPO) 算法训练智能体 100,000 步。以下两种方法运行相同的训练。从终端运行可能更便于进行超参数调优,而从 Unreal Editor 运行可能在编辑游戏时更方便。

  1. 在 Unreal Engine 中运行游戏(点击绿色三角形)。
  2. 打开一个终端或命令提示符,然后运行以下 Python 脚本:
终端窗口
schola-sb3 -p 8000 -t 100000 PPO

启用 TensorBoard

TensorBoard 是 TensorFlow 提供的一种可视化工具,可让您在训练过程中跟踪和可视化损失和奖励等指标。

向命令添加 --enable-tensorboard 标志以启用 TensorBoard。 --log-dir 标志设置保存日志的目录。

终端窗口
schola-sb3 -p 8000 -t 100000 --enable-tensorboard --log-dir experiment_ball_shooter PPO

使用 schola-rllib 运行 RLlib 默认已启用 TensorBoard。

训练完成后,您可以通过在终端或命令提示符中运行以下命令来查看 TensorBoard 中的训练进度。请务必首先 安装 TensorBoard,并将 --logdir 设置为保存日志的目录。

终端窗口
tensorboard --logdir experiment_ball_shooter/PPO_1

© . This site is unofficial and not affiliated with AMD.