第一个完整程序
从节点、XML 到第一个完整程序
对应 BehaviorTree.CPP 3.8 官方文档:Learn the Basic Concepts → Main Concepts、The XML schema,以及 Tutorial - Basics 中的 Your first Behavior Tree 和 Blackboard and ports。
前面已经能读懂节点与控制规则。接下来把它们变成可运行的程序,同时补齐 C++、XML、Ports 和 Blackboard 的基础关系。
本篇前半部分完成阶段 1 的概念学习,后半部分进入阶段 2 的编程实践。 实践部分在同一个独立 C++ 工程中逐步增加功能:先运行一个最小动作,再加入电量检查和目标传递。实际导航任务留到异步动作章节实现。
C++ 实现节点,XML 组织节点
可以把一个程序分成三部分:
| 部分 | 负责什么 | 例子 |
|---|---|---|
| C++ 节点 | 实现具体检查或操作,声明输入输出接口 | 检查电量、选择目标、打印目标 |
| XML | 描述树的结构,配置节点参数和数据连接 | 把三个节点放进 Sequence |
| 外部程序 | 注册节点、创建树、驱动执行 | BehaviorTreeFactory、tickRoot() |
例如:
C++ 注册可用的节点类型
↓
工厂读取 XML,创建具体节点实例并连接成树
↓
外部 tick 根节点
↓
按树的规则调用各节点的回调
“注册节点”是告诉工厂:遇到某个类型标识时,怎样创建对应的 C++ 节点。它不会立即执行任务。
例如 factory.registerNodeType<PrintGoal>("PrintGoal") 将字符串 PrintGoal 与 C++ 类关联;之后 XML 中每出现一次 <PrintGoal/>,工厂就可以创建一个实例。注册类型和创建实例是两个步骤,同一类型可以创建多个实例。
普通自定义节点通常通过继承实现,例如继承 ConditionNode、SyncActionNode。简单检查也可以通过 registerSimpleCondition() 包装函数或 lambda,简单同步动作可以用 registerSimpleAction()。
树通常在程序初始化时创建一次,后续反复 tick,而不是每轮重新解析 XML。调整已有节点的排列或配置后,可以重新运行程序加载新的 XML;新增 C++ 节点实现时,仍要重新编译。
用 XML 描述一棵完整的树
下面是本篇逐步扩展后的 XML,用来认识文件结构。实践部分会先运行更小的树,最后再得到这个版本:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<SelectGoal name="select_goal" goal="{mission_goal}"/>
<PrintGoal name="print_goal" goal="{mission_goal}"/>
</Sequence>
</BehaviorTree>
</root>
它对应:
Sequence
├── CheckBattery
├── SelectGoal 输出选定的目标
└── PrintGoal 读取并打印该目标
分清 XML 中几种不同的标识
| 写法 | 含义 |
|---|---|
<root> | XML 文档的外层容器,不是可执行的行为树节点 |
main_tree_to_execute="MainTree" | 指定默认执行哪棵树 |
<BehaviorTree ID="MainTree"> | 定义一棵名叫 MainTree 的树 |
<Sequence> | 这棵树实际的根节点 |
<PrintGoal> | 注册到工厂的节点类型标识,必须能找到对应实现 |
name="print_goal" | 当前节点实例的名称,便于日志和调试,可省略 |
goal="{mission_goal}" | 把节点的 goal 端口连接到 Blackboard 的 mission_goal 条目 |
因此,树的 ID、节点类型的注册标识、实例的 name 和数据条目的名称,各自有不同用途。
一个 XML 文件可以定义多棵 <BehaviorTree>;每棵树下面应有一个实际根节点。
只有一棵树时,可以省略 main_tree_to_execute;
有多棵树、需要从文件直接确定默认入口时,应明确填写。
XML 中的父子关系仍遵守前面的结构约束:控制节点包含一个或多个子节点,装饰节点包含一个,条件和动作节点没有子节点。
Ports 与 Blackboard:让数据在节点之间流动
执行状态和业务数据走的是不同的通道:
SUCCESS / FAILURE / RUNNING 告诉父节点任务的执行情况
Ports + Blackboard 传递目标、路径、观测值等业务数据
节点的输入输出端口类似函数接口; Blackboard 则用键值条目保存数据:
| 概念 | 本例中的作用 |
|---|---|
输出端口 OutputPort | SelectGoal 通过 goal 端口写出目标 |
| Blackboard 条目 | 使用键 mission_goal 保存目标值 |
输入端口 InputPort | PrintGoal 通过自己的 goal 端口读取目标 |
连接过程是:
SelectGoal 的输出端口 goal
↓ setOutput()
Blackboard["mission_goal"] = "loading_area"
↓ getInput()
PrintGoal 的输入端口 goal
两个端口恰好都叫 goal,但真正把它们连接起来的是 XML 中相同的 {mission_goal}。
生产者和消费者的端口名称可以不同,只要它们映射到同一个条目。
固定值与 Blackboard 引用
下面两种输入配置含义不同:
<PrintGoal goal="loading_area"/>
<PrintGoal goal="{mission_goal}"/>
第一种直接提供固定字符串;
第二种读取 Blackboard 中键为 mission_goal 的值。
花括号表达条目引用,不是 C++ 指针,也不会自动创建有效的业务数据。
读取前必须有人写入该值,例如前面的输出节点,或者由外部程序初始化 Blackboard。引用了一个从未写入的条目,输入读取就会失败。
接口由 C++ 声明,连接由 XML 配置
节点用静态方法 providedPorts() 声明端口的名称、方向和 C++ 类型。
XML 则给端口配置固定值,或把它映射到 Blackboard。
输入使用 getInput<T>(),输出使用 setOutput()。
读取可能因为缺少数据、类型或转换问题而失败,节点需要检查结果;本篇选择在配置错误时抛出异常,由主程序报告。
应在执行回调中读取输入,让后续执行能取得当前值。不要只在构造函数中读取一次,再假定这个值永远不变。
Blackboard 可以保存多种 C++ 类型,并不限于字符串;从 XML 文本转换自定义类型、双向端口以及子树的数据映射,会在后面的数据传递实践中学习。
认识 XML 中的几种补充写法
简写与显式写法
下面两种写法指向同一种动作节点:
<PrintGoal name="print_goal" goal="loading_area"/>
<Action ID="PrintGoal" name="print_goal" goal="loading_area"/>
前者直接把注册标识写作标签名;
后者用 Action 指明分类,再用 ID 指定注册标识。
本教程以简写为主。
XML 中还可能出现 <TreeNodeModel>,为 Groot 等工具提供自定义节点的分类和端口描述。
它是模型信息,不是执行分支,也不会替代 C++ 节点注册。
运行本篇示例时不需要额外填写它。
用 SubTree 引用另一棵树
同一个 XML 中可以定义并复用一段行为:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence>
<CheckBattery/>
<SubTree ID="ReportTask"/>
</Sequence>
</BehaviorTree>
<BehaviorTree ID="ReportTask">
<PrintGoal goal="loading_area"/>
</BehaviorTree>
</root>
这里先检查电量,再执行 ReportTask。
<SubTree ID="ReportTask"/> 引用的是树定义,不是普通 C++ 动作类;工厂在创建树时展开并建立相应结构。
所以这个 XML 标签本身虽然没有嵌套内容,运行时仍连接到所引用的子树。子树如何传数据,需要明确考虑其 Blackboard 范围,后面再讲。
用 include 拆分文件
也可以在 <root> 中写:
<include path="report_task.xml"/>
它把其他 XML 中的树定义引入当前文件,再通过 SubTree 使用。
相对路径以包含它的 XML 文件位置为基准;引入定义不会自动执行一遍那棵树。
另一种方式是在 C++ 中用 registerBehaviorTreeFromFile() 加载各个文件的树定义,再用 createTree("MainTree") 创建选定的树。
当前先掌握单文件示例,多文件组织留到任务组合时实践。
入门总结
读完以上概念部分,加上前面的入门、基本概念和控制规则,主线已经覆盖:
| 官方部分 | 已建立的认识 |
|---|---|
Introduction to BTs | 树结构、tick、状态、节点分类和基本执行规则 |
Main Concepts | 可复用节点、回调、运行时创建树,以及 Ports/Blackboard |
The XML schema | 树与节点标识、端口映射、简写与显式写法、子树及文件引用 |
Learn the Basic Concepts 到此结束。 从下面的完整工程开始,进入基础编程实践。 后面通过例子深化端口、异步和子树,不需要现在把所有接口一次记住。
从最小 demo 开始搭建工程
先把“注册一个动作 → 读取 XML → tick 执行”跑通,再逐步加功能。 本篇只使用一个工程目录,按下面的顺序修改同一组文件:
| 实践内容 | 本次新增什么 | 能观察到什么 |
|---|---|---|
| 最小 demo | 一个打印信息的动作 | C++ 动作被 XML 调用 |
| 电量检查 | 一个条件和 Sequence | 电量不足时,后面的动作被跳过 |
| 固定目标 | 一个带输入端口的节点类 | 从 XML 读取目标字符串 |
| 目标传递 | 一个输出节点和 Blackboard 映射 | 前面的节点产生目标,后面的节点读取 |
这些例子都使用模拟数据,不需要启动 ROS 2 或连接机器人。先运行最小版本,后续每完成一处修改,再编译或运行对应版本。
沿用 C++ 教程中的 CMake 框架
本篇基于 CMake工程模板 建立 BT 练习版,
沿用 CMakePresets.json、公共编译选项和 src 目录。相关文件的作用可以对照阅读:
| 本篇使用的部分 | 对应 C++ 教程 |
|---|---|
| Debug / Release 预设、Ninja 和编辑器构建 | CMakePresets与构建安装 |
| 顶层入口、C++ 标准和公共选项 | 顶层CMake与公共编译选项 |
| 主程序与依赖的链接 | src与lib模块CMake详解 |
先准备 Git、支持 C++20 的编译器、CMake 3.22 或更新版本,以及 Ninja; 工具安装见 C++开发环境搭建与测试。 下载模板需要访问 GitHub;行为树依赖优先使用已安装的 3.8.7,只有未找到对应版本时才联网下载。
Ubuntu 22.04 可以使用系统软件源提供的 CMake 3.22.1,当前模板已采用对应的最低版本和预设格式,直接沿用即可。 本篇使用的配置功能已受该版本支持,无需为这些练习升级 CMake。
在容器中开发时,CMake 和 Ninja 都应准备在容器内,VSCode 的 CMake Tools 也应使用容器内的工具。
在 VSCode 中打开工程并准备文件
先准备一份新的模板副本,练习目录命名为 bt_first_tree。
git clone https://github.com/tungchiahui/CMake_Template bt_first_tree
cd bt_first_tree
code .

