跳转至

第十四章:强化学习实战

本章导言

随着人工智能在游戏领域的不断深化,强化学习(Reinforcement Learning, RL)正逐渐成为开发智能游戏角色与自动化策略的强大工具。与传统规则驱动的游戏 AI 不同,强化学习强调“从经验中学习”。智能体通过与环境的不断交互,在试错中逐步掌握最优策略,最终实现接近甚至超越人类的决策水平。

本章将带你初步走入强化学习的世界。我们首先介绍强化学习的基本概念,包括智能体、环境、奖励机制与策略等核心组成部分,并通过“迷宫导航”等通俗示例帮助你建立直观理解。随后,我们将深入探讨强化学习在游戏开发中的多种应用方式,从对战 AI、自适应难度、NPC 行为设计,到程序化内容生成与虚拟角色训练,展示 RL 在提升游戏智能性和丰富交互体验方面的巨大潜力。

在实战部分,我们将结合前面开发过的《Flappy Bird》项目,使用流行的强化学习训练框架 Stable-Baselines3 和 Godot 插件 Godot RL Agents,构建一个完整的 AI 训练流程。你将亲手完成从环境改造、感知器设计、动作建模,到 Python 模型训练与调优的全过程,训练出一个真正会“飞”的智能小鸟。

1 强化学习基础

当我们训练小狗学会听从指令坐下时,会给它奖励以强化它遵从指令;当玩家控制游戏角色闯关成功时,会让玩家获得分数奖励,以强化这种成功的游戏操作。强化学习(Reinforcement Learning,简称 RL)正是模仿这种“试错-反馈-调整”的学习方式。在机器学习的三大支柱中(监督学习、无监督学习、强化学习),强化学习最贴近“如何决策”这一问题,它广泛应用于自动驾驶、金融决策、机器人控制以及我们本书所关注的“游戏 AI”。

本节将介绍强化学习的基本概念,帮助你理解这一强大工具背后的原理与结构。

1.1 强化学习的核心概念

强化学习的基本思想是:智能体(Agent)在环境(Environment)中尝试各种行为(Action),根据环境给予的奖励(Reward)来调整未来的行为策略(Policy),从而学会如何在不确定的环境中做出最优决策。核心组成要素如下:

概念 含义
智能体 Agent 学习的主体,负责决策和执行动作。比如游戏中的 AI 玩家、小鸟角色等。
环境 Environment 智能体所处的世界,会根据动作反馈状态和奖励。比如游戏场景、对手行为等。
状态 State 当前环境的观测信息,是智能体决策的依据。例如小鸟的位置、速度、障碍物位置。
动作 Action 智能体可以执行的行为。比如向上跳、移动、攻击等。
奖励 Reward 环境根据智能体动作给出的反馈,用来评价动作的好坏。
策略 Policy 智能体用来选择动作的行为准则,可通过学习不断优化。

强化学习的训练过程

强化学习的训练过程通常遵循以下步骤:

  1. 初始化策略:智能体一开始的初始策略通常是随机生成的。
  2. 试探性行动:智能体观察当前的状态,根据初始的策略去选择一个动作。
  3. 反馈和更新:环境根据智能体的动作进行反馈,反馈包括新的状态和奖励。
  4. 学习:智能体根据环境的反馈更新自己的策略,以使得将来做出更好的决策,拿到更高的奖励。
  5. 迭代: 重复上述过程,随着智能体在环境中进行更多的交互,直到策略趋于收敛(表现稳定)

这个过程中,最核心的是“试错”+“反馈”:不断尝试→获得奖励→更新策略。

示例:迷宫中的机器人

让我们通过一个通俗易懂的例子,机器人走迷宫,来完整理解强化学习的基本原理和工作流程。

一个机器人被放置在一个正方形迷宫的起点,它的目标是走出迷宫,找到终点出口。迷宫中包含障碍物、死胡同、正确路径等元素。机器人不知道地图结构,只能靠自己不断尝试来“学会”如何走出去。

alt text

整个过程可以用前面提到的强化学习的五要素来描述,以使我们更好的理解这些要素:

要素 在迷宫中的体现
智能体 Agent 机器人智能体,需要自己决定每一步该往哪走
环境Environment 整个迷宫地图,包括墙体、通道、终点等组成的环境
状态State 当前机器人的位置,例如在迷宫中的 (2, 3) 这个格子
动作Action 机器人可以执行的操作:向上、向下、向左、向右
奖励Reward 走向出口得 +10,碰墙或走进死路得 -1,普通移动得 0.1

这个例子很好地展现了强化学习的训练过程:

  1. 初始化策略:机器人起初不懂如何走,完全是随机选择方向,这就是“探索(Exploration)”。

  2. 执行动作:机器人选择一个方向(例如向上移动),这是它的当前“Action”。

  3. 环境反馈状态与奖励:如果碰到了墙,环境会返回“当前位置保持不变”和一个奖励 -1;如果走到新的位置,则可能得到 +0.1 或更高的奖励。这就是环境返回的“新状态和奖励”。

  4. 更新策略:机器人根据这次的反馈更新它对这个动作的认识。例如:“从 (2,3) 向上走不好”,于是它会倾向于下次换一个方向。这正是强化学习的核心算法在做的事,让策略(Policy)变得越来越好。

  5. 积累经验:经过数百次、上千次尝试,机器人逐渐摸索出迷宫的路线。在这一过程中,它不断进行探索(尝试新路径)与利用(走熟悉的好路线)之间的权衡。

  6. 学到最优策略:最终,机器人掌握了从任意位置快速走到出口的方法。这就是它学到的“最优策略”。

这个迷宫例子覆盖了强化学习的所有核心概念。同时,它也展示了强化学习中重要的两个挑战:

  • 如何设计奖励函数:如果奖励不合理,机器人可能学会“绕远路”或者“卡在某个得分点反复横跳”;
  • 如何平衡探索与利用:一味利用已有经验可能错失更优路径,一味探索则效率太低。

