Wiki
第一個完整程式
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.從節點、XML 到第一個完整程式
對應 BehaviorTree.CPP 4.x 官方文件:Learn the Basic Concepts → Main Concepts、The XML schema,以及 Tutorial - Basics 中的 Your first Behavior Tree 和 Blackboard and ports。
前面已經能讀懂節點與控制規則。接下來把它們變成可執行的程式,同時補齊 C++、XML、Ports 和 Blackboard 的基礎關係。
本篇前半部分完成階段 1 的概念學習,後半部分進入階段 2 的程式設計實踐。 實踐部分在同一個獨立 C++ 工程中逐步增加功能:先執行一個最小動作,再加入電量檢查和目標傳遞。實際導航任務留到非同步動作章節實現。
2.C++ 實現節點,XML 組織節點
可以把一個程式分成三部分:
| 部分 | 負責什麼 | 例子 |
|---|---|---|
| C++ 節點 | 實現具體檢查或操作,宣告輸入輸出介面 | 檢查電量、選擇目標、列印目標 |
| XML | 描述樹的結構,配置節點引數和資料連線 | 把三個節點放進 Sequence |
| 外部程式 | 註冊節點、建立樹、驅動執行 | BehaviorTreeFactory、tickExactlyOnce() |
例如:
C++ 注册可用的节点类型
↓
工厂读取 XML,创建具体节点实例并连接成树
↓
外部 tick 根节点
↓
按树的规则调用各节点的回调
“註冊節點”是告訴工廠:遇到某個型別標識時,怎樣建立對應的 C++ 節點。它不會立即執行任務。
例如 factory.registerNodeType<PrintGoal>("PrintGoal") 將字串 PrintGoal 與 C++ 類關聯;之後 XML 中每出現一次 <PrintGoal/>,工廠就可以建立一個例項。註冊型別和建立例項是兩個步驟,同一型別可以建立多個例項。
普通自定義節點通常通過繼承實現,例如繼承 ConditionNode、SyncActionNode。簡單檢查也可以通過 registerSimpleCondition() 包裝函式或 lambda,簡單同步動作可以用 registerSimpleAction()。
樹通常在程式初始化時建立一次,後續反覆 tick,而不是每輪重新解析 XML。調整已有節點的排列或配置後,可以重新執行程式載入新的 XML;新增 C++ 節點實現時,仍要重新編譯。
3.用 XML 描述一棵完整的樹
下面是本篇逐步擴充套件後的 XML,用來認識檔案結構。實踐部分會先執行更小的樹,最後再得到這個版本:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<SelectGoal name="select_goal" goal="{mission_goal}"/>
<PrintGoal name="print_goal" goal="{mission_goal}"/>
</Sequence>
</BehaviorTree>
</root>
它對應:
Sequence
├── CheckBattery
├── SelectGoal 输出选定的目标
└── PrintGoal 读取并打印该目标
3.1.分清 XML 中幾種不同的標識
| 寫法 | 含義 |
|---|---|
<root BTCPP_format="4"> |
XML 文件的外層容器,不是可執行的行為樹節點 |
main_tree_to_execute="MainTree" |
指定預設執行哪棵樹 |
<BehaviorTree ID="MainTree"> |
定義一棵名叫 MainTree 的樹 |
<Sequence> |
這棵樹實際的根節點 |
<PrintGoal> |
註冊到工廠的節點型別標識,必須能找到對應實現 |
name="print_goal" |
當前節點例項的名稱,便於日誌和除錯,可省略 |
goal="{mission_goal}" |
把節點的 goal 埠連線到 Blackboard 的 mission_goal 條目 |
因此,樹的 ID、節點型別的註冊標識、例項的 name 和資料條目的名稱,各自有不同用途。
一個 XML 檔案可以定義多棵 <BehaviorTree>;每棵樹下面應有一個實際根節點。
只有一棵樹時,可以省略 main_tree_to_execute;
有多棵樹、需要從檔案直接確定預設入口時,應明確填寫。
XML 中的父子關係仍遵守前面的結構約束:控制節點包含一個或多個子節點,裝飾節點包含一個,條件和動作節點沒有子節點。
4.Ports 與 Blackboard:讓資料在節點之間流動
執行狀態和業務資料走的是不同的通道:
SUCCESS / FAILURE / RUNNING 告诉父节点任务的执行情况
Ports + Blackboard 传递目标、路径、观测值等业务数据
節點的輸入輸出埠類似函式介面; Blackboard 則用鍵值條目儲存資料:
| 概念 | 本例中的作用 |
|---|---|
輸出埠 OutputPort |
SelectGoal 通過 goal 埠寫出目標 |
| Blackboard 條目 | 使用鍵 mission_goal 儲存目標值 |
輸入埠 InputPort |
PrintGoal 通過自己的 goal 埠讀取目標 |
連線過程是:
SelectGoal 的输出端口 goal
↓ setOutput()
Blackboard["mission_goal"] = "loading_area"
↓ getInput()
PrintGoal 的输入端口 goal
兩個埠恰好都叫 goal,但真正把它們連線起來的是 XML 中相同的 {mission_goal}。
生產者和消費者的埠名稱可以不同,只要它們對映到同一個條目。
4.1.固定值與 Blackboard 引用
下面兩種輸入配置含義不同:
<PrintGoal goal="loading_area"/>
<PrintGoal goal="{mission_goal}"/>
第一種直接提供固定字串;
第二種讀取 Blackboard 中鍵為 mission_goal 的值。
花括號表達條目引用,不是 C++ 指標,也不會自動建立有效的業務資料。
讀取前必須有人寫入該值,例如前面的輸出節點,或者由外部程式初始化 Blackboard。引用了一個從未寫入的條目,輸入讀取就會失敗。
4.2.介面由 C++ 宣告,連線由 XML 配置
節點用靜態方法 providedPorts() 宣告埠的名稱、方向和 C++ 型別。
XML 則給埠配置固定值,或把它對映到 Blackboard。
輸入使用 getInput<T>(),輸出使用 setOutput()。
讀取可能因為缺少資料、型別或轉換問題而失敗,節點需要檢查結果;本篇選擇在配置錯誤時丟擲異常,由主程式報告。
應在執行回撥中讀取輸入,讓後續執行能取得當前值。不要只在建構函式中讀取一次,再假定這個值永遠不變。
Blackboard 可以儲存多種 C++ 型別,並不限於字串;從 XML 文本轉換自定義型別、雙向埠以及子樹的資料對映,會在後面的資料傳遞實踐中學習。
5.認識 XML 中的幾種補充寫法
5.1.簡寫與顯式寫法
下面兩種寫法指向同一種動作節點:
<PrintGoal name="print_goal" goal="loading_area"/>
<Action ID="PrintGoal" name="print_goal" goal="loading_area"/>
前者直接把註冊標識寫作標籤名;
後者用 Action 指明分類,再用 ID 指定註冊標識。
本教程以簡寫為主。
XML 中還可能出現 <TreeNodeModel>,為 Groot 等工具提供自定義節點的分類和埠描述。
它是模型資訊,不是執行分支,也不會替代 C++ 節點註冊。
執行本篇示例時不需要額外填寫它。
5.2.用 SubTree 引用另一棵樹
同一個 XML 中可以定義並複用一段行為:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence>
<CheckBattery/>
<SubTree ID="ReportTask"/>
</Sequence>
</BehaviorTree>
<BehaviorTree ID="ReportTask">
<PrintGoal goal="loading_area"/>
</BehaviorTree>
</root>
這裡先檢查電量,再執行 ReportTask。
<SubTree ID="ReportTask"/> 引用的是樹定義,不是普通 C++ 動作類;工廠在建立樹時展開並建立相應結構。
所以這個 XML 標籤本身雖然沒有巢狀內容,執行時仍連線到所引用的子樹。子樹如何傳資料,需要明確考慮其 Blackboard 範圍,後面再講。
5.3.用 include 拆分檔案
也可以在 <root BTCPP_format="4"> 中寫:
<include path="report_task.xml"/>
它把其他 XML 中的樹定義引入當前檔案,再通過 SubTree 使用。
相對路徑以包含它的 XML 檔案位置為基準;引入定義不會自動執行一遍那棵樹。
另一種方式是在 C++ 中用 registerBehaviorTreeFromFile() 載入各個檔案的樹定義,再用 createTree("MainTree") 建立選定的樹。
當前先掌握單檔案示例,多檔案組織留到任務組合時實踐。
6.入門總結
讀完以上概念部分,加上前面的入門、基本概念和控制規則,主線已經覆蓋:
| 官方部分 | 已建立的認識 |
|---|---|
Introduction to BTs |
樹結構、tick、狀態、節點分類和基本執行規則 |
Main Concepts |
可複用節點、回撥、執行時建立樹,以及 Ports/Blackboard |
The XML schema |
樹與節點標識、埠對映、簡寫與顯式寫法、子樹及檔案引用 |
Learn the Basic Concepts 到此結束。 從下面的完整工程開始,進入基礎程式設計實踐。 後面通過例子深化埠、非同步和子樹,不需要現在把所有介面一次記住。
7.從最小 demo 開始搭建工程
先把“註冊一個動作 → 讀取 XML → tick 執行”跑通,再逐步加功能。 本篇只使用一個工程目錄,按下面的順序修改同一組檔案:
| 實踐內容 | 本次新增什麼 | 能觀察到什麼 |
|---|---|---|
| 最小 demo | 一個列印資訊的動作 | C++ 動作被 XML 呼叫 |
| 電量檢查 | 一個條件和 Sequence |
電量不足時,後面的動作被跳過 |
| 固定目標 | 一個帶輸入埠的節點類 | 從 XML 讀取目標字串 |
| 目標傳遞 | 一個輸出節點和 Blackboard 對映 | 前面的節點產生目標,後面的節點讀取 |
這些例子都使用模擬資料,不需要啟動 ROS 2 或連線機器人。先執行最小版本,後續每完成一處修改,再編譯或執行對應版本。
7.1.沿用 C++ 教程中的 CMake 框架
本篇基於 CMake工程模板 建立 BT 練習版,
沿用 CMakePresets.json、公共編譯選項和 src 目錄。
相關檔案的作用可以對照閱讀:
| 本篇使用的部分 | 對應 C++ 教程 |
|---|---|
| Debug / Release 預設、Ninja 和編輯器構建 | CMakePresets與構建安裝 |
| 頂層入口、C++ 標準和公共選項 | 頂層CMake與公共編譯選項 |
| 主程式與依賴的連結 | src與lib模組CMake詳解 |
先準備 Git、支援 C++20 的編譯器、CMake 3.22 或更新版本,以及 Ninja; 工具安裝見 C++開發環境搭建與測試。 下載模板需要訪問 GitHub;行為樹依賴優先使用已安裝的 4.10.0,只有未找到對應版本時才聯網下載。
Ubuntu 22.04 可以使用系統軟體源提供的 CMake 3.22.1,當前模板已採用對應的最低版本和預設格式,直接沿用即可。 本篇使用的配置功能已受該版本支援,無需為這些練習升級 CMake。
CMake、Ninja 與編譯器應安裝在當前 Ubuntu 系統中,VSCode 的 CMake Tools 使用同一套開發工具。
7.2.安裝行為樹庫並載入 ROS 環境
如果使用已經配置 ROS 2 Humble 軟體源的 Ubuntu 22.04,在執行練習的系統或容器中開啟終端,安裝行為樹庫:
sudo apt update
sudo apt install ros-humble-behaviortree-cpp
安裝後,先在這個終端載入 ROS 2 Humble 環境:
source /opt/ros/humble/setup.bash
再從同一個終端按下一節步驟開啟工程、執行 code .,讓 VSCode 的 CMake Tools、執行和除錯程序繼承這份環境。僅在已經開啟的 VSCode 整合終端中執行 source,不會更新擴充套件程序的環境。
v4 的 apt 包名是 ros-humble-behaviortree-cpp,末尾沒有 -v4。
本教程使用 4.10.0。安裝包的實際版本取決於當前軟體源,可以用下面的命令檢視已安裝版本和候選版本:
apt-cache policy ros-humble-behaviortree-cpp
後面的工程配置會優先使用已安裝的目標版本,找不到時獲取同版本原始碼。如果當前沒有 ROS 2 環境,也可以使用後文的固定版本原始碼配置完成獨立 CMake 練習。
7.3.在 VSCode 中開啟工程並準備檔案
本篇從獲取 CMake 模板開始建立第一個行為樹工程,練習目錄命名為 bt4_first_tree。
下面的命令會建立這個資料夾,並在 VSCode 中開啟;後面逐步新增行為樹依賴、C++ 程式和 XML 檔案。
git clone https://github.com/tungchiahui/CMake_Template bt4_first_tree
cd bt4_first_tree
code .
左側資源管理器中應直接看到頂層 CMakeLists.txt 和 CMakePresets.json,說明開啟的是工程根目錄。

