深度强化学习课程文档
快速入门
并获得增强的文档体验
开始使用
入门:
要开始,请从这里下载项目(点击GDRL-IL-Project.zip
旁边的下载图标)。zip文件包含“Starter”和“Complete”项目。
游戏代码已在 starter 项目中实现,并且节点已配置。我们将重点关注:
- 实现 AIController 节点的代码,
- 记录专家演示,
- 训练代理并导出 .onnx 文件,我们可以在 Godot 中使用该文件进行推理。
在 Godot 中打开入门项目
解压 zip 文件,打开 Godot,点击“导入”,然后导航到解压后的存档中的Starter\Godot
文件夹。
打开机器人场景
这个场景包含几个不同的节点,包括robot
节点,它包含机器人的视觉形状,CameraXRotation
节点用于在人工控制模式下使用鼠标旋转相机“上下”。AI 代理不控制此节点,因为它对于学习任务不是必需的。RaycastSensors
节点包含两个 Raycast 传感器,可帮助代理“感知”游戏世界的某些部分,包括墙壁、地板等。

点击 AIController3D 旁边的卷轴以打开脚本进行编辑
将 get_obs() 和 get_reward() 方法替换为以下实现:
func get_obs() -> Dictionary:
var observations: Array[float] = []
for raycast_sensor in raycast_sensors:
observations.append_array(raycast_sensor.get_observation())
var level_size = 16.0
var chest_local = to_local(chest.global_position)
var chest_direction = chest_local.normalized()
var chest_distance = clampf(chest_local.length(), 0.0, level_size)
var lever_local = to_local(lever.global_position)
var lever_direction = lever_local.normalized()
var lever_distance = clampf(lever_local.length(), 0.0, level_size)
var key_local = to_local(key.global_position)
var key_direction = key_local.normalized()
var key_distance = clampf(key_local.length(), 0.0, level_size)
var raft_local = to_local(raft.global_position)
var raft_direction = raft_local.normalized()
var raft_distance = clampf(raft_local.length(), 0.0, level_size)
var player_speed = player.global_basis.inverse() * player.velocity.limit_length(5.0) / 5.0
(
observations
.append_array(
[
chest_direction.x,
chest_direction.y,
chest_direction.z,
chest_distance,
lever_direction.x,
lever_direction.y,
lever_direction.z,
lever_distance,
key_direction.x,
key_direction.y,
key_direction.z,
key_distance,
raft_direction.x,
raft_direction.y,
raft_direction.z,
raft_distance,
raft.movement_direction_multiplier,
float(player._is_lever_pulled),
float(player._is_chest_opened),
float(player._is_key_collected),
float(player.is_on_floor()),
player_speed.x,
player_speed.y,
player_speed.z,
]
)
)
return {"obs": observations}
func get_reward() -> float:
return reward
在get_obs()
中,我们首先从检查器中添加到AIController3D
节点的两个 Raycast 传感器获取观测值,并将它们添加到观测值中,然后获取到宝箱、杠杆、钥匙和木筏的相对位置向量,我们将其分为方向和距离,然后也将它们添加到观测值中。
我们还将其他游戏状态信息添加到观测值中
- 杠杆是否已被拉动,
- 钥匙是否已收集,
- 宝箱是否已打开,
- 玩家是否在地板上(也决定玩家是否可以跳跃),
- 玩家的归一化局部速度。
我们将布尔值(如_is_lever_pulled
)转换为浮点数(0 或 1)。
在get_reward()
中,我们只需返回当前奖励。
将 _physics_process() 和 reset() 方法替换为以下实现:
func _physics_process(delta: float) -> void:
# Reset on timeout, this is implemented in parent class to set needs_reset to true,
# we are re-implementing here to call player.game_over() that handles the game reset.
n_steps += 1
if n_steps > reset_after:
player.game_over()
# In training or onnx inference modes, this method will be called by sync node with actions provided,
# For expert demo recording mode, it will be called without any actions (as we set the actions based on human input),
# For human control mode the method will not be called, so we call it here without any actions provided.
if control_mode == ControlModes.HUMAN:
set_action()
# Reset the game faster if the lever is not pulled.
steps_without_lever_pulled += 1
if steps_without_lever_pulled > 200 and (not player._is_lever_pulled):
player.game_over()
func reset():
super.reset()
steps_without_lever_pulled = 0
将 get_action_space()、get_action() 和 set_action() 方法替换为以下实现:
# Defines the actions for the AI agent ("size": 2 means 2 floats for this action)
func get_action_space() -> Dictionary:
return {
"movement": {"size": 2, "action_type": "continuous"},
"rotation": {"size": 1, "action_type": "continuous"},
"jump": {"size": 1, "action_type": "continuous"},
"use_action": {"size": 1, "action_type": "continuous"}
}
# We return the action values in the same order as defined in get_action_space() (important), but all in one array
# For actions of size 1, we return 1 float in the array, for size 2, 2 floats in the array, etc.
# set_action is called just before get_action by the sync node, so we can read the newly set values
func get_action():
return [
# "movement" action values
player.requested_movement.x,
player.requested_movement.y,
# "rotation" action value
player.requested_rotation.x,
# "jump" action value (-1 if not requested, 1 if requested)
-1.0 + 2.0 * float(player.jump_requested),
# "use_action" action value (-1 if not requested, 1 if requested)
-1.0 + 2.0 * float(player.use_action_requested)
]
# Here we set human control and AI control actions to the robot
func set_action(action = null) -> void:
# If there's no action provided, it means that AI is not controlling the robot (human control),
if not action:
# Only rotate if the mouse has moved since the last set_action call
if previous_mouse_movement == mouse_movement:
mouse_movement = Vector2.ZERO
player.requested_movement = Input.get_vector(
"move_left", "move_right", "move_forward", "move_back"
)
player.requested_rotation = mouse_movement
var use_action = Input.is_action_pressed("requested_action")
var jump = Input.is_action_pressed("requested_jump")
player.use_action_requested = use_action
player.jump_requested = jump
previous_mouse_movement = mouse_movement
else:
# If there is action provided, we set the actions received from the AI agent
player.requested_movement = Vector2(action.movement[0], action.movement[1])
# The agent only rotates the robot along the Y axis, no need to rotate the camera along X axis
player.requested_rotation = Vector2(action.rotation[0], 0.0)
player.jump_requested = bool(action.jump[0] > 0)
player.use_action_requested = bool(action.use_action[0] > 0)
对于get_action()
(仅在使用演示记录模式时需要),我们需要提供当代理遇到相同状态时希望它发送的动作。重要的是,这些值必须在正确的范围内(-1.0 到 1.0
),这就是为什么布尔状态使用-1 + 2 * variable
,并且顺序正确,如get_action_space()
中定义的那样。
在演示记录模式下,调用set_action()
时不需要提供动作,因为我们需要根据人类输入设置动作值。在训练/推理模式下,调用此方法时会带有一个action
参数,其中包含由强化学习模型提供的所有动作的值,因此我们有一个if/else
来处理这两种情况。
更多信息包含在代码注释中。
将 _input 方法替换为以下实现:
# Record mouse movement for human and demo_record modes
# We don't directly rotate in input to allow for frame skipping (action_repeat setting) which
# will also be applied to the AI agent in training/inference modes.
func _input(event):
if not (heuristic == "human" or heuristic == "demo_record"):
return
if event is InputEventMouseMotion:
var movement_scale: float = 0.005
mouse_movement.y = clampf(event.relative.y * movement_scale, -1.0, 1.0)
mouse_movement.x = clampf(event.relative.x * movement_scale, -1.0, 1.0)
此代码部分在人工控制和演示记录模式下记录鼠标移动。
最后,保存脚本。我们已为下一步做好准备。
打开演示录制场景,然后点击 AIController3D 节点