通过这个迷宫的例子,我们可以看到,强化学习不是依赖人工告诉智能体“该怎么做”,而是让它在不断试错中自己找到“怎么做才更好”。这也是它非常适合游戏 AI 的原因,游戏环境天然具备规则、目标和反馈,正好适合用来训练学习型智能体。

1.2 强化学习在游戏中的应用

电子游戏天然是强化学习的理想试验场。它们拥有明确的规则、有趣的目标、反馈及时的环境,因此非常契合强化学习的框架。随着深度学习和图形计算技术的发展,强化学习已从研究实验室走入了游戏开发实践,成为训练高智能 NPC、生成个性化关卡、构建动态难度系统等关键技术支撑。

对战 AI:从预设行为到自我进化

传统游戏 AI 主要依赖手工编写的规则或状态机,行为往往固定,难以适应复杂战局。而强化学习通过“试错”不断优化策略,使得 AI 能从零开始,自我学习如何打败对手。以下是一些经典案例:

  • AlphaGo(围棋):DeepMind 使用强化学习结合蒙特卡罗树搜索(MCTS),训练出超越人类冠军的围棋 AI。它不依赖人类棋谱,仅通过自我对弈学习最优策略。

  • OpenAI Five(Dota 2):OpenAI 使用数千个并行代理进行自博弈,OpenAI Five 最终达到了在多人战术 MOBA 游戏中与职业玩家一较高下的水准。

  • AlphaStar(星际争霸2):AlphaStar 使用强化学习训练智能体处理实时策略游戏中大量单位控制、资源管理、战术执行等复杂行为,实现战场级 AI 协同。

这些项目的核心并非“精通游戏规则”,而是通过百万次训练对局,学会如何应对复杂、动态的环境变化。

自适应难度与个性化体验

优秀的游戏体验,往往来自于“恰到好处的挑战感”。强化学习可以让 AI 实时评估玩家的行为和水平,从而调整自身策略或游戏参数,以适应玩家的技能水平,从而提供更好的游戏体验。具体的应用方式包括:

  • 根据玩家击败敌人所需时间、死亡次数、命中率等指标,自动调整敌人攻击频率或技能复杂度。
  • 利用强化学习模型实时学习玩家偏好,对游戏中的事件密度或资源投放进行微调,使其既不过度干扰也不失张力。

这种机制不仅提升玩家沉浸感,也极大延长了游戏寿命,是现代大型游戏中 AI 系统的常见设计之一。

更智能的NPC行为

在开放世界游戏或角色扮演游戏中,NPC 的智能程度直接影响世界的真实感。传统做法多为脚本控制或行为树,而强化学习提供了一种更具自主性的替代方案。具体的应用方式包括:

  • 动态行为学习:NPC 学会如何根据玩家行为调整态度,例如对玩家敌意增减做出战斗或逃跑反应。
  • 环境适应:不同地形、武器、敌人数量对 AI 行为产生影响,强化学习智能体能适应这些多样条件做出最优选择。

NPC 不再是“被设计出来”的演员,而是“在世界中成长”的智能体。

程序化内容生成(PCG)

程序化内容生成旨在通过算法自动创建地图、任务、敌人配置等游戏元素,而强化学习为此带来了目标驱动的内容设计方式。具体的应用方式包括:

  • 自适应关卡生成:AI 根据玩家过往表现,生成既能挑战玩家、又不会劝退玩家的定制化关卡。
  • 内容质量优化:RL 模型在关卡设计空间中寻找“最优可玩性”,平衡难度曲线、节奏变化与奖励反馈。

与随机生成相比,强化学习生成的内容更具有目标性与可调节性。

多智能体协作与竞技

许多竞技类或合作类游戏涉及多个智能体的协调与对抗。强化学习特别适合处理这种博弈式环境,通过多智能体训练提升整体战术水平。具体的应用方式包括:

  • 团队射击游戏:AI 学会队形协作、技能互补和资源共享。
  • 足球类游戏:训练智能体实现传球、拦截、防守等连续战术。
  • MOBA 游戏:多个 AI 英雄形成战术组合,进行局部配合或分线推进。

强化学习可以用于训练个体智能体,也可以形成一种“群体行为模型”,极大丰富 AI 在多人游戏中的表现层次。

虚拟角色训练与游戏测试

在飞行模拟、机器人操作、赛车、平台跳跃等任务中,强化学习能用来训练虚拟角色进行精准控制,替代传统动画驱动方案。此外,训练好的 AI 还能作为自动化测试代理,用于发现游戏漏洞、评估平衡性等。。具体的应用方式包括:

  • 飞行训练 AI:掌握复杂姿态控制、紧急降落、路径规划;
  • 跳跃精度 AI:学习在不同地形条件下执行完美跳跃,用于测试关卡设计是否合理;
  • 全自动通关代理:用于长时间游戏运行测试、性能评估等。

强化学习让游戏 AI 不再只是“编好的动画流程”,而成为能在环境中学习与成长的“决策系统”。它能提升游戏深度、优化用户体验、节省大量开发调优时间。未来,随着硬件与算法的持续演进,强化学习将在游戏世界中发挥越来越广泛的作用。

2 强化学习训练框架概览

要在游戏中使用强化学习,我们不仅需要构建好可交互的游戏环境,还需要一个能够训练智能体(Agent)进行学习的工具框架。在 Python 生态中,最常用、最成熟的强化学习训练库之一就是:Stable-Baselines3(简称 SB3)。本节将介绍该框架的核心功能、主流算法和使用方式。

2.1 Stable-Baselines3及其特性