左侧资源管理器中应直接看到顶层 CMakeLists.txt 和 CMakePresets.json,说明打开的是工程根目录。

在扩展面板确认已安装微软的 CMake Tools 和 C/C++。 本篇通过 CMake Tools 的 Configure、Build、Launch 和 Debug 入口操作工程;断点调试还需要前面 C++ 环境章节准备好的 GDB。
在左侧资源管理器中,右键工程根目录,新建 trees 文件夹;在其中新建 mission.xml。
再在已有的 cmake 文件夹中新建 BehaviorTree.cmake。
后面直接在编辑器中填写这些文件,先完成配置和代码,再执行 Configure。

本篇参与构建和运行的文件如下,模板中的其他演示文件没有列出:
bt_first_tree/
├── CMakeLists.txt
├── CMakePresets.json 沿用 linux-debug / linux-release 预设
├── cmake/
│ ├── ProjectOptions.cmake 公共编译选项
│ └── BehaviorTree.cmake 新增:查找 3.8.7,未安装时获取源码
├── src/
│ ├── CMakeLists.txt 创建主程序、链接依赖、设置 XML 路径
│ └── main.cpp 本篇逐步扩展的 C++ 程序
└── trees/
└── mission.xml 本篇逐步扩展的树
下面先在编辑器中准备构建配置,再保存最小程序与 XML。 完成后统一通过 CMake Tools 配置、构建和运行。
调整工程名称并沿用模板配置
将顶层 CMakeLists.txt 里的project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)改成project(bt_first_tree VERSION 1.0.0 LANGUAGES C CXX),主要是把项目名改成 bt_first_tree。
当前模板已经使用下列配置,保留原值即可:
| 文件与位置 | 当前配置 |
|---|---|
顶层 CMakeLists.txt 的最低版本声明 | cmake_minimum_required(VERSION 3.22) |
CMakePresets.json 顶层的 version | 3 |
CMakePresets.json 中的 cmakeMinimumRequired | major: 3、minor: 22、patch: 0 |
其中,预设文件的 version 表示 JSON 格式版本,cmakeMinimumRequired 表示工程要求的 CMake 版本。
当前预设只使用格式 3 已支持的功能,说明见 CMake 3.22 预设文档。
继续保留原有的 linux-debug / linux-release 配置、构建预设和 Ninja 生成器。
cmake/ProjectOptions.cmake 已默认使用 C++20,下面两项同样沿用模板原值;project_options、project_warnings 等其他配置继续保留:
set(CMAKE_CXX_STANDARD 20)
target_compile_features(project_options INTERFACE cxx_std_20)
本篇练习主程序统一使用 C++20。链接现成的行为树库时不重新编译该库;走源码获取分支时,库按自己的构建规则使用 C++14。
集中保存行为树依赖配置
将以下完整内容保存到新增的 cmake/BehaviorTree.cmake。这份配置只准备一次,后续几轮不用反复修改。它先查找已安装的 3.8.7;未找到对应版本时,再获取同一版本的源码:
include_guard(GLOBAL)
# 优先查找当前构建环境中已安装的目标版本
find_package(behaviortree_cpp_v3 3.8.7 EXACT QUIET CONFIG)
if(behaviortree_cpp_v3_FOUND)
# ROS 包和独立安装的源码包可能导出不同名称的 CMake target
if(TARGET behaviortree_cpp_v3::behaviortree_cpp_v3)
set(BTCPP_TARGET behaviortree_cpp_v3::behaviortree_cpp_v3)
elseif(TARGET BT::behaviortree_cpp_v3)
set(BTCPP_TARGET BT::behaviortree_cpp_v3)
else()
message(FATAL_ERROR "BehaviorTree.CPP 3.8.7 was found without a supported target")
endif()
message(STATUS "Using installed BehaviorTree.CPP 3.8.7: ${behaviortree_cpp_v3_DIR}")
else()
include(FetchContent)
# 未安装目标版本时,固定获取 3.8.7 源码
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(BUILD_MANUAL_SELECTOR OFF CACHE BOOL "" FORCE)
set(ENABLE_COROUTINES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
behaviortree_cpp_v3
URL https://codeload.github.com/BehaviorTree/BehaviorTree.CPP/tar.gz/refs/tags/3.8.7
)
FetchContent_MakeAvailable(behaviortree_cpp_v3)
# 3.8.7 会创建 sample_nodes,本篇将它排除在默认构建之外
set_property(DIRECTORY "${behaviortree_cpp_v3_SOURCE_DIR}/sample_nodes"
PROPERTY EXCLUDE_FROM_ALL TRUE)
set(BTCPP_TARGET behaviortree_cpp_v3)
message(STATUS "Building BehaviorTree.CPP 3.8.7 from source")
endif()
在已加载 ROS 2、能找到安装包的 Humble 环境中,Configure 输出会包含 Using installed BehaviorTree.CPP 3.8.7,并显示包配置所在路径。此时直接链接容器里的库,不下载或重新编译它。当前构建环境找不到该版本时,则显示 Building BehaviorTree.CPP 3.8.7 from source。
复用 Humble 安装包时,配置和运行程序都需要 ROS 2 环境。本篇测试在加载 /opt/ros/humble/setup.bash 后完成;VSCode 的 CMake Tools 与运行、调试进程也应继承这份环境。
只在已打开窗口的集成终端中加载环境,不会自动更新扩展进程。
未准备 ROS 2 环境的独立工程可以使用同版本源码分支,程序本身不需要启动 ROS 节点。
BTCPP_TARGET 保存本次应链接的 CMake target,主程序统一使用这个变量。这样两种依赖来源使用同一份 C++ 和 XML,框架也不需要手动切换。
源码分支的构建选项可对照官方 3.8.7 CMakeLists.txt。3.8.7 不再提供 BUILD_SAMPLES 开关,所以本篇直接将 sample_nodes 排除在默认构建之外。关闭协程不影响本篇同步节点,也不影响后面使用 StatefulActionNode。
3.8.7 的源码分支已在 GCC 16 环境中构建运行通过,配置无需额外添加模板兼容选项。
让主程序链接行为树库
将 src/CMakeLists.txt 整个替换为:
include("${PROJECT_SOURCE_DIR}/cmake/BehaviorTree.cmake")
add_executable(${PROJECT_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
project_options
project_warnings
${BTCPP_TARGET}
)
# 将源码目录中的 XML 完整路径作为字符串交给程序
target_compile_definitions(${PROJECT_NAME}
PRIVATE BT_XML_PATH="${PROJECT_SOURCE_DIR}/trees/mission.xml"
)

模板中的 lib1、lib2 目录可以保留,但这份文件已经不再构建或链接它们;替换主程序后,本篇也不需要 Eigen 示例依赖。
BT_XML_PATH 是这份配置提供的字符串,自动指向当前工程的 trees/mission.xml。后面的 C++ 直接使用它,不需要手写本机绝对路径,也不必随终端或编辑器的运行位置调整相对路径。
保存最小 C++ 程序
将以下完整内容保存到 src/main.cpp,替换模板原来的演示主程序:
#include "behaviortree_cpp_v3/bt_factory.h"
#include <iostream>
// 一个立即完成的动作:打印机器人已就绪
BT::NodeStatus ReportReady(BT::TreeNode&)
{
std::cout << "[ReportReady] robot is ready\n";
return BT::NodeStatus::SUCCESS;
}
int main()
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleAction("ReportReady", ReportReady);
// 根据 XML 创建树,再从根节点执行一次
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickRoot();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
ReportReady() 是普通 C++ 函数。
通过 registerSimpleAction() 注册后,框架可以将它作为同步动作调用。
参数 BT::TreeNode& 由框架传入,本例暂时不使用,所以没有为它命名。
这里先保留最小正常路径;等到读取端口输入时,再加入输入检查和异常报告。
注册简单动作与创建树的 API
BT::BehaviorTreeFactory factory; 创建一个节点工厂对象,本例不需要给构造函数传参。
后面的注册操作告诉工厂怎样创建自定义节点;加载 XML 时,工厂再按注册名创建节点实例。
下面的函数声明用于理解接口,不用再次粘贴到工程中。 原型按官方 3.8.7 bt_factory.h 核对。
registerSimpleAction()
函数原型:
void registerSimpleAction(
const std::string& ID,
const BT::SimpleActionNode::TickFunctor& tick_functor,
BT::PortsList ports = {});
作用:把一个回调函数注册成简单同步动作,之后可以在 XML 中使用对应的节点名。
| 参数 | 类型与含义 | 本例传入 |
|---|---|---|
第一个 ID | 字符串,注册到工厂的节点类型名称,对应 XML 标签 | "ReportReady",对应 <ReportReady/> |
第二个 tick_functor | 执行该节点时调用的函数,签名为 BT::NodeStatus(BT::TreeNode&) | 普通函数 ReportReady |
第三个 ports | 可选的端口声明,类型为 BT::PortsList,默认空列表 | 省略,因为这个最小动作不读写端口 |
第二个参数传的是 ReportReady 函数本身,没有调用括号。注册时先保存回调;树执行到这个动作时,框架才调用它,并传入代表当前节点实例的 BT::TreeNode&。本例不使用这个引用,所以函数定义中只保留类型,没有写形参名。
返回值:void,注册函数本身不返回任务状态;ReportReady() 回调返回的 BT::NodeStatus 才是节点执行结果。这个简单动作属于同步动作,回调应返回 SUCCESS 或 FAILURE,不能返回 RUNNING。注册名重复等注册错误会抛出异常。
createTreeFromFile()
函数原型:
BT::Tree createTreeFromFile(
const std::string& file_path,
BT::Blackboard::Ptr blackboard = BT::Blackboard::create());
作用:读取 XML 文件,并按照工厂已有的节点类型和 XML 结构创建一棵树。
| 参数 | 含义 | 本例用法 |
|---|---|---|
第一个 file_path | 入口 XML 的路径字符串 | BT_XML_PATH,由 CMake 提供的完整文件路径 |
第二个 blackboard | 可选,指定根树使用的 Blackboard | 本例省略,由默认参数创建;下一篇会从外部传入 |
返回值:BT::Tree,本例用 auto tree 接收。创建时会构造节点并检查 XML,但还没有开始执行动作的 tick()。文件不存在、XML 错误或使用未注册节点等情况会抛出异常。
tickRoot() 与 BT::toStr()
tree.tickRoot() 不接收参数,返回 BT::NodeStatus。调用一次,就从根节点驱动一次;一次根 tick 可能继续执行多个立即完成的节点,也可能在某个节点返回 RUNNING 时先返回主程序。
BT::toStr(result) 的参数是需要显示的节点状态,返回对应的 std::string,例如 SUCCESS。它只负责把状态变成文本;真正决定程序退出码的仍是后面的状态比较。
保存只有一个动作的 XML
将以下内容保存到 trees/mission.xml:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<ReportReady/>
</BehaviorTree>
</root>
这棵树只有一个动作,ReportReady 就是实际根节点,不需要为了凑结构而添加 Sequence。
在 CMake Tools 中配置工程
保存前面填写的所有文件,再打开左侧 CMake 面板。选择 Configure 预设 Linux Debug,对应 CMakePresets.json 中的 linux-debug。
也可以在命令面板中执行 CMake: Select Configure Preset,选择同一个预设。这里使用模板已有的预设,不需要重新创建 CMakePresets.json。
点击 CMake Tools 的 Configure。 这一步读取工程配置并准备 BehaviorTree.CPP 依赖;已安装 3.8.7 时直接使用,否则第一次会下载源码。等待输出中出现配置和生成成功的信息。


构建并选择运行目标
在 CMake 面板中确认 Build 预设为 linux-debug,点击 Build。等待构建完成,确认没有编译或链接错误。

在 CMake Tools 的 Launch 项中选择 bt_first_tree,然后点击旁边的运行按钮。
也可以使用 CMake Tools 提供的状态栏 Launch 入口。
这里启动的是整个 CMake 工程生成的可执行文件。

程序输出会显示在 VSCode 的集成终端中。配置、构建和运行的详细界面说明可对照 CMakePresets与构建安装,运行入口也可参考 CMake Tools 官方说明。
预期输出:
[ReportReady] robot is ready
Tree result: SUCCESS

执行过程是:
注册 ReportReady 对应的函数
↓
工厂读取 XML,创建 ReportReady 节点
↓
tree.tickRoot()
↓
调用 ReportReady(),打印信息并返回 SUCCESS
先看到这两行输出,再往下加功能。 到这里已经完成了一个可以独立运行的行为树程序。
在动作回调中设置断点
想观察函数什么时候被调用,可以在 ReportReady() 内的 std::cout 语句左侧单击,设置一个断点。
保持 linux-debug 和 bt_first_tree 目标不变,点击 CMake Tools 的 Debug。
程序会在执行打印语句前暂停。此时可以查看调用堆栈,按 F10 执行当前语句,再按 F5 继续到程序结束。后面的普通运行继续使用 Launch;这个最小示例由 CMake Tools 直接启动调试,不需要另外编写 launch.json。

加入电量检查,观察 Sequence 怎样执行
保留刚才的工程和 ReportReady()。在 src/main.cpp 中、main() 函数前面,增加下面的条件函数:
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
然后在 main() 中,已有的 registerSimpleAction() 后面、createTreeFromFile() 前面,增加这一行注册:
factory.registerSimpleCondition("CheckBattery", CheckBattery);
registerSimpleCondition() 的参数
函数原型:
void registerSimpleCondition(
const std::string& ID,
const BT::SimpleConditionNode::TickFunctor& tick_functor,
BT::PortsList ports = {});
作用:把回调注册成简单条件节点。
- 第一个参数
ID是 XML 中使用的注册名,本例为"CheckBattery",对应<CheckBattery/>。 - 第二个参数
tick_functor是条件检查函数,本例传CheckBattery,签名同样为BT::NodeStatus(BT::TreeNode&)。收到 tick 时才执行检查,本例用SUCCESS表示电量足够,用FAILURE表示不足。 - 第三个参数
ports是可选端口列表。本例直接使用函数中的模拟电量,所以省略它。
返回值:void。条件是否满足由 CheckBattery() 的返回状态表达;注册调用本身不执行这次电量检查。
将 trees/mission.xml 整个替换为:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<ReportReady name="report_ready"/>
</Sequence>
</BehaviorTree>
</root>
树变为:
Sequence
├── CheckBattery
└── ReportReady
保存 src/main.cpp 和 trees/mission.xml,在 CMake Tools 中先点击 Build,成功后点击 Launch。这次只改了 C++ 和 XML,构建配置没有变化,沿用当前预设和运行目标即可。
#include "behaviortree_cpp_v3/bt_factory.h"
#include <iostream>
// 一个立即完成的动作:打印机器人已就绪
BT::NodeStatus ReportReady(BT::TreeNode&)
{
std::cout << "[ReportReady] robot is ready\n";
return BT::NodeStatus::SUCCESS;
}
// 电量检查
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
int main()
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleAction("ReportReady", ReportReady);
factory.registerSimpleCondition("CheckBattery", CheckBattery);
// 根据 XML 创建树,再从根节点执行一次
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickRoot();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
电量为 80 时,预期输出:
[CheckBattery] battery=80
[ReportReady] robot is ready
Tree result: SUCCESS

接着在编辑器中将 CheckBattery() 的 battery_level 改为 10.0,保存后再次点击 Build → Launch:
[CheckBattery] battery=10
Tree result: FAILURE

CheckBattery 失败后,Sequence 不再执行 ReportReady。程序退出码为 1,表示树任务失败,不是编译错误。
继续下一步前,把 battery_level 改回 80.0。 否则后面新增的动作仍然会因为电量条件失败而被跳过。
将打印动作换成带输入端口的节点类
现在希望目标可以从 XML 配置。把原来固定打印“机器人已就绪”的动作,换成读取并打印 goal 的 PrintGoal。
这一轮只增加输入端口,先使用 XML 固定字符串;下一轮再连接 Blackboard。
添加头文件并替换动作实现
保留 bt_factory.h 和 <iostream>,再在文件顶部增加两个头文件:
#include <exception>
#include <string>
删除原来的 ReportReady() 函数,换成下面的完整 PrintGoal 类;
CheckBattery() 函数继续保留,电量恢复为 80.0:
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点需要一个名为 goal 的字符串输入
return {BT::InputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
// 每次执行时读取输入,读取成功后再使用
auto goal = getInput<std::string>("goal");
if (!goal)
{
throw BT::RuntimeError("Cannot read input [goal]: ", goal.error());
}
std::cout << "[PrintGoal] target=" << goal.value() << '\n';
return BT::NodeStatus::SUCCESS;
}
};
类里的几个部分各有职责:
| 部分 | 作用 |
|---|---|
继承 BT::SyncActionNode | 表示这是立即完成的同步动作 |
| 构造函数 | 将实例名称和配置交给基类保存 |
providedPorts() | 声明字符串输入端口 goal |
tick() | 读取目标,打印后返回成功 |
构造函数的 name 与 config
本例构造函数的两个参数由工厂在创建节点实例时提供:
| 参数 | 类型 | 含义 |
|---|---|---|
第一个 name | const std::string& | 当前节点的实例名称;XML 可以用 name 属性设置,省略时通常使用节点的注册名 |
第二个 config | const BT::NodeConfiguration& | 当前实例的配置,包含 Blackboard,以及输入、输出端口的映射 |
初始化列表中的 BT::SyncActionNode(name, config) 将这两个参数交给基类保存,后面的 getInput() 才能按本实例的 XML 配置读取数据。
这里不需要在 main() 中手动调用 PrintGoal 的构造函数。
providedPorts() 与 InputPort<T>()
static BT::PortsList providedPorts() 不接收参数,返回本节点的端口声明。它是自定义节点按约定提供的静态函数,工厂注册节点类时会读取它;声明里保存的是接口信息,实际目标值由 XML 或 Blackboard 提供。
本例使用 BT::InputPort<std::string>("goal"):
| 位置 | 含义 |
|---|---|
模板参数 <std::string> | 输入值的 C++ 类型 |
第一个函数参数 "goal" | 节点自己的输入端口名,对应 XML 的 goal 属性 |
| 可选的第二个函数参数 | 端口说明文字,本例省略;带默认值的三参数形式在下一篇说明 |
返回值:一个端口名称和 BT::PortInfo 组成的键值对,加入 BT::PortsList 后供工厂使用。这次调用声明端口,不会立即读取目标。
getInput<T>() 的参数与返回值
本例使用的原型为:
template <typename T>
BT::Optional<T> getInput(const std::string& key) const;
作用:根据当前节点的端口映射读取输入,并按 T 获取或转换数据。
- 模板参数
T是期望读取的类型,本例为std::string。 - 唯一的函数参数
key是节点自己的端口名,本例为"goal"。XML 决定它来自固定文本还是某个 Blackboard 条目。
返回值:BT::Optional<T>,这是 BehaviorTree.CPP 使用的带错误信息的结果类型。先用 if (!goal) 判断读取是否失败;有效时用 goal.value() 取得值,失败时用 goal.error() 取得错误字符串。
throw BT::RuntimeError("Cannot read input [goal]: ", goal.error()); 将固定提示和库返回的错误信息组成异常,再交给 main() 的 catch 显示。BT::RuntimeError 是库提供的异常类型,可以被 const std::exception& 捕获。接口细节见 3.8.7 tree_node.h。
替换 main 函数
将 main() 整个替换为以下版本。这里保留电量条件的注册,将动作注册改成 registerNodeType<PrintGoal>(),并添加错误报告:
int main()
{
try
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition("CheckBattery", CheckBattery);
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickRoot();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
catch (const std::exception& error)
{
// 创建树或读取输入出错时,显示原因
std::cerr << "Error: " << error.what() << '\n';
return 2;
}
}
PrintGoal 类和 CheckBattery() 都应放在 main() 前面,保证注册时已经有对应定义。
registerNodeType<T>() 的参数
本例使用的原型为:
template <typename T>
void registerNodeType(const std::string& ID);
作用:将一个自定义节点类注册到工厂,让工厂能够按 XML 创建该类的实例。
| 位置 | 含义 | 本例 |
|---|---|---|
模板参数 T | C++ 中已经定义的节点类 | PrintGoal |
第一个函数参数 ID | XML 使用的注册名 | "PrintGoal",对应 <PrintGoal/> |
<PrintGoal> 是模板参数,("PrintGoal") 中的字符串才是函数实参。这两个名字在本例中保持一致以便阅读;C++ 类名和 XML 注册名分别负责不同的一层。
返回值:void。注册时读取这个类的端口声明并保存创建方式;加载 XML 时,工厂再为每个节点实例提供 name 和 config,调用前面说明的构造函数。
从 XML 传入固定目标
将 trees/mission.xml 整个替换为:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<PrintGoal name="print_goal" goal="loading_area"/>
</Sequence>
</BehaviorTree>
</root>
#include "behaviortree_cpp_v3/bt_factory.h"
#include <iostream>
#include <exception>
#include <string>
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点需要一个名为 goal 的字符串输入
return {BT::InputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
// 每次执行时读取输入,读取成功后再使用
auto goal = getInput<std::string>("goal");
if (!goal)
{
throw BT::RuntimeError("Cannot read input [goal]: ", goal.error());
}
std::cout << "[PrintGoal] target=" << goal.value() << '\n';
return BT::NodeStatus::SUCCESS;
}
};
// 电量检查
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
int main()
{
try
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition("CheckBattery", CheckBattery);
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickRoot();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
catch (const std::exception& error)
{
// 创建树或读取输入出错时,显示原因
std::cerr << "Error: " << error.what() << '\n';
return 2;
}
}
保存修改,在 CMake Tools 中点击 Build,成功后点击 Launch。
预期输出:
[CheckBattery] battery=80
[PrintGoal] target=loading_area
Tree result: SUCCESS
现在只把 XML 中的 goal="loading_area" 改为 goal="charging_station",保存 trees/mission.xml 后再次点击 Launch。这一步没有改 C++,不需要手动重新 Build。
打印目标会变为 charging_station。这样就把节点的具体实现和本次配置的目标分开了。


