行为树(BehaviorTree.CPP)
行为树(BehaviorTree.CPP)
对应 BehaviorTree.CPP 3.8 官方文档:About。
行为树(Behavior Tree,简称 BT)是一种组织机器人任务及其切换的方式。 BehaviorTree.CPP 则是用于实现行为树的 C++ 库; 本教程的程序使用 3.8.7 验证,概念资料按官方 3.8 系列文档学习,官方教程入口在这里。
已经学过 FSM 后,可以从一个问题开始理解它:当导航、充电、任务取消等需求不断增加时,怎样把决策逻辑组织得清楚,并复用已有能力?
本篇先说明行为树解决什么问题,以及它与 FSM 的关系。节点状态、tick 传播和四类节点将在下一篇“基本概念”集中介绍。
从一个简单的 FSM 开始
假设移动机器人只有等待目标、执行导航、到达目标三个阶段:
IDLE → 收到目标 → NAVIGATING → 导航成功 → ARRIVED
└────── 导航失败 → IDLE
用 C++ 风格伪代码表示:
enum class RobotState
{
IDLE,
NAVIGATING,
ARRIVED
};
RobotState current_state = RobotState::IDLE;
void Update()
{
switch (current_state)
{
case RobotState::IDLE:
if (HasNavigationGoal())
{
StartNavigation();
current_state = RobotState::NAVIGATING;
}
break;
case RobotState::NAVIGATING:
if (NavigationSucceeded())
{
StopRobot();
current_state = RobotState::ARRIVED;
}
else if (NavigationFailed())
{
current_state = RobotState::IDLE;
}
// 尚未结束时保留当前状态,下次 Update() 再检查
break;
case RobotState::ARRIVED:
// 到达后的处理按具体任务补充
break;
}
}
外部周期性调用 Update()。启动导航的接口快速返回,机器人移动期间,每次更新只检查当前结果,不在函数里一直等待。
这种任务用 FSM 很容易描述:状态明确,转换条件也少。
复杂任务需要统一组织决策逻辑
任务增加后,同一条规则可能影响多个状态:
| 新需求 | 可能影响的任务 | 需要决定什么 |
|---|---|---|
| 电量不足 | 等待、导航、执行任务 | 是否暂停当前任务,前往充电 |
| 紧急停止请求 | 导航、充电、执行任务 | 是否中止当前操作并停止 |
| 定位丢失 | 导航、返回充电点 | 是否暂停移动、尝试恢复定位 |
| 收到新目标或取消请求 | 导航 | 是否取消旧请求、切换任务 |
| 导航失败 | 普通任务、返回充电点 | 是否重试、放弃或采用其他处理 |
例如,低电量发生在导航期间,可能需要先取消导航,再前往充电;发生在等待期间,则没有正在执行的导航需要取消。
如果每个状态都各自检查电量、取消请求和定位,规则就容易分散。修改一条策略时,需要检查多个状态及其进入、退出逻辑。
FSM 可以通过公共事件处理、分层组织等方式管理这些需求。学习行为树,是为了掌握另一种组织复杂任务的工具:用有层次的任务结构表达条件、优先级和执行关系。
区分底层能力与任务决策
机器人系统可以先按职责分成两层:
任务决策层
├── 当前应该执行哪个任务?
├── 应该去普通目标还是充电点?
└── 失败或条件变化后怎样处理?
↓ 调用
能力模块
├── Navigation 导航到指定目标
├── Localization 提供定位及其有效性
├── Battery 提供电量和充电信息
├── Sensors 提供环境数据
└── Actuators 执行运动等指令
导航模块主要解决“给定目标,如何到达”;任务决策层决定“现在是否导航、去哪里、失败怎么办”。行为树适合组织这部分任务决策,具体功能仍由相应模块实现。
用层次结构表达机器人任务
先用一个概念结构表达任务优先级:
RobotBehavior
├── EmergencyHandling 有紧急停止请求时,停止机器人
├── EnergyManagement 电量低时,处理充电
├── ExecuteMission 有任务时,执行导航等操作
└── WaitForTask 其余情况下,等待任务
这张图描述的是预期策略,不是可以直接运行的行为树。实际哪些条件会重新检查、怎样选择分支、如何中止旧任务,需要由具体节点和执行规则实现。
任务可以继续逐层展开:
RobotBehavior
├── EmergencyHandling
├── EnergyManagement
│ ├── CheckBattery
│ └── RechargeTask
│ ├── NavigateToChargingStation
│ └── StartCharging
└── ExecuteMission
├── CheckGoal
└── NavigateToGoal
阅读整体任务时,只看第一层就能知道主要职责;
研究充电流程时,再展开 RechargeTask。
小任务可以组成大任务,一整棵子树也可以成为更大任务的一部分。
这里仍只展示任务分解,具体节点类型和成功、失败时的执行规则后面再补充。
模块化、复用与组合怎样落到代码上
官方把节点比作积木:每个节点封装一种检查或操作,再把节点组合成任务。各个特点可以对应到具体的设计选择:
| 特点 | 在移动机器人中的做法 |
|---|---|
模块化 Modularity | 电量检查、导航请求、停止操作分别封装 |
可复用性 Reusability | 普通目标导航和返回充电点复用同一种导航节点实现,配置不同目标 |
可组合性 Composability | 将检查、导航、充电组合成完整充电任务 |
关注点分离 Separation of Concerns | 导航模块负责到达目标,行为树负责决定何时导航和如何处理结果 |
例如 IsBatteryLow 只检查电量,NavigateToGoal 负责请求导航,StartCharging 负责启动充电。这样可以分别测试和替换各项能力,也能在不同任务中重新组合。
可复用的是节点实现或子树设计,不意味着多处任务必须共享同一个正在运行的节点对象。具体怎样创建和配置实例,在后面的编程示例中学习。
行为树的树形图也有助于阅读工作流程。 不过,完整理解仍要结合每种节点的执行语义;仅凭连线无法判断所有运行细节。
反应性来自持续驱动与重新判断
机器人环境会在任务执行期间变化。例如:
开始导航:电量和定位正常,存在目标
↓
导航过程中:收到取消请求
↓
重新判断:取消当前导航,转入后续处理
反应性(Reactive)指系统能够在任务尚未完成时,根据变化调整行为。
前面学习的非阻塞 FSM,通过周期执行 Update() 检查事件和任务进度。
行为树通过外部持续发起 tick,按照树的规则推进任务:
| 执行方式 | 一次更新主要做什么 |
|---|---|
FSM 的 Update() | 根据当前状态处理事件,决定是否转移 |
| 行为树的一次 tick | 从根节点进入,按各节点的规则选择并执行当前需要的节点 |
对于持续任务,每次调用应及时交还控制权,方便后续继续决策。 导航系统可以在两次更新之间继续工作,决策层不必堵在一次调用中等待机器人到达。
行为树能否及时响应条件变化,取决于驱动频率、哪些条件被重新 tick,以及动作是否支持中止。 把条件节点放进树里,不代表它会自动持续监控;具体的响应式规则将在后续示例中学习。
状态机FSM与行为树BT可以配合使用
选择组织方式时,可以先看问题的结构:
| 场景 | 常见做法 |
|---|---|
| 设备具有明确阶段,如关闭、启动、运行、故障 | 使用 FSM 表达阶段及其转换 |
| 任务包含多种条件、优先级和可复用子流程 | 使用行为树组织任务 |
| 上层需要组合任务,某个底层模块内部又有明确阶段 | 上层使用行为树,模块内部保留 FSM |
例如:
RobotBehavior 行为树:组织整体任务
└── NavigateToGoal 动作节点:调用导航模块
└── NavigationModule 模块内部:用 FSM 管理运行阶段
这张图表达软件调用与组织关系。NavigationModule 是外部模块,不是动作节点下面的行为树子节点。
两种工具都能组织任务决策,适用范围也有重叠。 现有 FSM 结构清晰时,可以继续使用;任务需要更多层次和组合时,再考虑引入行为树。
本教程的学习顺序
本篇作为导读,后面按六个完整主题组织主线。阶段 1 集中建立概念,阶段 2 从最小程序逐步增加功能,统一使用 BehaviorTree.CPP 3.8.7、C++20 和 VSCode CMake Tools。
| 文档 | 主要内容 | 所在阶段 |
|---|---|---|
基本概念 | 节点、状态、tick 传播和四类节点 | 阶段 1 |
控制与装饰规则 | 顺序、备选方案、重试、重复和超时 | 阶段 1 |
第一个完整程序 | 先衔接概念,再从最小 demo 逐步增加条件和端口 | 阶段 1 收尾,进入阶段 2 |
节点之间传数据 | 输入输出、自定义类型和外部 Blackboard | 阶段 2 |
持续任务与中止 | 持续导航、多次 tick、响应式检查和取消 | 阶段 2 |
组合实际任务 | 子树映射、多 XML 文件和完整任务的成功、失败路径 | 阶段 2 收尾 |
实践按自己的完成情况推进:先运行当前版本,看到输出并理解执行过程,再增加下一步功能。各篇预留了 VSCode 截图位置,完整程序使用模拟移动机器人数据。