Stable-Baselines3 是一个基于 PyTorch 的强化学习库,专注于高性能、可扩展、易上手的训练流程。它是早期 Baselines 项目的现代重构版,由研究人员与开源社区共同维护,并广泛应用于学术研究和工程实践中。其核心目标是:让开发者能够专注于“训练什么”和“怎么玩”,而不是“怎么实现底层算法”。其主要特性包括:

  1. 基于 PyTorch 实现:Stable-Baselines3 使用 PyTorch 作为底层深度学习框架,使其具有更高的灵活性和计算性能,易于调试、支持GPU加速,而且生态丰富。

  2. 支持多种强化学习算法:它提供了多个开箱即用的强化学习算法,包括 PPO、DQN、A2C、TD3、SAC 等,涵盖大部分主流场景。

  3. 高易用性 API:框架的设计注重易用性,提供简洁的 API 来训练、评估和保存模型。用户可以用少量代码就能训练 RL 模型,并通过简单的接口进行调参和模型评估。

  4. 自动化训练过程:提供自动化的训练循环,用户无需编写复杂的代码即可快速开始训练任务。框架会处理诸如探索与开发之间的平衡、损失计算、更新策略等常见任务。

  5. 训练环境集成:框架支持与 OpenAI Gym 兼容的环境,可以直接与多个预定义的 RL 环境(如 Atari 游戏、MuJoCo、PyBullet 等)进行交互。

  6. 模型保存和加载:用户可以轻松地保存和加载训练好的模型,方便进行后续的推理、微调或部署。

2.2 快速入门示例

我们以经典的 CartPole 平衡杆游戏为例,带你完整体验使用 Stable-Baselines3 的强化学习工作流程。以下代码均需在Python环境中编写和运行。

第一步:安装依赖

请确保你已安装 Python 3.7 以上版本。然后在终端中执行以下命令安装必要的库:

pip install stable-baselines3[extra]
extra 包括常用依赖,如 matplotlib、gym、tensorboard 等。如遇安装缓慢,可使用国内镜像。

第二步:创建并训练模型

import gym
from stable_baselines3 import PPO

# 创建环境
env = gym.make('CartPole-v1')

# 初始化 PPO 模型
model = PPO('MlpPolicy', env, verbose=1)

# 训练模型
model.learn(total_timesteps=10000)

第三步:保存与加载模型

# 保存模型到本地文件夹
model.save("ppo_cartpole")

# 重新加载模型(可在新脚本中使用)
model = PPO.load("ppo_cartpole")
保存的模型是 .zip 文件,包含网络结构与参数,可部署到其他设备使用。

第四步:使用模型进行推理

# 使用模型进行推理
obs = env.reset()
for i in range(1000):
    action, _states = model.predict(obs)
    obs, rewards, dones, info = env.step(action)
    env.render()
    if dones:
        break
上述示例从环境创建到训练、保存、推理,全流程不超过十几行代码。

2.3 与游戏引擎集成的方式

Stable-Baselines3(SB3)可以非常方便地与Godot引擎集成使用,从而构建完整的强化学习训练系统。在这种系统中,游戏引擎和训练框架各自扮演不同角色,并通过网络接口协同工作。

其基本工作模式如下:

  • 游戏引擎:构建交互式环境。游戏引擎充当“训练场”,提供图形界面、物理系统和可交互的游戏场景。它以“环境服务器”的形式运行,负责收集智能体的状态信息(如位置、速度、感知输入等),并通过网络或本地 socket 发送给 Python 端。

  • SB3(Python):训练智能体策略。Python 端接收到来自游戏引擎的状态后,利用 SB3 中的强化学习算法计算出下一步的最优动作,然后将这个动作结果回传给游戏,引导智能体继续行动,并继续收集新的反馈。

这种模式实现了训练逻辑与游戏逻辑的解耦:游戏开发者可以专注于环境构建与游戏机制的实现;AI 开发者可以专注于训练算法和模型优化,而无需深入游戏引擎内部;两者通过统一的通信接口对接,保持模块独立但协同高效。这种架构使强化学习在游戏开发中的应用更加灵活高效,适合在实际项目中迭代训练、调试和部署。

3 Godot中的插件系统

Godot 引擎不仅支持场景编辑、动画制作和编程,还提供了强大的插件系统,使开发者可以根据自己的需求扩展编辑器功能,构建自定义工具,提高工作效率。在本章的强化学习实战中,我们也会用到一个专门的 RL 插件。因此,在进入训练之前,我们需要了解 Godot 的插件机制及其基本使用方式。

3.1 什么是插件?

Godot 插件是一组可选加载的脚本与资源,它们通常用于:

  • 扩展 Godot 编辑器的功能(如自定义工具面板)
  • 添加新的节点类型或脚本模板
  • 集成外部工作流(如 AI、数据可视化、关卡工具等)

狭义的插件是指专门在编辑器中使用的插件,更广义的插件也包括在游戏运行时使用的插件。二者均以文件夹形式组织,可以在Godot中通过GDScript和标准的场景文件进行开发。编辑器插件的特点是,其代码脚本通常放在项目的 res://addons/ 目录下,并通过配置文件 plugin.cfg 描述其元信息。

安装插件

Godot 插件通常有两种安装方式:

  • 从 AssetLib 下载:打开 Godot 编辑器,点击顶部菜单栏的 AssetLib。搜索你需要的插件(例如:GridMap、Dialogue Manager、RL Agents)。点击 Download 并选择导入项目目录。
  • 从 GitHub 或本地安装:从插件作者处下载插件文件(通常是一个包含脚本和配置文件的文件夹)。将插件文件夹复制到项目中的 res://addons/ 目录。若无 addons/ 文件夹,可以手动创建。

使用插件

  • 在 Godot 顶部菜单中点击 Project > Project Settings。切换到 Plugins 标签页
  • 找到你安装的插件,点击右侧按钮启用(Enable)
  • 启用成功后,插件可能会在编辑器中添加新的菜单项、工具栏按钮或自定义节点类型,具体取决于插件的功能

