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