在擴充套件面板確認已安裝微軟的 CMake Tools 和 C/C++。 本篇通過 CMake Tools 的 Configure、Build、Launch 和 Debug 入口操作工程;斷點除錯還需要前面 C++ 環境章節準備好的 GDB。
在左側資源管理器中,右鍵工程根目錄,新建 trees 資料夾;在其中新建 mission.xml。
再在已有的 cmake 資料夾中新建 BehaviorTree.cmake。
後面直接在編輯器中填寫這些檔案,先完成配置和程式碼,再執行 Configure。

本篇參與構建和執行的檔案如下,模板中的其他演示檔案沒有列出:
bt4_first_tree/
├── CMakeLists.txt
├── CMakePresets.json 沿用 linux-debug / linux-release 预设
├── cmake/
│ ├── ProjectOptions.cmake 公共编译选项
│ └── BehaviorTree.cmake 新增:查找 4.10.0,未安装时获取源码
├── src/
│ ├── CMakeLists.txt 创建主程序、链接依赖、设置 XML 路径
│ └── main.cpp 本篇逐步扩展的 C++ 程序
└── trees/
└── mission.xml 本篇逐步扩展的树
下面先在編輯器中準備構建配置,再儲存最小程式與 XML。 完成後統一通過 CMake Tools 配置、構建和執行。
7.4.調整工程名稱並沿用模板配置
將頂層 CMakeLists.txt 裡的project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)改成project(bt4_first_tree VERSION 1.0.0 LANGUAGES C CXX),主要是把專案名改成 bt4_first_tree。
當前模板已經使用下列配置,保留原值即可:
| 檔案與位置 | 當前配置 |
|---|---|
頂層 CMakeLists.txt 的最低版本宣告 |
cmake_minimum_required(VERSION 3.22) |
CMakePresets.json 頂層的 version |
3 |
CMakePresets.json 中的 cmakeMinimumRequired |
major: 3、minor: 22、patch: 0 |
其中,預設檔案的 version 表示 JSON 格式版本,cmakeMinimumRequired 表示工程要求的 CMake 版本。
當前預設只使用格式 3 已支援的功能,說明見 CMake 3.22 預設文件。
繼續保留原有的 linux-debug / linux-release 配置、構建預設和 Ninja 生成器。
cmake/ProjectOptions.cmake 已預設使用 C++20,下面兩項同樣沿用模板原值;project_options、project_warnings 等其他配置繼續保留:
set(CMAKE_CXX_STANDARD 20)
target_compile_features(project_options INTERFACE cxx_std_20)
本篇練習主程式統一使用 C++20。連結現成的行為樹庫時不重新編譯該庫;走原始碼獲取分支時,庫按自己的構建規則使用 C++17。
7.5.集中儲存行為樹依賴配置
將以下完整內容儲存到新增的 cmake/BehaviorTree.cmake。這份配置只准備一次,後續幾輪不用反覆修改。它先查詢已安裝的 4.10.0;未找到對應版本時,再獲取同一版本的原始碼:
include_guard(GLOBAL)
# 严格使用本教程验证的版本,避免链接到另一个 4.x 版本。
find_package(behaviortree_cpp 4.10.0 EXACT QUIET CONFIG)
if(behaviortree_cpp_FOUND)
if(TARGET BT::behaviortree_cpp)
set(BTCPP_TARGET BT::behaviortree_cpp)
elseif(TARGET behaviortree_cpp::behaviortree_cpp)
set(BTCPP_TARGET behaviortree_cpp::behaviortree_cpp)
elseif(TARGET behaviortree_cpp)
set(BTCPP_TARGET behaviortree_cpp)
else()
message(FATAL_ERROR "BehaviorTree.CPP 4.10.0 was found without a supported target")
endif()
message(STATUS "Using installed BehaviorTree.CPP 4.10.0: ${behaviortree_cpp_DIR}")
else()
include(FetchContent)
set(BTCPP_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BTCPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
# 后面的 Groot2 和日志实践继续使用同一份依赖。
set(BTCPP_GROOT_INTERFACE ON CACHE BOOL "" FORCE)
set(BTCPP_SQLITE_LOGGING ON CACHE BOOL "" FORCE)
FetchContent_Declare(
behaviortree_cpp
URL https://codeload.github.com/BehaviorTree/BehaviorTree.CPP/tar.gz/refs/tags/4.10.0
URL_HASH SHA256=c758fdedb3666f7ca4c9998a4c0b243251e0a5fb5d03c99e2ee63d615af7a71d
)
# 4.10.0 上游用顶层版本生成库版本字符串,在此临时指定库版本。
set(_BTCPP_PARENT_VERSION "${CMAKE_PROJECT_VERSION}")
set(CMAKE_PROJECT_VERSION "4.10.0")
FetchContent_MakeAvailable(behaviortree_cpp)
set(CMAKE_PROJECT_VERSION "${_BTCPP_PARENT_VERSION}")
set(BTCPP_TARGET behaviortree_cpp)
message(STATUS "Building BehaviorTree.CPP 4.10.0 from source")
endif()
配置輸出包含 Using installed BehaviorTree.CPP 4.10.0 時,使用的是已安裝的精確版本;顯示 Building BehaviorTree.CPP 4.10.0 from source 時,使用固定標籤原始碼。BTCPP_TARGET 儲存本次連結的目標,後續程式不必分別維護兩種寫法。
本教程先建立獨立 CMake 程式,不需要啟動 ROS 節點。Ubuntu 22.04 在 Configure 前安裝原始碼分支所需依賴:
sudo apt update
sudo apt install build-essential cmake ninja-build git libzmq3-dev libsqlite3-dev
Groot2 釋出器需要 ZeroMQ,SQLite 日誌開關需要 SQLite 開發包。原始碼獲取分支已為後續工具實踐開啟這兩項。使用已安裝庫時,當前工程設定開關不能為現成庫補回缺失功能,需要使用構建時已開啟相應功能的 4.10.0 庫。
(看不懂也不要緊,可以忽略)如果庫來自 ROS 安裝或自定義字首,應讓 CMake Tools 繼承相應的環境,或在配置中設定 CMAKE_PREFIX_PATH。只在已開啟的整合終端執行 source 不會更新擴充套件程序的環境。本例使用的依賴包名是 behaviortree_cpp,版本固定為 4.10.0。
首次 Configure 後,可在 CMakeCache.txt 搜尋 behaviortree_cpp_DIR 或 behaviortree_cpp_SOURCE_DIR 確認來源。
4.10.0 的獨立原始碼安裝分支沒有生成供 CMake 比較版本的 ConfigVersion 檔案;這種安裝可能被 EXACT 查詢顯示為 version unknown,隨後會走固定原始碼分支,不能為方便而直接接受任意版本。 原始碼嵌入時,上游還用頂層 CMAKE_PROJECT_VERSION 生成庫的版本字串,因此本模組在新增庫的短暫範圍內指定為 4.10.0,再恢復練習工程的版本。
構建開關以 4.10.0 CMakeLists.txt 為準;本例用 BTCPP_EXAMPLES、BTCPP_BUILD_TOOLS 和 BUILD_TESTING 關閉上游示例與測試,自己的練習程式仍正常構建。
7.6.讓主程式連結行為樹庫
將 src/CMakeLists.txt 整個替換為:
include("${PROJECT_SOURCE_DIR}/cmake/BehaviorTree.cmake")
add_executable(${PROJECT_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)
target_link_libraries(${PROJECT_NAME}
PRIVATE
project_options
project_warnings
${BTCPP_TARGET}
)
# 将源码目录中的 XML 完整路径作为字符串交给程序
target_compile_definitions(${PROJECT_NAME}
PRIVATE BT_XML_PATH="${PROJECT_SOURCE_DIR}/trees/mission.xml"
)

模板中的 lib1、lib2 目錄可以保留,但這份檔案已經不再構建或連結它們;替換主程式後,本篇也不需要 Eigen 示例依賴。
BT_XML_PATH 是這份配置提供的字串,自動指向當前工程的 trees/mission.xml。
後面的 C++ 直接使用它,不需要手寫本機絕對路徑,也不必隨終端或編輯器的執行位置調整相對路徑。
7.7.儲存最小 C++ 程式
將以下完整內容儲存到 src/main.cpp,替換模板原來的演示主程式:
#include "behaviortree_cpp/bt_factory.h"
#include <iostream>
// 一个立即完成的动作:打印机器人已就绪
BT::NodeStatus ReportReady(BT::TreeNode&)
{
std::cout << "[ReportReady] robot is ready\n";
return BT::NodeStatus::SUCCESS;
}
int main()
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleAction("ReportReady", ReportReady);
// 根据 XML 创建树,再从根节点执行一次
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickExactlyOnce();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
ReportReady() 是普通 C++ 函式。
通過 registerSimpleAction() 註冊後,框架可以將它作為同步動作呼叫。
引數 BT::TreeNode& 由框架傳入,本例暫時不使用,所以沒有為它命名。
這裡先保留最小正常路徑;等到讀取埠輸入時,再加入輸入檢查和異常報告。
7.8.註冊簡單動作與建立樹的 API
BT::BehaviorTreeFactory factory; 建立一個節點工廠物件,本例不需要給建構函式傳參。
後面的註冊操作告訴工廠怎樣建立自定義節點;載入 XML 時,工廠再按註冊名建立節點例項。
下面的函式宣告用於理解介面,不用再次貼上到工程中。 原型按官方 4.10.0 bt_factory.h 核對。
7.8.1.registerSimpleAction()
函式原型:
void registerSimpleAction(
const std::string& ID,
const BT::SimpleActionNode::TickFunctor& tick_functor,
BT::PortsList ports = {});
作用:把一個回撥函式註冊成簡單同步動作,之後可以在 XML 中使用對應的節點名。
| 引數 | 型別與含義 | 本例傳入 |
|---|---|---|
第一個 ID |
字串,註冊到工廠的節點型別名稱,對應 XML 標籤 | "ReportReady",對應 <ReportReady/> |
第二個 tick_functor |
執行該節點時呼叫的函式,簽名為 BT::NodeStatus(BT::TreeNode&) |
普通函式 ReportReady |
第三個 ports |
可選的埠宣告,型別為 BT::PortsList,預設空列表 |
省略,因為這個最小動作不讀寫埠 |
第二個引數傳的是 ReportReady 函式本身,沒有呼叫括號。註冊時先儲存回撥;樹執行到這個動作時,框架才呼叫它,並傳入代表當前節點例項的 BT::TreeNode&。本例不使用這個引用,所以函式定義中只保留型別,沒有寫形參名。
返回值:void,註冊函式本身不返回任務狀態;ReportReady() 回撥返回的 BT::NodeStatus 才是節點執行結果。這個簡單動作屬於同步動作,回撥應返回 SUCCESS 或 FAILURE,不能返回 RUNNING。註冊名重複等註冊錯誤會丟擲異常。
7.8.2.createTreeFromFile()
函式原型:
BT::Tree createTreeFromFile(
const std::string& file_path,
BT::Blackboard::Ptr blackboard = BT::Blackboard::create());
作用:讀取 XML 檔案,並按照工廠已有的節點型別和 XML 結構建立一棵樹。
| 引數 | 含義 | 本例用法 |
|---|---|---|
第一個 file_path |
入口 XML 的路徑字串 | BT_XML_PATH,由 CMake 提供的完整檔案路徑 |
第二個 blackboard |
可選,指定根樹使用的 Blackboard | 本例省略,由預設引數建立;下一篇會從外部傳入 |
返回值:BT::Tree,本例用 auto tree 接收。建立時會構造節點並檢查 XML,但還沒有開始執行動作的 tick()。檔案不存在、XML 錯誤或使用未註冊節點等情況會丟擲異常。
7.8.3.tickExactlyOnce() 與 BT::toStr()
tree.tickExactlyOnce() 不接收引數,返回 BT::NodeStatus。呼叫一次,就從根節點驅動一次;一次根 tick 可能繼續執行多個立即完成的節點,也可能在某個節點返回 RUNNING 時先返回主程式。
BT::toStr(result) 的引數是需要顯示的節點狀態,返回對應的 std::string,例如 SUCCESS。它只負責把狀態變成文本;真正決定程式退出碼的仍是後面的狀態比較。
7.9.儲存只有一個動作的 XML
將以下內容儲存到 trees/mission.xml:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<ReportReady/>
</BehaviorTree>
</root>
這棵樹只有一個動作,ReportReady 就是實際根節點,不需要為了湊結構而新增 Sequence。
7.10.在 CMake Tools 中配置工程
儲存前面填寫的所有檔案,再開啟左側 CMake 面板。選擇 Configure 預設 Linux Debug,對應 CMakePresets.json 中的 linux-debug。
也可以在命令面板中執行 CMake: Select Configure Preset,選擇同一個預設。這裡使用模板已有的預設,不需要重新建立 CMakePresets.json。
點選 CMake Tools 的 Configure。 這一步讀取工程配置並準備 BehaviorTree.CPP 依賴;已安裝 4.10.0 時直接使用,否則第一次會下載原始碼。等待輸出中出現配置和生成成功的資訊。

7.11.構建並選擇執行目標
在 CMake 面板中確認 Build 預設為 linux-debug,點選 Build。等待構建完成,確認沒有編譯或連結錯誤。

在 CMake Tools 的 Launch 項中選擇 bt4_first_tree,然後點選旁邊的執行按鈕。
也可以使用 CMake Tools 提供的狀態列 Launch 入口。
這裡啟動的是整個 CMake 工程生成的執行檔。

程式輸出會顯示在 VSCode 的整合終端中。配置、構建和執行的詳細介面說明可對照 CMakePresets與構建安裝,執行入口也可參考 CMake Tools 官方說明。
預期輸出:
[ReportReady] robot is ready
Tree result: SUCCESS

執行過程是:
注册 ReportReady 对应的函数
↓
工厂读取 XML,创建 ReportReady 节点
↓
tree.tickExactlyOnce()
↓
调用 ReportReady(),打印信息并返回 SUCCESS
先看到這兩行輸出,再往下加功能。 到這裡已經完成了一個可以獨立執行的行為樹程式。
7.12.在動作回撥中設定斷點
想觀察函式什麼時候被呼叫,可以在 ReportReady() 內的 std::cout 語句左側單擊,設定一個斷點。
保持 linux-debug 和 bt4_first_tree 目標不變,點選 CMake Tools 的 Debug。

程式會在執行列印語句前暫停。此時可以檢視呼叫堆疊,按 F10 執行當前語句,再按 F5 繼續到程式結束。後面的普通執行繼續使用 Launch;這個最小示例由 CMake Tools 直接啟動除錯,不需要另外編寫 launch.json。

8.加入電量檢查,觀察 Sequence 怎樣執行
保留剛才的工程和 ReportReady()。在 src/main.cpp 中、main() 函式前面,增加下面的條件函式:
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
然後在 main() 中,已有的 registerSimpleAction() 後面、createTreeFromFile() 前面,增加這一行註冊:
factory.registerSimpleCondition("CheckBattery", CheckBattery);
8.1.registerSimpleCondition() 的引數
函式原型:
void registerSimpleCondition(
const std::string& ID,
const BT::SimpleConditionNode::TickFunctor& tick_functor,
BT::PortsList ports = {});
作用:把回撥註冊成簡單條件節點。
- 第一個引數
ID是 XML 中使用的註冊名,本例為"CheckBattery",對應<CheckBattery/>。 - 第二個引數
tick_functor是條件檢查函式,本例傳CheckBattery,簽名同樣為BT::NodeStatus(BT::TreeNode&)。收到 tick 時才執行檢查,本例用SUCCESS表示電量足夠,用FAILURE表示不足。 - 第三個引數
ports是可選埠列表。本例直接使用函式中的模擬電量,所以省略它。
返回值:void。條件是否滿足由 CheckBattery() 的返回狀態表達;註冊呼叫本身不執行這次電量檢查。
將 trees/mission.xml 整個替換為:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<ReportReady name="report_ready"/>
</Sequence>
</BehaviorTree>
</root>
樹變為:
Sequence
├── CheckBattery
└── ReportReady
儲存 src/main.cpp 和 trees/mission.xml,在 CMake Tools 中先點選 Build,成功後點擊 Launch。這次只改了 C++ 和 XML,構建配置沒有變化,沿用當前預設和執行目標即可。
#include "behaviortree_cpp/bt_factory.h"
#include <iostream>
// 一个立即完成的动作:打印机器人已就绪
BT::NodeStatus ReportReady(BT::TreeNode&)
{
std::cout << "[ReportReady] robot is ready\n";
return BT::NodeStatus::SUCCESS;
}
// 电量检查
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
int main()
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleAction("ReportReady", ReportReady);
factory.registerSimpleCondition("CheckBattery", CheckBattery);
// 根据 XML 创建树,再从根节点执行一次
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus result = tree.tickExactlyOnce();
std::cout << "Tree result: " << BT::toStr(result) << '\n';
return result == BT::NodeStatus::SUCCESS ? 0 : 1;
}
電量為 80 時,預期輸出:
[CheckBattery] battery=80
[ReportReady] robot is ready
Tree result: SUCCESS

接著在編輯器中將 CheckBattery() 的 battery_level 改為 10.0,儲存後再次點選 Build → Launch:
[CheckBattery] battery=10
Tree result: FAILURE

CheckBattery 失敗後,Sequence 不再執行 ReportReady。程式退出碼為 1,表示樹任務失敗,不是編譯錯誤。
繼續下一步前,把 battery_level 改回 80.0。 否則後面新增的動作仍然會因為電量條件失敗而被跳過。
9.將列印動作換成帶輸入埠的節點類
現在希望目標可以從 XML 配置。把原來固定列印“機器人已就緒”的動作,換成讀取並列印 goal 的 PrintGoal。
這一輪只增加輸入埠,先使用 XML 固定字串;下一輪再連線 Blackboard。
9.1.新增標頭檔案並替換動作實現
保留 bt_factory.h 和 <iostream>,再在檔案頂部增加兩個標頭檔案:
#include <exception>
#include <string>
刪除原來的 ReportReady() 函式,換成下面的完整 PrintGoal 類;
CheckBattery() 函式繼續保留,電量恢復為 80.0:
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点需要一个名为 goal 的字符串输入
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;
}
};
類裡的幾個部分各有職責:
| 部分 | 作用 |
|---|---|
繼承 BT::SyncActionNode |
表示這是立即完成的同步動作 |
| 建構函式 | 將例項名稱和配置交給基類儲存 |
providedPorts() |
宣告字串輸入埠 goal |
tick() |
讀取目標,列印後返回成功 |
9.1.1.建構函式的 name 與 config
本例建構函式的兩個引數由工廠在建立節點例項時提供:
| 引數 | 型別 | 含義 |
|---|---|---|
第一個 name |
const std::string& |
當前節點的例項名稱;XML 可以用 name 屬性設定,省略時通常使用節點的註冊名 |
第二個 config |
const BT::NodeConfig& |
當前例項的配置,包含 Blackboard,以及輸入、輸出埠的對映 |
初始化列表中的 BT::SyncActionNode(name, config) 將這兩個引數交給基類儲存,後面的 getInput() 才能按本例項的 XML 配置讀取資料。
這裡不需要在 main() 中手動呼叫 PrintGoal 的建構函式。
9.1.2.providedPorts() 與 InputPort<T>()
static BT::PortsList providedPorts() 不接收引數,返回本節點的埠宣告。它是自定義節點按約定提供的靜態函式,工廠註冊節點類時會讀取它;聲明裡儲存的是介面資訊,實際目標值由 XML 或 Blackboard 提供。
本例使用 BT::InputPort<std::string>("goal"):
| 位置 | 含義 |
|---|---|
模板引數 <std::string> |
輸入值的 C++ 型別 |
第一個函式引數 "goal" |
節點自己的輸入埠名,對應 XML 的 goal 屬性 |
| 可選的第二個函式引數 | 埠說明文字,本例省略;帶預設值的三引數形式在下一篇說明 |
返回值:一個埠名稱和 BT::PortInfo 組成的鍵值對,加入 BT::PortsList 後供工廠使用。這次呼叫宣告埠,不會立即讀取目標。
9.1.3.getInput<T>() 的引數與返回值
本例使用的原型為:
template <typename T>
BT::Optional<T> getInput(const std::string& key) const;
作用:根據當前節點的埠對映讀取輸入,並按 T 獲取或轉換資料。
- 模板引數
T是期望讀取的型別,本例為std::string。 - 唯一的函式引數
key是節點自己的埠名,本例為"goal"。XML 決定它來自固定文本還是某個 Blackboard 條目。
返回值:BT::Optional<T>,這是 BehaviorTree.CPP 使用的帶錯誤資訊的結果型別。先用 if (!goal) 判斷讀取是否失敗;有效時用 goal.value() 取得值,失敗時用 goal.error() 取得錯誤字串。
throw BT::RuntimeError("Cannot read input [goal]: ", goal.error()); 將固定提示和庫返回的錯誤資訊組成異常,再交給 main() 的 catch 顯示。BT::RuntimeError 是庫提供的異常型別,可以被 const std::exception& 捕獲。介面細節見 4.10.0 tree_node.h。
9.2.替換 main 函式
將 main() 整個替換為以下版本。這裡保留電量條件的註冊,將動作註冊改成 registerNodeType<PrintGoal>(),並新增錯誤報告:
int main()
{
try
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition("CheckBattery", CheckBattery);
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus 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;
}
}
PrintGoal 類和 CheckBattery() 都應放在 main() 前面,保證註冊時已經有對應定義。
9.2.1.registerNodeType<T>() 的引數
本例使用的原型為:
template <typename T>
void registerNodeType(const std::string& ID);
作用:將一個自定義節點類註冊到工廠,讓工廠能夠按 XML 建立該類的例項。
| 位置 | 含義 | 本例 |
|---|---|---|
模板引數 T |
C++ 中已經定義的節點類 | PrintGoal |
第一個函式引數 ID |
XML 使用的註冊名 | "PrintGoal",對應 <PrintGoal/> |
<PrintGoal> 是模板引數,("PrintGoal") 中的字串才是函式實參。這兩個名字在本例中保持一致以便閱讀;C++ 類名和 XML 註冊名分別負責不同的一層。
返回值:void。註冊時讀取這個類的埠宣告並儲存建立方式;載入 XML 時,工廠再為每個節點例項提供 name 和 config,呼叫前面說明的建構函式。
9.3.從 XML 傳入固定目標
將 trees/mission.xml 整個替換為:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<PrintGoal name="print_goal" goal="loading_area"/>
</Sequence>
</BehaviorTree>
</root>
#include "behaviortree_cpp/bt_factory.h"
#include <iostream>
#include <exception>
#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()
{
// 节点需要一个名为 goal 的字符串输入
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;
}
};
// 电量检查
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
int main()
{
try
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition("CheckBattery", CheckBattery);
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus 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;
}
}
儲存修改,在 CMake Tools 中點選 Build,成功後點擊 Launch。
預期輸出:
[CheckBattery] battery=80
[PrintGoal] target=loading_area
Tree result: SUCCESS
現在只把 XML 中的 goal="loading_area" 改為 goal="charging_station",儲存 trees/mission.xml 後再次點選 Launch。
這一步沒有改 C++,不需要手動重新 Build。
列印目標會變為 charging_station。這樣就把節點的具體實現和本次配置的目標分開了。