3.2 理解 Godot 插件的生命周期

Godot中插件(Plugin)运行在编辑器上下文中,不同于普通游戏节点。插件的生命周期由 _enter_tree()_exit_tree() 控制,理解并正确使用这两个函数,是编写稳定、高效的智能插件的关键。

插件不是游戏节点

我们平常在游戏开发中经常使用 Node节点 ,并在其中重写常见函数,如:

  • _ready():当节点加入场景树,并且所有子节点准备就绪后调用;
  • _process():每帧执行一次,用于逻辑更新;

但插件并不运行在游戏场景中,它运行在 Godot 编辑器本身*的上下文中,是一种“编辑器扩展逻辑”,而非场景逻辑。因此,它的生命周期与游戏运行完全不同。

插件的激活与退出

在 Godot 中创建插件时,我们需要继承 EditorPlugin 类,并使用 @tool关键字使其在编辑器中生效。插件的生命周期由两个关键函数控制:

  • _enter_tree():当插件被启用(也就是用户在“插件”界面勾选它)时调用。此时你应该在这里注册 UI 控件、添加功能按钮、构建对话界面等。

  • _exit_tree():当插件被禁用或 Godot 编辑器卸载插件实例时调用。你应在此处释放资源、移除界面控件、断开信号,避免内存泄漏和界面残留。

理解插件的生命周期不仅能让你避免逻辑错误和编辑器崩溃,还能让你构建出更清晰、可维护的插件系统。

3.3 创建自己的插件

除了使用第三方插件,Godot 也允许你创建自己的插件,来扩展编辑器功能、自动化任务、或打造专属工具面板。这一功能特别适合中高级用户,也非常适合希望将游戏开发流程“工具化”的独立开发者。本节将引导你通过使用 @toolEditorPlugin,从零开始创建一个简单的自定义插件。

使用 @tool

Godot 中的脚本默认只在“游戏运行”时生效。但对于插件,我们希望它能在“编辑器状态下运行”,这就需要使用 GDScript 中的 @tool 修饰符。其如能如下:

  • 让脚本在编辑器中就可以运行 _ready()_process() 等函数
  • 支持可视化、自动布局、脚本生成等行为
  • 可用于自定义节点或编辑器扩展

如下是一个简单示例,在Godot中新建一个场景,根节点为Sprite2D,然后在脚本中添加如下代码:

@tool
extends Sprite2D

func _process(delta: float) -> void:
    if Engine.is_editor_hint():
        rotation_degrees += 1
    else:
        rotation_degrees -= 1
代码解释:

  • 使用 @tool 修饰符标识脚本为工具脚本
  • 使用 Engine.is_editor_hint() 判断是否在编辑器中,如果是,将图片旋转角度+1,否则将图片旋转角度-1

上面的例子中,如果运行游戏场景,你会看到图片逆时钟旋转,而在编辑器中,图片顺时针旋转。你可能需要重新打开项目,才能看到在编辑器中的旋转效果。

开发插件流程

我们会通过一个简单的示例,来解释插件开发流程。这个示例插件会在 Godot 编辑器的右侧属性栏底部添加一个按钮,点击后将在输出面板中显示当前系统的日期和时间。开发插件的步骤如下:

  • 步骤一:创建插件目录。在你的项目中手动创建如下路径:res://addons/myplugin,作为插件的目录。
  • 步骤二:编写配置文件:在myplugin目录中点右键,创建一个新的TextFile,命名为 plugin.cfg ,通过配置文件Godot引擎可以识别插件。配置文件中写入如下内容:

[plugin]
name="MyPlugin"
description="show current time"
author="Your Name"
version="1.0"
script="my_plugin.gd"
这告诉 Godot 插件的基本信息和主脚本路径。

  • 步骤三:创建插件主脚本:在目录中新建一个脚本文件my_plugin.gd,编写插件的脚本,代码如下:

    @tool   
    extends EditorPlugin
    var button = Button.new()
    
    func _enter_tree():
        button.text = "显示当前时间"
        button.pressed.connect(_on_button_pressed)
        add_control_to_container(CONTAINER_INSPECTOR_BOTTOM, button)
    
    func _exit_tree():
        remove_control_from_container(CONTAINER_INSPECTOR_BOTTOM, button)
        button.queue_free()
    
    func _on_button_pressed():
        var now = Time.get_datetime_string_from_system(false,true)
        print("当前时间:", now)
    
    代码解释:

    • 使用 @tool 修饰符标识脚本为工具脚本,即在编辑器里运行
    • 继承 EditorPlugin 类,表示这个脚本是一个“插件类”
    • 使用 Button.new() 创建一个新的按钮
    • 重写 _enter_tree() 函数,它会在插件加载时执行
    • 使用 pressed.connect() 方法绑定按钮的点击事件
    • 使用 add_control_to_container() 方法将按钮添加到工具栏窗口底部,此函数是专门为编辑器插件设计,底层会自动把按钮添加到编辑器UI的节点树中,所以不需要使用 add_child()
    • 重写 _exit_tree() 函数,它会在插件卸载时执行,函数中移除按钮并释放资源
    • _on_button_pressed() 函数响应按钮的点击事件,函数中调用Time.get_datetime_string_from_system() 获取当前时间,并在控制台输出当前时间。
  • 步骤四:启用插件,打开 Project > Project Settings > Plugins。找到 “MyPlugin”,点击右侧的“Enable”启用插件

此时你将在编辑器右侧属性栏底部看到一个新的按钮,点击后将在输出面板中显示当前系统的日期和时间。可能需要重启 Godot 以使插件生效。

你也可以另外创建一个button的场景文件,然后在代码中加载场景并进行实例化,再将按钮添加到工具栏窗口。对于非常简单的插件,我们就直接使用button.new()方法。对于复杂布局的插件,可以使用场景的方式。

