跳到正文

Wiki

指令碼與前後置條件

約 8 分鐘閱讀

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

1.用 XML 表達資料更新與節點條件

官方資料:Scripting、Pre and Post Conditions。執行時機按 4.10.0 tree_node.cpp 和 tree_node.h 核對。

前面通過 C++ 節點讀寫 Blackboard。 本篇把簡單的資料賦值與條件判斷寫到 XML,並沿用同一個計數導航,觀察任務結束、中止和跳過的區別。 導航仍不連線機器人,回撥計數達到三次才報告成功。

2.Script、ScriptCondition 與條件屬性各管一層

<Script code="battery:=80; ready:=true; location:='loading_area'"/>
<ScriptCondition code="battery >= 30"/>

Script 是內建同步動作,code 是指令碼文本。正常賦值執行完返回成功;指令碼解析或型別錯誤會產生異常。ScriptCondition 是內建條件,表示式為真返回成功,為假返回失敗。它們已由工廠註冊,不需要再寫同名節點類。

寫法 含義
finished:=false 建立或賦值 Blackboard 變數
finished=true 對已有變數賦值;未宣告的名稱會報錯
battery >= 30 比較表示式,返回布林結果
ready && battery >= 30 兩個條件都滿足;寫在 XML 屬性中需把 && 編碼為 &amp;&amp;
location:='loading_area' 儲存字串,指令碼字串使用單引號,XML 外層使用雙引號

指令碼中的名稱通常是當前 Blackboard 的鍵;節點 XML 屬性如 goal="{mission_goal}" 中的花括號則用於埠對映,兩種語法不要混用。指令碼適合簡短的資料和策略表達,傳送導航、檔案讀寫等業務操作繼續由 C++ 實現。

3.前置條件何時檢查

屬性 條件滿足/不滿足時的行為 RUNNING 期間是否繼續檢查
_failureIf="expr" 表示式為真時,跳過業務回撥,報告 FAILURE 否
_successIf="expr" 表示式為真時,跳過業務回撥,報告 SUCCESS 否
_skipIf="expr" 表示式為真時,不執行業務回撥,報告 SKIPPED 否
_while="expr" 表示式為假時,報告 SKIPPED;已經執行的節點先被 halt 是,但仍須執行流 tick 到該節點

前三項在節點處於 IDLE 或 SKIPPED 時檢查,不能僅靠 _failureIf 持續監控正在導航的動作。4.10.0 按 _failureIf、_successIf、_skipIf、_while 的順序處理,因此同一節點上多項同時成立時,前面的結果可能先返回。開始時只給一個節點設定一種主要前置策略,更容易理解。

_while 不會建立監控執行緒。如果外部不再 tick,或者父節點不再執行到該節點,就沒有機會檢查新條件。

4.後置指令碼與 C++ 中止回撥

屬性 觸發位置
_onSuccess 節點被執行框架判定為成功時
_onFailure 節點被執行框架判定為失敗時
_post 成功或失敗後的公共指令碼;不能當作每次 tick 或所有退出路徑的 finally
_onHalted 框架 halt 該節點時

前置條件直接返回成功或失敗時,也可能觸發對應的後置指令碼;因此 _onSuccess 不證明業務回撥確實執行過。SKIPPED 不等於成功或失敗,不能假定它觸發這兩項指令碼。

_onHalted 是框架層指令碼,StatefulActionNode::onHalted() 是 C++ 業務回撥。後者負責取消外部操作;把 cancelled=true 寫進 Blackboard 只更新一條資料,不會替你取消真實導航。指令碼執行錯誤也不能成為依賴指令碼清理外部資源的理由。

5.複製持續任務工程

本篇複用本系列 持續任務與中止 中建立的 bt4_running。先完成該篇的執行練習,再建立指令碼練習工程:

  1. 在檔案管理器中複製整個 bt4_running 資料夾,將副本重新命名為 bt4_script。
  2. 刪除副本中的 build、install 資料夾(如果存在),清理原工程的構建快取和結果。
  3. 用 VSCode 新視窗開啟 bt4_script,在頂層 CMakeLists.txt 修改專案名。依賴模組、預設和 BT_XML_PATH 配置繼續使用副本中的檔案:
project(bt4_script VERSION 1.0.0 LANGUAGES C CXX)

本例不再需要 XML 中獨立的 NavigationAllowed,由 _while 檢查允許導航的值。下面完整程式保留導航節點的定義,並用指令碼條件替代原來的獨立條件節點。

5.1.src/main.cpp

#include "behaviortree_cpp/bt_factory.h"

#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>

class NavigateToGoal : public BT::StatefulActionNode
{
public:
    NavigateToGoal(const std::string& name, const BT::NodeConfig& config)
        : BT::StatefulActionNode(name, config)
    {}

    static BT::PortsList providedPorts()
    {
        return {BT::InputPort<std::string>("goal"),
                BT::InputPort<int>("ticks", 3, "完成导航所需的 onRunning 调用次数")};
    }

    BT::NodeStatus onStart() override
    {
        auto goal = getInput<std::string>("goal");
        auto ticks = getInput<int>("ticks");
        if (!goal)
        {
            throw BT::RuntimeError("Cannot read input [goal]: ", goal.error());
        }
        if (!ticks)
        {
            throw BT::RuntimeError("Cannot read input [ticks]: ", ticks.error());
        }
        if (goal.value().empty() || ticks.value() < 1)
        {
            throw std::runtime_error("goal must not be empty and ticks must be positive");
        }

        goal_ = goal.value();             // 保存本次目标,供后续回调使用
        required_ticks_ = ticks.value();  // 保存模拟完成所需的检查次数,本例为 3
        completed_ticks_ = 0;             // 每次开始新任务,都从零次检查开始
        // 真实导航通常在这里发送一次请求,本例只保存模拟任务状态。
        std::cout << "[NavigateToGoal] start goal=" << goal_ << '\n';
        // 结束本次回调并报告“尚未完成”,后续 tick 再进入 onRunning。
        return BT::NodeStatus::RUNNING;
    }

    BT::NodeStatus onRunning() override
    {
        // 每次进入这个回调,模拟检查次数就加一;函数返回后成员计数仍会保留。
        ++completed_ticks_;
        std::cout << "[NavigateToGoal] progress=" << completed_ticks_
                  << '/' << required_ticks_ << '\n';
        // 本例把“检查次数达到要求”当作完成条件,没有查询机器人的真实位置。
        if (completed_ticks_ >= required_ticks_)
        {
            std::cout << "[NavigateToGoal] arrived goal=" << goal_ << '\n';
            // 已满足模拟完成条件,向父节点报告成功,本次函数到这里结束。
            return BT::NodeStatus::SUCCESS;
        }
        // 尚未达到要求,结束本次检查,等待后续 tick 再进入这个回调。
        return BT::NodeStatus::RUNNING;
    }

    void onHalted() override
    {
        // 运行中的任务收到中止要求时,基类才会调用这个回调。
        // 真实导航在这里取消请求、停止当前任务并清理资源。
        std::cout << "[NavigateToGoal] halted goal=" << goal_
                  << ", progress=" << completed_ticks_ << '\n';
        completed_ticks_ = 0;
    }

private:
    std::string goal_;          // 本次目标
    int required_ticks_ = 0;    // 模拟完成所需的运行检查次数,由 onStart 读取
    int completed_ticks_ = 0;   // 已完成的运行检查次数,可跨多次回调保存
};

int main()
{
    try
    {
        BT::BehaviorTreeFactory factory;
        factory.registerNodeType<NavigateToGoal>("NavigateToGoal");
        factory.registerSimpleAction("ReportArrival", [](BT::TreeNode&) {
            std::cout << "[ReportArrival] mission completed\n";
            return BT::NodeStatus::SUCCESS;
        });
        auto blackboard = BT::Blackboard::create();
        blackboard->set("navigation_allowed", true);
        const bool cancel_after_two_ticks = false;
        auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);

        BT::NodeStatus result = BT::NodeStatus::RUNNING;
        int tick_count = 0;
        while (result == BT::NodeStatus::RUNNING)
        {
            ++tick_count;
            std::cout << "--- tick " << tick_count << '\n';
            result = tree.tickExactlyOnce();
            if (cancel_after_two_ticks && tick_count == 2)
            {
                blackboard->set("navigation_allowed", false);
            }
            if (result == BT::NodeStatus::RUNNING)
            {
                tree.sleep(std::chrono::milliseconds(100));
            }
        }
        std::cout << "Tree result: " << BT::toStr(result)
                  << ", finished=" << std::boolalpha << blackboard->get<bool>("finished")
                  << ", cancelled=" << blackboard->get<bool>("cancelled") << '\n';
        return result == BT::NodeStatus::SUCCESS ? 0 : 1;
    }
    catch (const std::exception& error)
    {
        std::cerr << "Error: " << error.what() << '\n';
        return 2;
    }
}

