Wiki
v3与v4区别及迁移
1.从 BehaviorTree.CPP 3.8.7 迁移到 4.10.0
官方资料:Migration from BT.CPP 3.x、4.10.0 源码、版本更新记录。官方迁移网页当前标注 4.8,本篇进一步对照 4.10.0 的声明与实现。
本篇承接已经完成的 v3 基础实践,目标是迁移独立 CMake 程序。先完成最小工程,再迁移组合任务,最后使用 Groot2。示例继续使用 C++20、VSCode 和模拟移动机器人数据。
迁移需要同时检查三件事:能否编译、能否加载 XML、实际执行的目标和状态是否符合原任务。 没有异常不代表语义没有改变,子树固定字符串就是容易漏掉的例子。
2.版本变化先看任务层面
| 项目 | 本组 v3.8.7 写法 | 本组 v4.10.0 写法 |
|---|---|---|
| CMake 包名 | behaviortree_cpp_v3 | behaviortree_cpp |
| 头文件目录 | behaviortree_cpp_v3/ | behaviortree_cpp/ |
| 节点配置类型 | BT::NodeConfiguration | BT::NodeConfig |
| 精确一次根 tick | tree.tickRoot() | tree.tickExactlyOnce() |
| 库内部驱动 RUNNING | tree.tickRootWhileRunning() | tree.tickWhileRunning() |
| 可处理唤醒的单次调用 | 本组未采用此形式 | tree.tickOnce(),内部可能多次根 tick |
| XML root | 不需要 v4 格式声明 | BTCPP_format="4" |
| 普通 SubTree 传父键 | target="pickup_goal" | target="{pickup_goal}" |
| 节点状态 | IDLE、RUNNING、SUCCESS、FAILURE | 增加 SKIPPED,需要检查父节点的组合结果 |
| 图形实时连接 | PublisherZMQ 配 Groot1 | Groot2Publisher 配 Groot2 |
| 本组状态文件日志 | FileLogger,.fbl | FileLogger2,.btlog |
节点、Ports、Blackboard、注册工厂以及 StatefulActionNode 的基本分工仍然沿用。端口属于节点,Blackboard 保存键值,XML 决定映射;这层认识不需要推倒重学。
3.先保留原工程,再迁移构建
复制整个 v3 的 bt_first_tree 为 bt4_first_tree,删除副本 build/install,用 VSCode 新窗口打开。修改顶层 project 名称,保留公共 project_options、project_warnings 和 linux-debug/linux-release 预设。
将副本 cmake/BehaviorTree.cmake 替换为 v4 第一个完整程序 中的完整版本。它精确查找 4.10.0,找不到时获取同一标签源码,并开启后续工具实践需要的接口。
src/CMakeLists.txt 继续链接 ${BTCPP_TARGET},继续由 CMake 设置 BT_XML_PATH;顶层不是把整个模板替换成上游库的 CMakeLists.txt。
| 检查项 | 迁移后的要求 |
|---|---|
| 主程序标准 | 继续使用 C++20 |
| 上游源码标准 | 4.10.0 库使用 C++17;与主程序的标准设置分开理解 |
| 版本来源 | Configure 显示精确安装包或固定源码来源,不能混用不确定版本 |
| 构建缓存 | 在新工程重新 Configure,不能继续使用指向旧路径的缓存 |
| 运行库 | 启动与调试应能找到当前链接的动态库 |
上游独立源码安装可能缺少 ConfigVersion 文件,EXACT 查找会显示 version unknown 并走源码分支;这是版本元信息问题,不能据此放宽成“任意 v4 都能用”。源码作为子工程加入时,4.10.0 用顶层 CMAKE_PROJECT_VERSION 生成版本字符串,教程依赖模块也处理了这一点。
4.头文件与类型逐项改
// v3
#include "behaviortree_cpp_v3/bt_factory.h"
PrintGoal(const std::string& name, const BT::NodeConfiguration& config);
// v4
#include "behaviortree_cpp/bt_factory.h"
PrintGoal(const std::string& name, const BT::NodeConfig& config);
4.10.0 仍保留 NodeConfiguration 的类型别名,所以旧构造参数不一定立即报错;新代码统一采用 NodeConfig。能通过兼容别名编译,只说明这一处名字可用,不代表 XML、执行接口和日志已经完成迁移。
| 其他旧名称 | v4 常用名称 | 注意 |
|---|---|---|
| AsyncActionNode | ThreadedAction | 不等同于推荐把所有节点改成线程动作;本组继续采用 StatefulActionNode |
| Optional | Expected | 用值或错误描述输入读取结果,不是仅表示“可能没有值” |
| SequenceStar | SequenceWithMemory | 4.10.0 在失败和 halt 后仍保留索引,不能假定 halt 总会从头开始 |
| SubTreePlus | SubTree | v4 普通 SubTree 使用带花括号的映射语法 |
部分旧名称受 USE_V3_COMPATIBLE_NAMES 构建选项控制,不能假定不同安装包都开启它。教程迁移使用新名称;自己的插件也应逐个重新编译。
5.用最小程序确认新版本
将以下完整内容保存为迁移副本的 src/main.cpp。它将库版本与执行结果一起打印,不依赖旧节点的实现。
#include "behaviortree_cpp/bt_factory.h"
#include <iostream>
#include <string>
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
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;
}
};
int main()
{
try
{
std::cout << "BT.CPP version: " << BT::LibraryVersionString() << '\n';
BT::BehaviorTreeFactory factory;
factory.registerNodeType<PrintGoal>("PrintGoal");
auto blackboard = BT::Blackboard::create();
blackboard->set("pickup_goal", std::string("loading_area"));
auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);
const auto result = tree.tickExactlyOnce();
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;
}
}
将 trees/mission.xml 替换为:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<SubTree ID="PrintTask" target="{pickup_goal}"/>
</BehaviorTree>
<BehaviorTree ID="PrintTask">
<PrintGoal goal="{target}"/>
</BehaviorTree>
</root>
Configure、Build,选择 bt4_first_tree 作为 Launch 目标,再运行:
BT.CPP version: 4.10.0
[PrintGoal] target=loading_area
Tree result: SUCCESS
LibraryVersionString() 没有参数,返回库提供的版本字符串指针,用于输出诊断信息,不执行节点。这个结果还应与固定源码来源对应,不能只凭一个可修改的字符串证明所有构建来源正确。
截图预留:最小迁移程序。 展示 Configure 的依赖来源、版本输出和传入的正确目标。
6.SubTree 是最需要检查的 XML 边界
本组 v3 组合任务使用普通 SubTree,它直接将属性值作为父键名:
<!-- v3.8.7 普通 SubTree -->
<SubTree ID="NavigateLeg" target="pickup_goal" reached="pickup_reached"/>
迁移到 v4 后,应明确使用 Blackboard 引用:
<!-- v4.10.0 -->
<SubTree ID="NavigateLeg" target="{pickup_goal}" reached="{pickup_reached}"/>
| v4 写法 | 实际含义 |
|---|---|
| target="{pickup_goal}" | 子树 target 映射到父树 pickup_goal |
| target="loading_area" | 子树 target 得到固定字符串 loading_area |
| target="pickup_goal" | 子树 target 得到固定字符串 pickup_goal,不读取父树变量 |
在最小迁移示例中去掉花括号,再运行,会打印 target=pickup_goal,程序仍可能成功退出。这一对照说明为什么迁移验证要检查业务数据。
输出端口也必须正确映射回父树。遗漏 reached 映射,子树可以写入自己的局部键,但主树未必有结果;不能因为两侧键名相同就认为共享。
多层子树仍然各有 Blackboard。v4 的 _autoremap="true" 可按约定自动映射,但本组迁移先写出必要输入输出,方便审核边界。全局 @ 访问也不应作为批量替代所有映射的办法。
截图预留:不报异常的错误目标。 对照带花括号与不带花括号时 PrintGoal 的实际输出。
7.tick 接口先确定外部需要控制什么
// 需要一次外部调用严格对应一次根 tick,例如在两次调用间修改外部数据。
const auto result = tree.tickExactlyOnce();
// 允许处理本次执行产生的唤醒信号,内部可能多次 tick 根节点。
const auto result_with_wakeup = tree.tickOnce();
// 在库内部重复驱动,根结果不再是 RUNNING 时返回。
const auto completed_result = tree.tickWhileRunning(std::chrono::milliseconds(100));
三者都返回 BT::NodeStatus。前两者没有参数;tickWhileRunning 的可选参数是等待时长,默认 10 毫秒。唤醒信号可使等待提前结束,参数不是严格的固定周期。
本组计数导航为了对照“一次 onStart 加三次 onRunning”,使用 tickExactlyOnce;主程序在两次调用之间更新取消条件。使用 tickWhileRunning 会把循环交给库,如果同一线程要在第二次 tick 后更新数据,就必须重新安排这段调度逻辑。
Retry、Repeat 等节点也可能发出唤醒。一处 API 替换后,终态结果可能相同,但“一次外部函数调用推进了几次根 tick”可能变化。不要拿终端打印的次数直接当成固定运行时间。
4.10.0 在成功或失败的根 tick 后同样会重置根状态到 IDLE。判断本次结果应保存返回值,不能稍后只读取 rootNode()->status()。
8.SKIPPED 与持续动作的关系
v4 的 SKIPPED 表示本次未执行。普通 Sequence 遇到它会继续后面的步骤,全部被跳过时整个序列也会返回 SKIPPED。因此:
Sequence
├── NavigateToGoal [_while="navigation_allowed"]
└── ReportArrival
条件变假时导航被 halt,随后可能跳过并继续报告到达。这个结构不满足“只有真实完成才能报告”的需求。应检查完成标志、明确转换任务结果,或选择原来的 ReactiveSequence+失败条件结构。
_failureIf、_successIf 和 _skipIf 不在 RUNNING 期间反复检查;_while 才会在执行到运行节点时继续检查。不能把它们全部当成旧条件节点的持续监控替代。完整对照见 脚本与前后置条件。
已有 StatefulActionNode 的三个回调仍保留各自职责:onStart 发送一次请求,onRunning 查询进展,onHalted 取消与清理。用脚本写一个 cancelled=true 不会替代取消外部 Action 的实现。
9.迁移已有组合任务
复制 v3 的 bt_mission 为 bt4_mission,清理副本 build/install,按下面逐项修改。完整迁移结果在 v4 组合实际任务,可以对照其完整 C++、mission.xml 和 navigation.xml。
| 文件 | 修改 |
|---|---|
| 顶层 CMakeLists.txt | 改项目名为 bt4_mission |
| cmake/BehaviorTree.cmake | 使用本组精确版本的 v4 依赖配置 |
| src/main.cpp | 头文件换为 behaviortree_cpp,配置类型采用 NodeConfig,驱动采用 tickExactlyOnce |
| trees/mission.xml | root 增加 BTCPP_format;两次 SubTree 的目标和结果都采用花括号引用 |
| trees/navigation.xml | root 增加 BTCPP_format,保留导航端口与有限重试规则 |
先保持业务参数与旧工程一致:电量20、两次总尝试、第一次导航失败。应仍然观察到充电、取货点第二次成功、放货点第二次成功,并读回两个结果。
| 对照场景 | 迁移后应保留的任务行为 |
|---|---|
| 电量80 | 不执行模拟充电 |
| fail_attempts=0 | 两段导航各第一次成功 |
| num_attempts=1,fail_attempts=1 | 第一段耗尽,执行失败报告,不开始第二段 |
| fail_attempts=2,最多两次 | 尝试耗尽,任务 FAILURE |
| 去掉目标映射花括号 | 暴露固定字符串目标错误,即使返回 SUCCESS 也不能通过验收 |
| 遗漏结果映射 | 父树缺少结果时报告数据错误 |
调试时比较两段 NavigateLeg 的 this 指针与 attempt_:子树定义复用,但节点对象与未映射的局部数据独立。只比较最终一个 SUCCESS 字符串,会漏掉目标、重试次数和数据流变化。
截图预留:组合任务迁移前后。 对照两段目标、重试次数及主树读取到的结果。
10.从 Groot1 接到 Groot2
v3 本组使用 PublisherZMQ 和 FileLogger,迁移后的工具篇使用 Groot2Publisher 与 FileLogger2:
#include "behaviortree_cpp/loggers/groot2_publisher.h"
#include "behaviortree_cpp/loggers/bt_file_logger_v2.h"
// 放在 tree 创建之后,并与整个执行过程保持相同作用域。
BT::Groot2Publisher publisher(tree, 1667);
BT::FileLogger2 file_logger(tree, BT_LOG_PATH);
发布器第二个参数是服务端口,不是旧发布器的频率;还需要后一号 TCP 端口。日志路径应使用 .btlog 后缀,旧 .fbl 不能靠改文件名转换。
Groot2 的编辑、监控与回放需要分别验证。编辑器能打开 XML,不等于程序已经发布实时状态;实时能连接,也不等于保存下来的日志能完整回放。完整安装、模型和成功/中止两条路径见 Groot2 教程。
Groot2 的编辑器也支持 BT3 XML,但本篇迁移目标统一采用 v4.10.0 的原生接口,不把两代监控协议混在同一个示例中。
11.独立工程与 ROS 2 系统迁移的边界
完成本篇证明独立示例已迁移。实际 ROS 2/Nav2 系统还需要检查:执行器链接的库、节点插件构建时使用的头文件与 ABI、插件导出和加载方式、现有 XML 的自定义节点、配置及取消处理。
v3 插件动态库不能直接当作 v4 插件加载。不同库版本在磁盘上共存,也不代表能在同一个进程中任意混用。已有 Nav2 应按自身分支和依赖构建,独立 v4 练习不要覆盖其运行库。
本篇迁移学习到此结束。已经学完 v3 时,接下来重点运行 v4 的脚本、全局数据与 Groot2;需要查阅完整程序时,按各篇主题回看 v4 基础实践。