使用插件可以大大提高你的开发效率,无论是增强Godot编辑器的功能,还是为你的游戏添加新的功能。插件的管理和调试也很方便,能够帮助你更好地组织和扩展你的项目。

4 Godot中的强化学习插件

为了构建一个可以“边玩边学”的智能体训练环境,需要将 Godot 场景与 Python 训练框架连接起来,我们将使用一个专门的插件:Godot RL Agents。它旨在将强化学习(RL)技术与 Godot 游戏引擎相结合,使游戏开发者、AI 研究人员和爱好者能够为非玩家角色(NPC)或代理创建复杂的行为模式。

4.1 插件的作用

Godot RL Agents 插件可以让你的游戏“变成训练场”,极大简化了游戏与 AI 系统之间的连接流程,适合教学实验和原型开发。它提供以下能力:

  • 将游戏中的状态(如位置、速度、传感器数据)发送给 Python 端;
  • 接收 AI 模型输出的动作指令,控制游戏角色行为;
  • 同步奖励信息和游戏回合的完成状态;
  • 提供用于感知环境的辅助节点(如射线感知、区域检测);
  • 支持离线训练、人类控制、模型推理等不同控制模式。

可以把 Godot RL Agents 插件理解成“Godot 游戏 = 环境,Python = 大脑”。运行时,Godot 负责实时模拟游戏世界(角色位置、速度、奖励、是否失败等),并通过网络通信把这些数据序列化后发送给 Python 端的强化学习框架。Python 根据收到的“状态”用神经网络计算出一个“动作”(例如向左、跳跃、开火),再通过网络把动作发回 Godot,Godot 执行动作并推进一小步游戏时间。这个 “状态 → 动作 → 奖励 → 新状态” 的循环会在每一帧或固定步长中高速重复,从而在不阻塞引擎渲染和物理系统的前提下,让 Python 逐步训练出智能体策略。这样就实现了 引擎负责模拟,Python 负责学习 的松耦合协作,也正是 RL Agents 能把 Godot 和主流强化学习框架连接起来的核心原理。

4.2 插件的使用步骤

要使用 Godot RL Agents 插件,一般需要完成以下几个步骤:

  1. 安装 Python 依赖:在本地安装插件所需的 Python 包,确保可以与 Godot 通信和进行训练。

  2. 引入插件到项目中:将插件文件夹复制到项目的 addons/ 目录,并在 Godot 的插件设置中启用它。

  3. 在场景中添加训练节点:例如 Sync 节点和 AIController2D 节点。

  4. 设置观察值、奖励和动作接口:通过编写脚本或使用插件提供的感知节点,将游戏角色的行为转换为强化学习所需的数据结构。

  5. 运行训练流程:启动 Python 训练脚本,并运行 Godot 场景,与 AI 模型进行实时通信和训练。

Godot RL Agents 插件为 Godot 引擎提供了强化学习的接口桥梁,让游戏开发者可以不依赖复杂底层逻辑,快速构建可训练的智能体系统。接下来我们将使用这个插件,改造 Flappy Bird 游戏,让小鸟学会在环境中独立飞行与生存,完成一个完整的强化学习项目实战。

5 Flappy Bird 强化学习实战

在之前的章节内容中,我们学习了强化学习原理和插件使用方法,本节我们将通过一个完整的项目来动手实践,训练一个会“自己飞”的 Flappy Bird 小鸟。

这个项目改编自本书第 5 章的 Flappy Bird 游戏,我们将对其进行改造,使其具备作为强化学习训练环境的能力。通过与 Python 训练框架 Stable-Baselines3 联动,小鸟将在不断尝试中学会躲避障碍、积累得分。

5.1 项目功能和实现步骤:

我们要实现的效果如下:

  • 小鸟可以根据环境感知信息做出决策;
  • 游戏每轮失败后自动重启,无需人工操作;
  • 与 Python 强化学习模型进行通信,实时接收动作指令;
  • 根据得分和生存时间给予奖励,让 AI 学会更好地“活下去”。

本实战项目的总体工作流程步骤包括如下三部分:

  • 准备阶段:安装项目所需的 Python 包和 Godot 插件;
  • 在 Godot 中改造游戏逻辑
    • 简化游戏流程:自动开始、失败后自动重置;
    • 添加 AI 控制器节点,实现状态输入和动作响应;
    • 增加环境感知组件,如射线传感器、目标检测区域;
    • 设置奖励机制,如生存奖励、得分奖励、失败惩罚。
  • 在 Python 中编写强化学习训练代码
    • 使用 Stable-Baselines3 创建模型;
    • 连接 Godot 环境并收集交互数据;
    • 定义网络结构、学习率、训练步数等超参数;
    • 启动训练并可视化学习曲线,观察 AI 的进步。

5.2 项目准备阶段任务

在准备阶段中,我们需要完成如下的依赖包安装和 Godot 插件下载:

  • 步骤一:在本地安装好Python环境,并安装强化学习训练框架stable-baselines3。
  • 步骤二:安装godot-rl插件的Python依赖包,在你的计算机终端中使用如下命令安装:pip install godot-rl。
  • 步骤三:在godot中安装godot-rl插件,从github中下载godot_rl_agents_plugin。具体地址为“https://github.com/edbeeching/godot_rl_agents_plugin”,文件具体位置如图所示

alt text

需要将addons/godot_rl_agents这个目录制下载复制到你的项目中addons目录。

5.3 Godot游戏端改造

首先,要确认已经完成以下准备阶段的所有工作。并且,在 Project > Project Settings > Plugins 中启用 GodotRLAgents 插件。设置如下图所示:

alt text

游戏改造的核心目标有三项:

  • 简化控制流程:无需按键启动,游戏自动开始;失败后自动重启;
  • 接入 AI 控制节点:替代人工操作,接收模型指令控制小鸟;
  • 添加环境感知与奖励反馈:提供状态输入与奖励输出,便于 AI 学习。

