Skip to content

Wiki

v3与v4区别及迁移

About 12 min read

Existing English translations were reused. 58 current source blocks remain in Simplified Chinese, without triggering paid translation.

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 基础实践。