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