下面我们将逐一进行游戏改造。

修改main场景

打开主场景文件main.tscn,在节点树中新增一个节点,选择Sync节点,此节点是插件提供的组件,会负责将godot的信息和python进行同步。其属性设置如下:

  • control mode设为human,便为初期调试,后续会将它设置为training。
  • action repeat设置为1。

action_repeat 指每个 AI 动作会在多少帧内重复执行不变。也就是说,当 AI 决定了一个动作(如“跳”或“不跳”),这个动作将保持固定并重复执行指定的帧数,然后才允许重新做决策。对于 帧率较高 或 动作影响较小 的游戏(如赛车、控制角度类),适当设置 action_repeat > 1 可以加快训练速度,减少模型振荡;对于 需要精细操作 的游戏(如 Flappy Bird、跳跃类),建议使用 action_repeat = 1,确保 AI 每帧都可以调整行为;如果观察到训练过程中的行为非常抖动或不稳定,也可以尝试调整此参数进行平滑。

打开main.gd文件,修改代码如下:

func _ready():
    GameManager.GameOver.connect(on_game_over)
    ceil.body_entered.connect(on_border_body_enter)
    floor.body_entered.connect(on_border_body_enter)
    new_pipes()
    timer.start()
代码解释:因为不需要手动启动游戏,在_ready函数中删除了GameStart信号的连接,同时将原来的游戏启动时的代码整合到_ready函数中。

func reset_game():
    bird.global_position = Vector2(160,300)
    var pipes = pipes_group.get_children()
    for pipe in pipes:
        pipe.queue_free()
    call_deferred("new_pipes")
    timer.start()
代码解释:修改reset_game函数,在游戏重启时,会重置小鸟的位置,清空PipesGroup节点下的子节点,然后会重新创建新的管道,同时启动定时器。此时游戏自动重新开始。

修改hud场景

为了简化 UI,只保留显示分数的功能,游戏中可以删除启动按钮等无用控件,仅保留 score_label,用于显示得分。最终的节点树如下图:

alt text

同时修改UI的代码,关联分数更新事件:

func _ready():
    update_score()
    GameManager.GameOver.connect(on_game_over)
    GameManager.UpdateScore.connect(update_score)

func update_score():
    score_label.text = "Score: %s" %str(GameManager.score)

func on_game_over():
    update_score()

修改bird场景

打开bird场景文件bird.tscn,在这个场景中需要增加两个功能,一个是环境感知节点,获取有关的状态数据。另一个是AI控制节点,用于定义AI接口函数。

增加环境感知功能

先新增一个Node2D节点,重命名为Sensors,我们会在这个节点下放置几个环境感知节点。

新建一个场景,根节点为RayCast2D节点,重命名为Sensor,我们之前在坦克游戏中用过这个节点来进行射线碰撞检测。我们会在小鸟上放置多个此类节点,用于不同方向的射线检测。

RayCast2D节点中设置Collide with属性,仅开启Areas,不需要开启Bodies,这是因为我们的管道障碍是Area2D节点类型,所以只需要检测Area节点。

给节点附加代码,定义射线方向与距离计算方式:

extends RayCast2D
class_name Sensor

enum Direction {Horizontal , Vertical, Degree45}

@export var direction: Direction
var max_distance :int
代码解释:

  • 定义了枚举值Direction,它有三种方向,分别是横向,纵向以及斜向45度
  • 然后定义了方向变量和最大距离变量

func _ready():
    if direction == Direction.Horizontal:
        max_distance = abs(target_position.x)
    if direction == Direction.Vertical:
        max_distance = abs(target_position.y)
    if direction ==Direction.Degree45:
        max_distance = abs(target_position.x) * sqrt(2)
代码解释:_ready函数中定义了计算max_distance的不同方式,需要根据射线的放置方向来进行不同的计算。

func get_distance():
    if is_colliding():
        var distance = global_position.distance_to(get_collision_point())
        return distance/max_distance
    else:
        return 1.0
代码解释:get_distance函数用于计算射线原点到碰撞点的距离,然后返回一个相对距离,这个距离在0到1之间。

回到bird场景,在Sensors节点下,放置六个Sensor场景,其中两个是横向放置,指向小鸟的飞行前方,原点分别在小鸟的上边缘和下边缘,将Direction属性设置为Horizontal,将target position的x设置为300,所以这两个射线会检测前方障碍的相对距离数值。

类似的,再放置两个纵向射线,分别指向上方和下方,Direction设置为Vertical,target Position的y属性分别设置为200和-200。

最后是两个斜向45度指向的射线,Direction设置为Degree45,Target Position设置为(200,200)和(200,-200)

这六个射线完成后所下图所示,它们用于感知小鸟周边的障碍。

alt text

此外,我们还需要让小鸟能够感知到金币所在的位置,这样它才可能学习到是向上飞行还是向下飞行。在Sensors节点下新增一个Area2D,重命名为SensorCoin,这个节点只用于检测飞行前方金币的位置,所以在Collision属性中将Mask设置为2,然后打开Pipes场景,将Coin的Layer也设置为2,这样射线不会去检测金币,而SensorCoin会检测到金币。

在SensorCoin节点下增加一个CollisionShape2D,设置为长方形,Size设置为(300,1500),设置完成后如下图所示:

alt text

还需要给SensorCoin附加代码如下:

func get_distance():
    if has_overlapping_areas():
        var others = get_overlapping_areas()
        if others.size()>0:
            var other = others[0]
            var distance = other.global_position.y- global_position.y
            return distance/800
    else:
        return 0
代码解释:

  • 实现一个同样名为get_distance的函数,用于检测金币的纵向坐标和自身纵向坐标之间的距离
  • 然后将屏幕尺寸作为分母计算出一个相对值。