再增加输出节点,让两个动作传递目标
固定值已经能读到了,现在让前面的节点产生目标,后面的节点通过 Blackboard 读取。保留上一轮的 PrintGoal、CheckBattery() 和 main(),只增加一个类和一行注册。
增加 SelectGoal 类并注册
在 PrintGoal 类前面加入:
class SelectGoal : public BT::SyncActionNode
{
public:
SelectGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点通过 goal 输出选定的目标
return {BT::OutputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
const std::string goal = "loading_area";
setOutput<std::string>("goal", goal);
std::cout << "[SelectGoal] goal=" << goal << '\n';
return BT::NodeStatus::SUCCESS;
}
};
在 main() 中、注册 PrintGoal 的前面增加:
factory.registerNodeType<SelectGoal>("SelectGoal");
工厂已有 Sequence 等内置节点;本轮只是多注册一种自定义动作类型,不需要修改构建配置。
OutputPort<T>() 与 setOutput()
BT::OutputPort<std::string>("goal") 声明字符串输出端口:模板参数指定值类型,第一个函数参数是端口名,可选的第二个参数是说明文字。返回值同样是用于组成 BT::PortsList 的端口键值对。
写入使用下面的接口:
template <typename T>
BT::Result setOutput(const std::string& key, const T& value);
| 参数 | 含义 | 本例 |
|---|---|---|
第一个 key | 当前节点自己的输出端口名 | "goal" |
第二个 value | 要写出的 C++ 值,类型为 T | 保存 "loading_area" 的字符串变量 goal |
XML 会把这个输出端口连接到 Blackboard 条目;节点实现仍然只使用自己的端口名。返回值为 BT::Result,成功时可判真,失败时可判假并调用 .error() 查看原因。本篇先展示正常写入路径,下一篇会用 written 变量显式检查这个结果。
把固定值改成 Blackboard 映射
将 trees/mission.xml 整个替换为下面的最终版本:
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<SelectGoal name="select_goal" goal="{mission_goal}"/>
<PrintGoal name="print_goal" goal="{mission_goal}"/>
</Sequence>
</BehaviorTree>
</root>
#include "behaviortree_cpp_v3/bt_factory.h"
#include <iostream>
#include <exception>
#include <string>
class SelectGoal : public BT::SyncActionNode
{
public:
SelectGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点通过 goal 输出选定的目标
return {BT::OutputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
const std::string goal = "loading_area";
setOutput<std::string>("goal", goal);
std::cout << "[SelectGoal] goal=" << goal << '\n';
return BT::NodeStatus::SUCCESS;
}
};
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfiguration& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点需要一个名为 goal 的字符串输入
return {BT::InputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
// 每次执行时读取输入,读取成功后再使用
auto goal = getInput<std::string>("goal");
if (!goal)
{
throw BT::RuntimeError("Cannot read input [goal]: ", goal.error());
}
std::cout << "[PrintGoal] target=" << goal.value() << '\n';
return BT::NodeStatus::SUCCESS;
}
};
// 电量检查
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
int main()
{
try
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition("CheckBattery", CheckBattery);
factory.registerNodeType<SelectGoal>("SelectGoal");
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickRoot();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
catch (const std::exception& error)
{
// 创建树或读取输入出错时,显示原因
std::cerr << "Error: " << error.what() << '\n';
return 2;
}
}
保存修改,在 CMake Tools 中点击 Build,成功后点击 Launch。
预期输出:
[CheckBattery] battery=80
[SelectGoal] goal=loading_area
[PrintGoal] target=loading_area
Tree result: SUCCESS