5.2.trees/mission.xml

<root BTCPP_format="4" main_tree_to_execute="MainTree">
    <BehaviorTree ID="MainTree">
        <Sequence>
            <Script code="finished:=false; cancelled:=false"/>
            <NavigateToGoal goal="loading_area" ticks="3"
                            _while="navigation_allowed"
                            _onSuccess="finished=true"
                            _onHalted="cancelled=true"/>
            <ScriptCondition code="finished"/>
            <ReportArrival/>
        </Sequence>
    </BehaviorTree>
</root>

外層普通 Sequence 在導航 RUNNING 時直接繼續當前節點,導航自己的 _while 仍會被檢查。本例的條件寫在動作上,不依賴外層 ReactiveSequence 重查一個前置葉節點。

6.正常完成與中途取消

在 CMake Tools 選擇 linux-debug,Configure、Build 後選擇 bt4_script 為 Launch 目標,再執行。 cancel_after_two_ticks=false 時,導航經過一次啟動和三次執行檢查,最後輸出:

[ReportArrival] mission completed
Tree result: SUCCESS, finished=true, cancelled=false

_onSuccess 把 finished 改為真;後面的 ScriptCondition 才允許執行報告。

截圖預留:指令碼標記正常完成。 展示 XML 的 _onSuccess、四次 tick 和最終的兩個布林值。

把取消設定改為 true,儲存並 Build、Launch。第二次根 tick 之後,主程式修改允許導航的值。第三次根 tick,_while 檢查失敗,呼叫導航的 C++ 中止回撥和框架的中止後置指令碼。結果為:

[NavigateToGoal] halted goal=loading_area, progress=1
Tree result: FAILURE, finished=false, cancelled=true

這裡導航向父節點交回的是 SKIPPED。普通 Sequence 會繼續後面的 ScriptCondition;由於 finished 仍為假,條件返回失敗,樹才最終失敗。ReportArrival 不執行。

截圖預留:條件變假後的中止。 在 onHalted() 設定斷點,展示計數仍為 1,以及最終取消標記。

7.SKIPPED 不能直接當成任務失敗

如果刪掉 <ScriptCondition code="finished"/>,取消路徑會跳過導航並繼續 ReportArrival,整個 Sequence 可能報告成功。這是樹組合規則產生的結果,不是導航已經到達。

對照修改 應觀察的行為
主示例,取消關閉 導航成功,執行到達報告
主示例,取消開啟 中止導航,完成條件失敗,不報告到達
刪掉完成條件,取消開啟 中止後仍執行報告,暴露不正確的業務組合
恢復完成條件,將 _while 改成 _failureIf="!navigation_allowed" 開始時允許,執行中變假後不會靠該屬性中止,導航仍可成功

每次只修改一項。只改 XML 時儲存後重新 Launch;改 C++ 時 Build 後 Launch。觀察完恢復主示例。

截圖預留:跳過與失敗對照。 展示刪除完成條件前後的結果,解釋為什麼不能把 SKIPPED 當作 FAILURE。

本篇的 ScriptCondition 檢查的是模擬導航的完成標記。實際系統可選擇返回失敗、取消結果或切換到取消處理分支,但必須明確哪一條路徑允許對外報告完成。

8.當前學習進度

本篇完成了 Script、條件屬性、後置指令碼與 SKIPPED 的組合實踐。接下來把外部資料放到全域性 Blackboard,並區分“同一個值”“同一次更新”和“仍然足夠新”。


v4 學習路線 · 上一篇:組合實際任務 · 下一篇:全域性Blackboard與資料更新