Wiki
基本概念
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.Behavior Tree 的基本概念
對應 BehaviorTree.CPP 4.x 官方文檔:Learn the Basic Concepts → Introduction to BTs → Basic Concepts、Types of nodes。
理解行為樹,先把三個問題連起來:節點怎樣組成樹,tick 怎樣驅動執行,節點怎樣通過狀態報告結果。 在此基礎上,再區分控制、裝飾、條件、動作四類節點。
本文使用通用移動機械人示例。節點邏輯用 C++ 風格偽代碼表示;實際節點的繼承、註冊和 XML 配置將在完整程序中學習。
2.節點怎樣組成一棵樹
Behavior Tree,簡稱 BT,是一棵由 TreeNode 組成的層次化任務樹:
Mission [ControlNode]
├── CheckBattery [ConditionNode]
├── Inverter [DecoratorNode]
│ └── IsPathBlocked [ConditionNode]
└── NavigateToGoal [ActionNode]
樹中的位置關係是:
| 名稱 | 含義 | 圖中的例子 |
|---|---|---|
根節點 Root |
樹的執行入口,沒有父節點 | Mission |
父節點 Parent |
直接包含其他節點的節點 | Inverter 是 IsPathBlocked 的父節點 |
子節點 Child |
直接掛在另一個節點下面的節點 | Inverter 是 Mission 的子節點 |
葉子節點 Leaf |
沒有子節點的節點 | CheckBattery、IsPathBlocked、NavigateToGoal |
同一個節點可以既是子節點,又是父節點。例如 Inverter 在 Mission 下面,同時又包含 IsPathBlocked。
上層節點組織任務,葉子節點檢查條件或執行操作。葉子節點內部仍然可以調用普通函數、讀取傳感器數據或請求外部服務;“沒有子節點”只針對行為樹結構。
這裏的 Mission 是示例名稱,沒有規定具體控制規則。圖中的包含關係也沒有規定每次執行一定會經過哪些節點。
3.tick 向下傳播,狀態向上返回
3.1.tick 表示讓節點執行一次當前邏輯
官方文檔中的 “tick a node”,可以理解為調用這個節點的執行回調。
外部從根節點發起一次 tick;有子節點的節點按照自己的規則決定是否繼續傳播、傳播給誰,以及傳播多少次:
外部程序
↓ tick
根节点
↓ 按规则传播 tick
中间节点
↓ 按规则传播 tick
叶子节点
↓ 检查条件或执行操作
葉子節點返回狀態,中間節點處理子節點的結果,再向更上層返回自己的狀態:
根节点
↑ 自己组织的任务的状态
中间节点
↑ 子节点状态
叶子节点
子節點只有被父節點 tick 時,才會執行相應回調。 因此,一次根節點 tick 不等於遍歷整棵樹;樹中的某些分支可能完全沒有執行。
父節點拿到子節點結果後,也不一定原樣返回。它可以繼續執行其他子節點、轉換結果,或者報告這部分任務仍在運行,具體取決於節點規則。
3.2.和 FSM 的 update() 聯繫起來
在非阻塞 FSM 中,外部週期性調用 fsm.update(),根據當前狀態推進任務。行為樹同樣需要外部持續驅動,但內部沿樹結構組織執行。
在 BehaviorTree.CPP 4.x 中,單次驅動樹的常見入口是 tickExactlyOnce(),可對照官方 4.10.0 bt_factory.h:
// 从根节点开始一次执行,取得整棵树这次返回的状态
BT::NodeStatus status = tree.tickExactlyOnce();
實際節點執行通常經過框架的 executeTick() 入口,再進入節點自己的 tick() 等回調。
概念上的“tick 一個節點”包含了這個調用過程,不需要業務代碼繞過框架直接調用每個節點的回調。
外部如何安排下一次 tick,以及使用哪些循環接口,後面在完整程序中説明。
4.節點通過狀態報告執行結果
一個節點被 tick 後,正常通過以下三個狀態報告本次執行結果:
| 狀態 | 基本含義 | 條件節點的含義 | 動作節點的含義 |
|---|---|---|---|
BT::NodeStatus::SUCCESS |
本次執行成功結束 | 條件成立 | 動作成功完成 |
BT::NodeStatus::FAILURE |
本次執行未成功 | 條件不成立 | 動作失敗結束 |
BT::NodeStatus::RUNNING |
尚未得到最終結果 | 不允許返回 | 支持異步執行的動作仍在運行 |
例如電量不足時,CheckBattery 返回 FAILURE,表示“電量足夠”這個條件不成立;它本身不代表程序異常,也不一定讓整棵樹失敗。父節點還要決定如何處理。
對於 NavigateToGoal,返回 SUCCESS 則表示導航成功完成,不能僅因為導航請求已經發出,就報告已經成功到達。
因此,SUCCESS、FAILURE 的具體業務含義由節點職責決定,不能一概替換成布爾值 true、false。
4.1.RUNNING 表示任務未結束,當前調用先返回
導航需要時間,典型過程是:
第 1 次 tick 到导航节点:启动导航 → RUNNING
第 2 次 tick 到导航节点:检查进度 → RUNNING
第 3 次 tick 到导航节点:已经成功到达 → SUCCESS
如果最終導航失敗,最後一次結果就是 FAILURE。
SUCCESS 和 FAILURE 都是這次任務的結束結果,RUNNING 則表示尚未結束。
實現非阻塞動作時,每次回調只啓動任務或快速檢查當前進度,然後返回。RUNNING 表示任務還沒完成,不是函數一直不返回。
例如,在驅動樹的線程中這樣等待,會卡住這次執行:
// 不适合直接放进非阻塞动作的 tick() 中
while (!GoalReached())
{
// 一直等到机器人到达,导致这次 tick 无法及时返回
}
持續任務應在後續 tick 中繼續檢查,具體示例見下面的動作節點。
4.2.IDLE 是未執行或已重置的狀態
BehaviorTree.CPP 內部還有 BT::NodeStatus::IDLE,表示節點尚未開始執行,或者已被重置回初始狀態:
IDLE → 收到 tick → RUNNING → 后续 tick → SUCCESS / FAILURE
可以立即完成的節點也可能從 IDLE 直接得到 SUCCESS 或 FAILURE。
IDLE 用於表示節點生命週期中的狀態,普通執行回調不應把它作為執行結果返回。以後再次執行已經結束的任務時,還涉及框架的狀態重置;具體機制留到節點實現時學習。
5.SKIPPED 表示本次沒有執行
v4 還包含 BT::NodeStatus::SKIPPED,表示節點本次被跳過。
例如 _skipIf 條件為真時,框架不調用節點的業務回調,而是把跳過結果交給父節點。
| 狀態 | 表示什麼 |
|---|---|
SUCCESS |
執行成功,或框架按條件直接判定成功 |
FAILURE |
執行失敗,或框架按條件直接判定失敗 |
RUNNING |
本次任務尚未完成,需要後續執行 |
SKIPPED |
本次跳過,需要由父節點決定怎樣組合 |
IDLE |
尚未執行或已重置,不能作為普通執行回調的結果 |
SKIPPED 不等於 SUCCESS 或 FAILURE。例如普通 Sequence 會繼續後面的子節點,但所有子節點都被跳過時,整個序列返回 SKIPPED。自己的普通動作節點先按成功、失敗、運行三種業務結果實現;腳本與前置條件篇再實踐由框架產生的跳過。
6.四類節點分別負責什麼
| 節點類型 | 中文含義 | 直接子節點數量 | 主要職責 |
|---|---|---|---|
ControlNode |
控制節點 | 1 到 N 個 | 組織子節點,決定 tick 的傳播和整體結果 |
DecoratorNode |
裝飾節點 | 恰好 1 個 | 調整子節點的執行方式或返回結果 |
ConditionNode |
條件節點 | 0 個 | 檢查條件是否成立 |
ActionNode |
動作節點 | 0 個 | 執行具體操作 |