点击Sensros节点,也为它附加代码如下:

func get_observation():
    var distances: Array
    for child in get_children():
        distances.append(child.get_distance())
    return distances
代码解释:get_observation函数负责计算距离,它会获取所有的子节点,并调用其子节点的get_distance函数计算相对距离,并把这个相对距离整合在一个数组中返回。

增加AI控制功能

第二个步骤是增加AI控制能力,打开bird场景新增AIController节点,这是插件自带的控制节点,在节点上点击右键,选择extend script,这样会基于AIController2D进行继承。继承后的代码如下:

extends AIController2D

var move_action :int = 0

func get_obs() -> Dictionary:
    var obs = _player.sensors.get_observation()
    var player_speed = _player.velocity.y/_player.max_speed
    obs.append(player_speed)
    return {"obs": obs}
代码解释:

  • 定义了move_action变量,这是一个整数型变量用于定义小鸟的动作,0表示不进行任何动作,1表示一次翅膀煽动的动作。
  • get_obs函数负责从环境中获取信息,它从射线感知器中调用get_observation获取信息,这是一个包含了16个元素的数组
  • 然后重新定义一个小鸟速度值,将速度值也加到数组中,作为状态信息一部分。
  • 所以最终AI可以处理的状态信息是17个,将这些信息封装成一个字典对象返回。

func get_reward() -> float: 
    return reward

func get_action_space() -> Dictionary:
    return {
        "move_action" : {
            "size": 2,
            "action_type": "discrete"
        },
    }

func set_action(action) -> void:    
    move_action = action["move_action"]
代码解释:

  • get_reward函数不作其它处理,直接输出reward,
  • get_action_space函数负责设置小鸟的动作空间,因为只有0和1两种选择,所以这里的size是2,动作类型是离散的。
  • set_action是设置动作的函数,将动作赋值给move_action变量。

然后打开bird.gd代码进行修改。

@onready var ai_controller = $AIController
@onready var sensors: Node2D = $Sensors


func _ready():
    GameManager.GameOver.connect(on_game_over)
    GameManager.UpdateScore.connect(on_get_score)
    ai_controller.init(self)
代码解释:

  • 定义了两个新节点引用变量,分别是之前的感知节点和AI控制节点
  • _ready函数中删除了游戏启动的代码,并加入到AIController的初始化函数。这样AIController就知道它需要控制哪一个节点。

func _physics_process(delta):

    if not is_dead:
        ai_controller.reward += 0.01
        velocity.y += GameManager.GRAVITY * delta

        if ai_controller.heuristic == "human":
            if Input.is_action_just_pressed("fly"):
                velocity.y = GameManager.JUMP_VELOCITY
                fly_sound.stream = WING
                fly_sound.play()
            rot_degree = clampf(-30 * velocity.y/GameManager.JUMP_VELOCITY, -30,30)
            animated_sprite_2d.rotation_degrees = rot_degree
        else:
            var action = ai_controller.move_action
            if action == 1:
                velocity.y = GameManager.JUMP_VELOCITY
                fly_sound.stream = WING
                fly_sound.play()
            rot_degree = clampf(-30 * velocity.y/GameManager.JUMP_VELOCITY, -30,30)
            animated_sprite_2d.rotation_degrees = rot_degree
        velocity.y = clampf(velocity.y, -max_speed, max_speed)
        move_and_slide()
代码解释:_physics_process函数负责控制小鸟的动作,如果是由人工来操作小鸟,它的逻辑和原来的游戏代码一样,如果是由AI来控制操作,它会获取ai_controller中的move_action变量,根据这个值来决定小鸟的动作。而且每一帧的操作中,如果小鸟还没有死亡,我们都会给ai_controllerreward变量增加0.01,这是给一个正向的奖励机制,让AI知道我们希望你活的越久越好。

func on_game_over():

    ai_controller.reward -= 5
    ai_controller.done = true
    ai_controller.reset()

func on_get_score():
    ai_controller.reward += 1
    score_sound.stream = POINT
    score_sound.play()
代码解释:

  • on_game_over函数负责处理游戏失败时的操作,此时reward变量会扣减5分,这是一个负向的惩罚机制,让AI不要去撞向障碍。
  • 然后done变量设置为true,表示这一轮游戏结束,重置信息。
  • on_get_score函数负责处理游戏得分时的操作,此时reward变量会增加1分,这是一个正向的奖励机制,让AI知道我们希望你得分越高越好。

你可以通过打印ai_controller的观察值了解AI的输入,进行调试。

到这里,所有的 Godot 部分代码修改就完成了,完成以上修改后,游戏无需人工点击即可自动开始与重启。传感器将环境状态编码为数组传给 AI,AI 可控制小鸟是否跳跃,影响生存和得分。奖励机制清晰,便于模型优化目标行为。

你可以先将 Sync 节点设为 human,手动测试运行;确认无误后,切换为 training 模式,即可配合后续的训练代码开启训练流程。

5.4 编写强化学习训练代码

完成 Godot 端改造后,我们就可以开始编写 Python 端的训练脚本,用于驱动 AI 学会玩 Flappy Bird。Python 部分的任务是:接收游戏状态 → 使用模型决策动作 → 将动作返回游戏 → 接收奖励并更新模型参数,形成一个完整的强化学习闭环。

强化学习逻辑回顾

整个训练流程可以概括为以下几个关键步骤:

  • 状态采集:Godot 中的小鸟每一帧会将环境信息(17 个感知数据)打包发给 Python。
  • 动作决策:Python 使用强化学习模型对当前状态进行前向推理,输出一个动作(如是否跳跃)。
  • 反馈交互:该动作在游戏中执行,影响小鸟的行为,同时根据表现给予奖励或惩罚。
  • 模型更新:每隔一定步数(如 128),模型使用最近一段的奖励信息对策略参数进行优化。

