跳到正文

Wiki

第一個完整程序

約 36 分鐘閱讀

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

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。


v4 學習路線 · 上一篇:控制與裝飾規則 · 下一篇:節點之間傳數據