您无需进行任何更改,因为所有内容都已预设好,但让我们回顾一下您需要在自己的环境中设置的内容
场景包含修改后的Level > Robot > AIController3D
节点设置
Control Mode
设置为Record Expert Demos
Expert Demo Save Path
已填写Action Repeat
设置为与training_scene
和onnx_inference_scene
中的Sync
节点相同的值。这意味着代理设置的每个动作都会重复 3 个物理帧。AIController
中的设置将相同的动作重复添加到人类输入(这会引入一些延迟)以匹配相同的行为。这是一个相当低的值,不会引入太多延迟。如果您更改此值,请务必在所有 3 个地方进行更改。Remove Last Episode
键允许我们设置一个键,该键可用于在录制过程中删除失败的剧集,而无需重新启动整个会话。例如,如果机器人掉入水中并且游戏重置,我们可以使用此键在录制下一个剧集时删除之前录制的剧集。它设置为R
,但您可以通过单击它,然后单击Configure
按钮将其更改为任何键。
在挑战性环境中使情节录制更容易的另一种方法是在录制期间减慢环境速度。这可以通过单击场景中的Sync
节点并调整Speed Up
属性(默认为 1)轻松完成。
让我们录制一些演示:
确保您仍处于demo_record_scene
中,按 F6
,演示录制将开始。
控制
- 鼠标控制摄像机(如果您需要调整鼠标灵敏度,打开
robot
场景,点击Robot
节点并调整Rotation Speed
,在录制演示、训练和推理时保持相同的值), WASD
控制玩家移动,SPACE
跳跃,E
激活杠杆并打开宝箱
您可以先练习几次以熟悉环境。如果您希望跳过录制演示,您也可以在完成的项目中找到预先录制的演示,并使用其中的expert_demos.json
文件。
录制的演示应至少包含 22-24 个完整的成功剧集。在训练阶段也可以使用多个演示文件,因此您不必一次性录制所有演示(您可以使用之前提到的Expert Demo Save Path
属性更改文件名)。
录制 23 个剧集花费了我大约 10 分钟(因为钥匙有 2 个交替的生成位置,22 或 24 个剧集将提供演示中钥匙位置的均匀分布,但这已经相当接近了)。当接近杠杆或宝箱时,我按住E
键的时间稍长一些,以确保在靠近这些物体时,该动作在多个步骤中被记录下来。我还通过在下一集期间按下R
键删除了几个我未成功完成的剧集。
这是演示录制过程的加速视频
导出游戏进行训练:
您可以使用Project > Export
从 Godot 导出游戏。