这时再对照数据流:
SelectGoal::tick()
↓ setOutput("goal", "loading_area")
Blackboard["mission_goal"]
↓ getInput<std::string>("goal")
PrintGoal::tick()
C++ 操作各自的 goal 端口,XML 中的 {mission_goal} 决定它们连接哪个 Blackboard 条目。
SelectGoal 先写入,PrintGoal 后读取,执行顺序由 Sequence 保证。
解释端口名和blackboard
Blackboard 存放数据;端口名更像“函数参数名”,数据类型是另外指定的。
拿“柜子取东西”来比喻:
- Blackboard:一个储物柜。
- Blackboard 键名:抽屉上的标签,例如 pickup_goal。
- 数据:抽屉里放的内容,例如 "loading_area"。
- 端口名:节点接收数据的接口名称,例如 goal。
看这一行 C++:
BT::InputPort<std::string>("goal")
意思是:这个节点有一个叫 goal 的输入接口,接收字符串。 其中 std::string 才是类型,goal 是端口名。
再看 XML:
<NavigateToGoal goal="{pickup_goal}"/>
意思是:到 Blackboard 中找到标着 pickup_goal 的抽屉,把里面的数据交给这个导航节点的 goal 输入接口。
对应关系就是:
Blackboard 的 pickup_goal 抽屉
里面保存 "loading_area"
↓ XML 指定从这里取
导航节点的 goal 输入端口
↓
节点知道:这次去 loading_area
端口负责让节点接收或写出数据;Blackboard 负责保存数据;XML 负责指定它们怎么连接。
保持每一步的编译、执行和路径一致
本篇各轮实践都沿用同一套框架配置、同一个可执行文件和 trees/mission.xml:
| 改了什么 | 接下来执行什么 |
|---|---|
| 第一次建立工程 | 选择 linux-debug,点击 Configure → Build → Launch |
| 修改 C++ 函数、类或注册代码 | 保存后点击 Build → Launch |
| 只改 XML 结构或端口配置 | 保存后再次点击 Launch |
| 修改 CMake 配置或移动工程目录 | 点击 Configure → Build → Launch |
C++ 中使用 createTreeFromFile(BT_XML_PATH),实际 XML 路径由 src/CMakeLists.txt 根据源码目录提供。无论从工程根目录、构建目录,还是编辑器中启动,程序读取的都是源码中的 trees/mission.xml;只修改 XML 后重新运行即可。
这个完整路径也避免了向解析器传入不含目录部分的裸文件名。本篇的路径设置用于源码工程的开发和调试,安装发布时的资源布局后续按需处理;当前练习编译后直接运行,不要求执行安装。
本篇以 VSCode 的 CMake Tools 为操作入口:Configure 准备构建配置,Build 编译程序,Launch 直接运行,Debug 启动断点调试。不同扩展版本的按钮位置可能略有不同,以 CMake 面板中的操作名称为准;界面细节可对照 CMakePresets与构建安装。
CMake Tools 默认可能在 Launch 前自动检查并构建。只修改 XML 时,即使界面出现这一步,通常也没有 C++ 文件需要重新编译;本篇所说“直接 Launch”表示无需手动先点 Build。
本篇使用 tickRoot() 执行一次。所有示例节点都立即结束,因此一次根节点 tick 就足够。3.8 还有 tickRootWhileRunning():先 tick 一次,如果得到 RUNNING 就继续驱动,直到成功或失败。持续任务如何多次 tick,后面实现导航时再实践。
引入异常报告后的两个版本中,如果将 PrintGoal 输入改为 {unknown_goal},又没有节点写入该条目,主程序会显示读取错误并返回 2;未注册的节点名则会在创建树时报告错误。它们与电量不足时返回的树任务 FAILURE 是不同情况。
以上四个版本已按文中的增量修改顺序,在 Ubuntu 22.04 的 ysu_ros_humble 容器中,使用 C++20、CMake 3.22.1、GCC 11.4 和已安装的 BehaviorTree.CPP 3.8.7 编译运行验证,输出与示例一致。测试使用临时 Ninja 和工程副本,容器系统软件及已有工程没有升级或改动。
本篇逐步串起了“最小动作 → 条件控制 → 固定输入 → 节点间传值”。
节点之间传数据继续使用坐标数据,练习类型转换、双向端口和外部 Blackboard。