10.再增加輸出節點,讓兩個動作傳遞目標
固定值已經能讀到了,現在讓前面的節點產生目標,後面的節點通過 Blackboard 讀取。
保留上一輪的 PrintGoal、CheckBattery() 和 main(),只增加一個類和一行註冊。
10.1.增加 SelectGoal 類並註冊
在 PrintGoal 類前面加入:
class SelectGoal : public BT::SyncActionNode
{
public:
SelectGoal(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点通过 goal 输出选定的目标
return {BT::OutputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
const std::string goal = "loading_area";
setOutput<std::string>("goal", goal);
std::cout << "[SelectGoal] goal=" << goal << '\n';
return BT::NodeStatus::SUCCESS;
}
};
在 main() 中、註冊 PrintGoal 的前面增加:
factory.registerNodeType<SelectGoal>("SelectGoal");
工廠已有 Sequence 等內建節點;本輪只是多註冊一種自定義動作型別,不需要修改構建配置。
10.1.1.OutputPort<T>() 與 setOutput()
BT::OutputPort<std::string>("goal") 宣告字串輸出埠:模板引數指定值型別,第一個函式引數是埠名,可選的第二個引數是說明文字。返回值同樣是用於組成 BT::PortsList 的埠鍵值對。
寫入使用下面的介面:
template <typename T>
BT::Result setOutput(const std::string& key, const T& value);
| 引數 | 含義 | 本例 |
|---|---|---|
第一個 key |
當前節點自己的輸出埠名 | "goal" |
第二個 value |
要寫出的 C++ 值,型別為 T |
儲存 "loading_area" 的字串變數 goal |
XML 會把這個輸出埠連線到 Blackboard 條目;節點實現仍然只使用自己的埠名。返回值為 BT::Result,成功時可判真,失敗時可判假並呼叫 .error() 檢視原因。本篇先展示正常寫入路徑,下一篇會用 written 變數顯式檢查這個結果。
10.2.把固定值改成 Blackboard 對映
將 trees/mission.xml 整個替換為下面的最終版本:
<root BTCPP_format="4" main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<Sequence name="mission_sequence">
<CheckBattery name="check_battery"/>
<SelectGoal name="select_goal" goal="{mission_goal}"/>
<PrintGoal name="print_goal" goal="{mission_goal}"/>
</Sequence>
</BehaviorTree>
</root>
#include "behaviortree_cpp/bt_factory.h"
#include <iostream>
#include <exception>
#include <string>
class SelectGoal : public BT::SyncActionNode
{
public:
SelectGoal(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点通过 goal 输出选定的目标
return {BT::OutputPort<std::string>("goal")};
}
BT::NodeStatus tick() override
{
const std::string goal = "loading_area";
setOutput<std::string>("goal", goal);
std::cout << "[SelectGoal] goal=" << goal << '\n';
return BT::NodeStatus::SUCCESS;
}
};
class PrintGoal : public BT::SyncActionNode
{
public:
PrintGoal(const std::string& name, const BT::NodeConfig& config)
: BT::SyncActionNode(name, config)
{}
static BT::PortsList providedPorts()
{
// 节点需要一个名为 goal 的字符串输入
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;
}
};
// 电量检查
BT::NodeStatus CheckBattery(BT::TreeNode&)
{
// 修改这里的数值,可以模拟电量变化
const double battery_level = 80.0;
std::cout << "[CheckBattery] battery=" << battery_level << '\n';
if (battery_level >= 20.0)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
int main()
{
try
{
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition("CheckBattery", CheckBattery);
factory.registerNodeType<SelectGoal>("SelectGoal");
factory.registerNodeType<PrintGoal>("PrintGoal");
auto tree = factory.createTreeFromFile(BT_XML_PATH);
const BT::NodeStatus 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;
}
}
儲存修改,在 CMake Tools 中點選 Build,成功後點擊 Launch。
預期輸出:
[CheckBattery] battery=80
[SelectGoal] goal=loading_area
[PrintGoal] target=loading_area
Tree result: SUCCESS

這時再對照資料流:
SelectGoal::tick()
↓ setOutput("goal", "loading_area")
Blackboard["mission_goal"]
↓ getInput<std::string>("goal")
PrintGoal::tick()
C++ 操作各自的 goal 埠,XML 中的 {mission_goal} 決定它們連線哪個 Blackboard 條目。
SelectGoal 先寫入,PrintGoal 後讀取,執行順序由 Sequence 保證。
10.3.解釋埠名和blackboard
Blackboard 存放資料;埠名更像“函式引數名”,資料型別是另外指定的。
拿“櫃子取東西”來比喻:
- Blackboard:一個儲物櫃。
- Blackboard 鍵名:抽屜上的標籤,例如 pickup_goal。
- 資料:抽屜裡放的內容,例如 "loading_area"。
- 埠名:節點接收資料的介面名稱,例如 goal。
看這一行 C++:
BT::InputPort<std::string>("goal")
意思是:這個節點有一個叫 goal 的輸入介面,接收字串。 其中 std::string 才是型別,goal 是埠名。
再看 XML:
<NavigateToGoal goal="{pickup_goal}"/>
意思是:到 Blackboard 中找到標著 pickup_goal 的抽屜,把裡面的資料交給這個導航節點的 goal 輸入介面。
對應關係就是:
Blackboard 的 pickup_goal 抽屉
里面保存 "loading_area"
↓ XML 指定从这里取
导航节点的 goal 输入端口
↓
节点知道:这次去 loading_area
埠負責讓節點接收或寫出資料; Blackboard 負責儲存資料; XML 負責指定它們怎麼連線。
11.保持每一步的編譯、執行和路徑一致
本篇各輪實踐都沿用同一套框架配置、同一個執行檔和 trees/mission.xml:
| 改了什麼 | 接下來執行什麼 |
|---|---|
| 第一次建立工程 | 選擇 linux-debug,點選 Configure → Build → Launch |
| 修改 C++ 函式、類或註冊程式碼 | 儲存後點擊 Build → Launch |
| 只改 XML 結構或埠配置 | 儲存後再次點選 Launch |
| 修改 CMake 配置或移動工程目錄 | 點選 Configure → Build → Launch |
C++ 中使用 createTreeFromFile(BT_XML_PATH),實際 XML 路徑由 src/CMakeLists.txt 根據原始碼目錄提供。無論從工程根目錄、構建目錄,還是編輯器中啟動,程式讀取的都是原始碼中的 trees/mission.xml;只修改 XML 後重新執行即可。
這個完整路徑也避免了向解析器傳入不含目錄部分的裸檔名。本篇的路徑設定用於原始碼工程的開發和除錯,安裝釋出時的資源佈局後續按需處理;當前練習編譯後直接執行,不要求執行安裝。
本篇以 VSCode 的 CMake Tools 為操作入口:Configure 準備構建配置,Build 編譯程式,Launch 直接執行,Debug 啟動斷點除錯。不同擴充套件版本的按鈕位置可能略有不同,以 CMake 面板中的操作名稱為準;介面細節可對照 CMakePresets與構建安裝。
CMake Tools 預設可能在 Launch 前自動檢查並構建。只修改 XML 時,即使介面出現這一步,通常也沒有 C++ 檔案需要重新編譯;本篇所說“直接 Launch”表示無需手動先點 Build。
本篇使用 tickExactlyOnce() 執行一次。所有示例節點都立即結束,因此一次根節點 tick 就足夠。tickWhileRunning() 將等待與重複驅動放在庫內部,根結果不再是 RUNNING 時返回;tickOnce() 在收到喚醒訊號時可能繼續執行多次根 tick。需要精確觀察“一次呼叫對應一次根 tick”時,使用本篇的 tickExactlyOnce()。
引入異常報告後的兩個版本中,如果將 PrintGoal 輸入改為 {unknown_goal},又沒有節點寫入該條目,主程式會顯示讀取錯誤並返回 2;未註冊的節點名則會在建立樹時報告錯誤。它們與電量不足時返回的樹任務 FAILURE 是不同情況。
按照文中的增量修改順序執行四個版本,檢查正常輸出、電量不足、缺失輸入和未註冊節點等情況。分別記錄樹任務返回 FAILURE 與程式報告配置或輸入錯誤時的差異。
本篇逐步串起了“最小動作 → 條件控制 → 固定輸入 → 節點間傳值”。
节点之间传数据繼續使用座標資料,練習型別轉換、雙向埠和外部 Blackboard。