Wiki
指令碼與前後置條件
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
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 屬性中需把 && 編碼為 && |
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。先完成該篇的執行練習,再建立指令碼練習工程:
- 在檔案管理器中複製整個
bt4_running資料夾,將副本重新命名為bt4_script。 - 刪除副本中的
build、install資料夾(如果存在),清理原工程的構建快取和結果。 - 用 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,並區分“同一個值”“同一次更新”和“仍然足夠新”。