经过多次这样的循环,AI 会逐渐学会什么行为能活得更久、得分更高,从而掌握“飞”的技能

使用官方模板快速开始

你无需从零开始编写python的训练脚本,插件项目提供了完整的训练模板。你只需访问以下地址,下载并修改模板文件: https://github.com/edbeeching/godot_rl_agents/tree/main/examples 插件代码仓库中包括了主要训练框架的训练模板,如果你安装了SB3,可以找到名为stalbe_baselines3_examples.py的脚本,这就是训练的主程序。你可以在此基础上修改,也可以尝试其它训练框架。模板文件地址如下图所示: alt text

重要参数说明

在模板中,有几个重要参数可以根据项目需求进行调整:

  • 实验名称

    parser.add_argument(
        "--experiment_name",
        default="godot_flappy",
        type=str,
    )
    
    这里的experiment_name参数是用来设置实验名称,我们可以自定义实验名称,该名称用于保存模型、生成日志,并在 TensorBoard 中展示。

  • 模型保存路径

    parser.add_argument(
        "--save_model_path",
        default="model/ppo_model.zip",
        type=str,
    )
    
    save_model_path参数是用来设置模型保存路径,我们可以自定义模型保存路径,模型会自动保存到指定路径(.zip 格式),以便后续加载或部署。

  • 训练步数

    parser.add_argument(
        "--timesteps",
        default=1_000_000,
        type=int,
    )
    
    timesteps参数是用来设置训练总步数,默认设置为 100 万步。你也可以设置更少的步数(如 100000)进行快速实验。训练总步数表示和环境交互的总次数,越大的步数意味着越有可能学到复杂行为;但训练时间也会更长。可以配合保存模型中间结果,观察表现,再决定是否继续训练。

  • 设置模型网络结构

    policy_kwargs = dict(
        net_arch=[16]
    )
    
    if args.resume_model_path is None:
        learning_rate = 0.001 if not args.linear_lr_schedule else linear_schedule(0.0003)
        model: PPO = PPO(
            "MultiInputPolicy",
            env,
            ent_coef=0.0001,
            verbose=2,
            n_steps=128,
            tensorboard_log=args.experiment_dir,
            learning_rate=learning_rate,
            policy_kwargs=policy_kwargs,
        )
    
    代码中,还可以自定义神经网络的结构,此外在policy_kwargs中定义了一个字典,用于定义神经网格结构,这里只需要定义中间隐藏层的神经元个数为16,因为输入层和输出层是预先定义好的。整体来看,输入层是对应状态信息的17个神经元,输出层是对应动作的1个神经元。

神经网络越复杂(层数更多,神经元更多),表达能力越强,但也更难训练,容易过拟合。建议从小网络开始。如果模型很快陷入重复行为(如只会飞不动、或只会跳一下就死),可以适当 增加神经元数量;如果训练很慢甚至崩溃,考虑减少网络复杂度。

learning_rate参数是用来设置学习率,学习率用于控制模型参数每次更新的“步长”。学习率太大:训练震荡、不收敛;学习率太小:训练极慢、效果不明显。建议从 0.001 或 0.0003 起步;如果 loss 曲线抖动严重可以降低学习率;如果奖励提升太慢可以提高学习率或加快训练步数。

在我们的例子中使用的强化学习算法是PPO。PPO(Proximal Policy Optimization)是目前强化学习中应用最广、表现最稳的策略优化算法之一。其优势是稳定性强,相比早期策略梯度方法,PPO 更不容易“崩”;效率较高,可以用较少的数据完成较好的训练;而且PPO 对奖励设计不敏感,适合初学者快速上手调试。

启动强化学习训练

设置完成后,在终端运行以下命令启动训练脚本:

python stable_baseline_example.py
你将看到如下提示:
waiting for remote GODOT connection on port 11008
此时表示 Python 已准备好等待 Godot 连接。回到 Godot,运行项目。启动后,控制台中会显示:
Handshake complete
这说明 Godot 成功与 Python 建立连接,强化学习训练正式开始!训练过程中你会看到终端中的日志信息,包括训练步数、奖励等等,你也会在Godot中看到小鸟在由AI驱动飞行。如下图所示: alt text

在训练过程中,SB3 会将日志数据保存在 logs/ 文件夹中。你可以使用 TensorBoard 实时查看模型的表现指标:

tensorboard --logdir logs
打开浏览器访问 http://localhost:6006,即可查看如奖励曲线、损失值、策略熵等训练数据。如下图所示: alt text

通常在训练 1~2 万步之后,小鸟就能学会躲避第一个障碍。随着训练持续进行,它会逐渐掌握更稳健的飞行策略。

本章小结

在本章中,我们首次将人工智能真正“请进”了 Godot 游戏世界,完成了 Flappy Bird 游戏的强化学习训练过程。从游戏的改造到模型的训练,每一步都充满挑战,也充满启发。

以下是本章的主要收获:

  • 理解了强化学习的基本原理:包括状态、动作、奖励、策略优化等关键概念;
  • 掌握了 SB3 的基本使用方式:包括算法选择、网络结构、学习率设置等训练参数的含义;
  • 学会了与游戏引擎的集成方式:利用 Godot RL Agents 插件将游戏环境变为可训练场;
  • 完成了 Flappy Bird 游戏的 AI 改造:通过 Sensors 感知环境、AIController 接管行为,实现了自动学习的飞鸟;
  • 掌握了训练代码的使用与调优方法:包括模板下载、模型保存、TensorBoard 可视化等完整流程。
  • 学习了插件的使用与开发方法:不仅使用了 Godot RL Agents 插件进行强化学习,还通过 @tool 关键字学会了自定义编辑器插件的开发流程,为未来扩展打下基础。

通过这一章的实战,你不仅让游戏“能玩”,更让它“会学”。这是 AI 游戏开发的重要转折点。