子節點數量只計算直接掛在當前節點下面的節點。例如開頭的 Mission 有三個直接子節點;IsPathBlocked 是它的後代,但不是直接子節點。
6.1.ControlNode:組織子節點
控制節點根據自身規則、當前狀態,以及子節點返回的結果,決定 tick 哪個子節點和自己返回什麼狀態。
它可以包含條件、動作、裝飾節點,也可以繼續包含控制節點:
Mission [ControlNode]
├── NavigateTask [ControlNode]
│ ├── CheckPathClear [ConditionNode]
│ └── NavigateToGoal [ActionNode]
└── StopRobot [ActionNode]
這樣就能將大任務拆成子任務,再由子任務組織更具體的檢查和操作。控制節點既可以處於根部,也可以嵌套在其他節點下面。
控制節點的子節點是有順序的:橫向圖通常從左到右表示,本教程的豎向樹結構從上到下表示。這個順序會參與具體控制規則,但不保證每次 tick 都執行全部子節點。
Sequence 和 Fallback 都屬於 ControlNode,具體規則下一篇集中學習。
1 到 N 个 是這一大類的數量範圍;某些具體控制節點還要求固定數量的子節點,不能把這個範圍理解為所有控制節點都接受任意數量。
6.2.DecoratorNode:包裝一個子節點
裝飾節點收到 tick 後,按自己的規則決定是否、何時、多少次 tick 唯一的子節點,並處理它的執行結果。
按照 4.x 官方裝飾節點文檔,不同裝飾節點可以轉換結果、重複執行,或中止子節點;每一種只提供自身規定的行為。
例如 Inverter 對成功、失敗取反:
Inverter [DecoratorNode]
└── IsPathBlocked [ConditionNode]
IsPathBlocked 檢查“路徑被堵住了嗎”。有障礙物返回 SUCCESS,沒有障礙物返回 FAILURE;加上 Inverter 後,這部分樹表達的判斷就變成“路徑沒有被堵住嗎”。
| 子節點返回 | Inverter 返回 |
|---|---|
SUCCESS |
FAILURE |
FAILURE |
SUCCESS |
RUNNING |
RUNNING |
SKIPPED |
SKIPPED |
取反邏輯可以寫成:
BT::NodeStatus Inverter::tick()
{
// 只展示状态转换,省略框架内部的状态维护
BT::NodeStatus child_status = child.executeTick();
if (child_status == BT::NodeStatus::SUCCESS)
{
return BT::NodeStatus::FAILURE;
}
if (child_status == BT::NodeStatus::FAILURE)
{
return BT::NodeStatus::SUCCESS;
}
// RUNNING 保持运行,SKIPPED 保持跳过;此伪代码省略 IDLE 非法状态检查。
return child_status;
}
例子中的條件節點不會返回 RUNNING;(一般執行起來需要一段時間的才可能返回就RUNNING,而一些能瞬間就知道結果的,不會返回RUNNING。)
表中保留這一行,是因為 Inverter 也能包裝支持持續執行的節點。
裝飾節點可以位於根部,也可以夾在其他節點之間。它唯一的子節點既可以是葉子,也可以是一整棵子樹的入口:
Timeout [DecoratorNode]
└── NavigateTask [ControlNode]
├── CheckPathClear [ConditionNode]
└── NavigateToGoal [ActionNode]
這裏 Timeout 仍然只有一個直接子節點,下面有多少後代不影響這個數量限制。超時、重試、重複等具體規則將在控制與裝飾規則一篇中學習。
6.3.ConditionNode:檢查條件
條件節點是葉子節點,沒有子節點。它通常讀取已有數據,快速判斷當前條件是否成立。
官方要求 ConditionNode 不應改變系統,也不能返回 RUNNING。
例如檢查機械人電量:
BT::NodeStatus CheckBattery::tick()
{
// battery_level 是已经获得的电量数据,单位为百分比
const double minimum_battery_level = 20.0;
if (battery_level >= minimum_battery_level)
{
return BT::NodeStatus::SUCCESS;
}
return BT::NodeStatus::FAILURE;
}
這個節點只判斷電量,不會順便啓動充電。
如果需要執行充電操作,應交給 StartCharging 等動作節點。
條件節點只有被 tick 時才會檢查。將它放在導航節點旁邊,不等於它會一直在後台監控電量;下一輪是否重新檢查,仍取決於父節點有沒有再次 tick 它。
如果需求是“等待直到電量足夠”,其中包含持續等待過程,應使用能表達持續執行的動作或樹結構,不能讓條件節點用 RUNNING 表示等待。
6.4.ActionNode:執行操作
動作節點也是葉子節點。它可以設置指示燈、發送停止命令,或向外部導航系統發起任務並檢查結果。
判斷和操作的區別,可以通過這兩個節點看清:
| 節點 | 類型 | 職責 |
|---|---|---|
IsGoalReached |
ConditionNode |
判斷機械人現在是否已到達目標 |
NavigateToGoal |
ActionNode |
讓機械人前往目標,並報告執行結果 |
動作分為同步與異步兩種:
| 類型 | 執行方式 | 返回狀態 |
|---|---|---|
| 同步動作 | 在當前調用內完成,樹的執行等待該調用返回 | SUCCESS / FAILURE |
| 異步動作 | 啓動任務後先返回,後續繼續檢查或推進 | 可以返回 RUNNING,最終返回 SUCCESS / FAILURE |
例如能立即完成的本地指示燈設置適合同步執行; 導航需要等待機械人移動,適合異步執行。
在 4.x 中,SyncActionNode 明確禁止返回 RUNNING,否則框架會拋出異常。
ActionNode 是本節使用的分類名稱;
實際 C++ 類的共同基類叫 ActionNodeBase,後面常用 SyncActionNode 和 StatefulActionNode 實現動作。
相關接口可對照官方 4.10.0 action_node.h。
7.用導航任務串起 tick、狀態與節點職責
下面用一個導航動作説明持續任務怎樣執行。navigation_started 表示保存在節點裏的成員狀態,初始為 false;導航接口本身應快速返回,實際移動由外部導航系統完成。
BT::NodeStatus NavigateToGoal::tick()
{
// 第一次执行时发送请求,不在每次 tick 时重复启动
if (!navigation_started)
{
StartNavigation();
navigation_started = true;
}
if (NavigationFailed())
{
navigation_started = false;
return BT::NodeStatus::FAILURE;
}
if (GoalReached())
{
navigation_started = false;
return BT::NodeStatus::SUCCESS;
}
// 任务仍在进行,当前调用先返回
return BT::NodeStatus::RUNNING;
}
這是執行語義的偽代碼,省略了請求發送失敗、取消以及框架狀態重置等實現細節。實際使用 StatefulActionNode 時,會通過 onStart()、onRunning()、onHalted() 分別組織啓動、運行檢查和中止處理。
假設父節點這次選擇了導航動作,執行過程是:
外部 tick 根节点
↓
父节点按规则 tick NavigateToGoal
↓
启动或检查导航
↓
RUNNING / SUCCESS / FAILURE
↑
父节点处理结果,返回自己的状态
↑
外部程序取得根节点的结果
導航返回 RUNNING 後,需要外部再次驅動樹,並且父節點再次選擇這個動作,執行才會繼續到達它。RUNNING 本身不會安排下一次 tick,也不會自動創建後台線程;實際工作如何繼續,由節點實現和外部系統決定。
如果上層切換任務並中止導航,動作還需要取消尚未完成的請求、清理內部狀態。
這個過程不能只靠返回 FAILURE 代替,後面實現持續任務時再展開。
回看整棵樹:控制節點組織執行,裝飾節點調整執行規則,條件節點判斷,動作節點操作。Root、Parent、Child、Leaf 則描述它們的位置關係,不是額外四種節點類型;一棵只有一個動作節點的樹,也可以直接以這個動作為根節點。
下一篇集中學習 Sequence、Fallback 和常用裝飾節點,用具體規則把這些節點組合起來。