跳到正文

Wiki

v3與v4區別及遷移

約 12 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

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 基礎實踐。