跳到正文

Wiki

基本概念

約 12 分鐘閱讀

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

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 和常用裝飾節點,用具體規則把這些節點組合起來。


v4 學習路線 · 下一篇:控制與裝飾規則