Wiki
持續任務與中止
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.讓導航任務跨越多次 tick
對應 BehaviorTree.CPP 3.8 官方文件:Reactive and Asynchronous behaviors。實現細節參考 3.8.7 的 action_node.cpp、reactive_sequence.cpp 和 bt_factory.h。
前兩篇實踐分別完成了最小行為樹和節點間的資料傳遞。 如果還沒有執行,可以先完成它們,再進入本篇。 本篇仍使用模擬資料,集中解決一個問題:導航還沒結束時,節點怎樣立即返回,後面怎樣繼續檢查,以及外部條件變化後怎樣中止它。
先明確本篇的模擬規則:程式用計數器表示導航進度,約定檢查三次就算任務完成。 目標暫時用 loading_area 這樣的字串表示。
這段程式沒有控制機器人移動,也沒有讀取機器人的實際位置;日誌中的 arrived 表示滿足了我們設定的模擬完成條件。
先用這個簡單規則看清行為樹的呼叫過程,後面再說明真實導航應替換哪一部分。
本例中的 ticks="3" 表示執行三次 onRunning(),不是三秒,也不是主程式只調用三次 tickRoot()。
開始任務還要單獨呼叫一次 onStart(),所以正常完成需要四次根 tick,後面會逐次展開。
ReactiveSequence
├── NavigationAllowed 每次根 tick 都重新检查是否允许导航
└── NavigateToGoal 开始一次任务,后续 tick 检查进度
├── 未完成:RUNNING
├── 已到达:SUCCESS
└── 被中止:执行 onHalted()
2.用三個回撥管理一個持續任務
SyncActionNode 用於一次呼叫就能給出結果的動作,不能用它返回 RUNNING。
持續任務使用 StatefulActionNode,實現下面三個回撥:
| 回撥 | 呼叫時機 | 本例做什麼 | 返回值 |
|---|---|---|---|
onStart() |
節點從 IDLE 開始執行 |
讀取目標和所需次數,將計數清零 | RUNNING、SUCCESS 或 FAILURE |
onRunning() |
當前節點為 RUNNING,再次收到 tick |
將計數加一,判斷是否達到所需次數 | RUNNING、SUCCESS 或 FAILURE |
onHalted() |
正在 RUNNING 的節點被中止 |
列印中止資訊,清零模擬計數 | void |
三個回撥都沒有函式引數,由 StatefulActionNode 在對應時機呼叫,不需要在 main() 中直接呼叫。
它們的宣告分別是 BT::NodeStatus onStart() override、BT::NodeStatus onRunning() override 和 void onHalted() override;override 表示實現基類規定的虛擬函式。
回撥通過當前節點的 getInput() 讀取輸入,通過物件成員儲存執行進度。
onStart() 返回 RUNNING 後,下一次 tick 才進入 onRunning();執行期間不會每次都重新呼叫 onStart()。
兩個有返回值的回撥都不能返回 IDLE。
StatefulActionNode 本身不會自動建立工作執行緒。
本例的回撥都在主程式驅動行為樹的執行緒裡執行,所以每次回撥要儘快返回。
真實導航可以在 onStart() 中傳送一次請求,在 onRunning() 中檢查請求結果,在 onHalted() 中發起取消。
2.1.從主程式的一次 tick 找到回撥呼叫者
主程式驅動行為樹時,呼叫的是:
result = tree.tickRoot();
tickRoot() 沒有引數,返回這一次根 tick 的 BT::NodeStatus。
本例中,條件允許導航時,呼叫會沿著下面的路徑傳遞:
main() 调用 tree.tickRoot()
↓
ReactiveSequence 从第一个子节点开始执行
↓
NavigationAllowed 检查是否允许导航
↓ 返回 SUCCESS
轮到 NavigateToGoal 节点
↓
库中 StatefulActionNode 的 tick() 检查导航节点的当前状态
├── IDLE → 调用 onStart()
└── RUNNING → 调用 onRunning()
↓
回调返回的状态沿父节点传回 main() 的 result
NavigateToGoal 繼承 BT::StatefulActionNode,因此直接使用了基類已經寫好的這套分派規則。
我們實現 onStart()、onRunning() 和 onHalted(),決定每個時機具體做什麼。
基類呼叫這些虛擬函式時,會執行當前 NavigateToGoal 物件提供的實現。
宣告中的 override 用於讓編譯器檢查是否正確覆蓋了基類虛擬函式;實際呼叫由庫的執行流程發起,不是由這個關鍵字發起。
因此,第一次執行導航節點時,節點狀態為 IDLE,進入 onStart();它返回 RUNNING 後,框架儲存這個狀態。
下一次根 tick 再次執行到導航節點,基類看到它仍為 RUNNING,才進入 onRunning()。
這些規則對應 3.8.7 StatefulActionNode 的實現。
2.2.onStart 準備一次任務,返回後把執行權交回去
先看本篇稍後要填寫的導航 XML:
<NavigateToGoal goal="loading_area" ticks="3"/>
埠 goal 接收目標字串,埠 ticks 接收模擬完成所需的執行檢查次數。
onStart() 通過 getInput<std::string>("goal") 和 getInput<int>("ticks") 讀取並檢查它們,然後執行下面的關鍵語句。
這裡先逐段理解,後面的完整程式再統一複製到工程中:
goal_ = goal.value(); // 保存本次目标:loading_area
required_ticks_ = ticks.value(); // 保存完成条件:检查三次
completed_ticks_ = 0; // 开始新任务,目前检查了零次
return BT::NodeStatus::RUNNING; // 报告任务已开始,还没有完成
| 成員變數 | 本次儲存的值 | 含義 |
|---|---|---|
goal_ |
"loading_area" |
本次導航目標 |
required_ticks_ |
3 |
達到多少次執行檢查就算完成 |
completed_ticks_ |
0 |
已經執行過多少次執行檢查 |
return RUNNING 會結束這一次函式呼叫,並把執行權交回父節點,最終回到主程式。
它報告的是“任務尚未完成”;函式返回後,沒有一個 onStart() 還在後臺繼續執行。
這幾個變數是節點物件的成員,物件仍然存在,所以儲存的目標和計數可以留到下一次回撥使用。
2.3.onRunning 每次加一,由節點自己判斷是否成功
下面只保留 onRunning() 中與計數相關的語句,便於看清完成條件:
++completed_ticks_; // 每进入一次 onRunning,就把已检查次数加一
if (completed_ticks_ >= required_ticks_)
{
return BT::NodeStatus::SUCCESS; // 达到约定次数,报告模拟任务完成
}
return BT::NodeStatus::RUNNING; // 次数还不够,等待后续 tick 再检查
++completed_ticks_ 相當於把原來的計數加一,再儲存回這個成員變數。
第一次進入 onRunning() 時,計數從 0 變成 1。接著判斷 1 >= 3,條件不成立,就跳過大括號,執行最後的 return RUNNING。
第二次進入時,計數從 1 變成 2,仍然返回 RUNNING。
第三次進入時,計數從 2 變成 3,3 >= 3 成立,執行大括號裡的 return SUCCESS,本次函式就結束了,不會再執行下面的 return RUNNING。
這裡之所以返回成功,是因為我們編寫節點時,把“計數達到三次”定義成了模擬任務的完成條件。
BehaviorTree.CPP 接收節點報告的狀態,再按樹的規則決定後續流程;它不會根據目標字串自行判斷機器人是否到達。
完整程式中的 std::cout 只負責列印日誌,列印 arrived 本身不會產生導航行為。
2.4.把四次根 tick 連起來看
先保持條件始終允許導航。主程式每次呼叫 tree.tickRoot(),都會先檢查 NavigationAllowed,再執行導航節點:
| 根 tick | 導航回撥 | 計數怎樣變化 | 完成條件 | 本次根節點返回 |
|---|---|---|---|---|
| 第一次 | onStart() |
初始化為 0 |
本次只准備任務 | RUNNING |
| 第二次 | onRunning() |
0 → 1 |
1 >= 3,不成立 |
RUNNING |
| 第三次 | onRunning() |
1 → 2 |
2 >= 3,不成立 |
RUNNING |
| 第四次 | onRunning() |
2 → 3 |
3 >= 3,成立 |
SUCCESS |
前面三次根 tick 返回 RUNNING,主程式等待一下,再發起下一次 tick。
第四次導航返回 SUCCESS,而前面的條件也已成功,ReactiveSequence 因此返回 SUCCESS;主程式發現任務完成,退出這次迴圈。
一次啟動,加上三次執行檢查,共四次根 tick。 本例兩次 tick 之間等待 100 毫秒,實際執行時間還受列印、除錯暫停和系統排程影響,計數不代表固定時長。
2.5.onHalted 處理執行中的任務被中斷
onHalted() 走的是另一條分支。當父節點要求停止導航,或主程式呼叫 tree.haltTree() 時,中止沿樹傳到導航節點。
基類用於處理中止的 StatefulActionNode::halt() 沒有引數,返回 void;它發現導航節點仍為 RUNNING,才呼叫 onHalted()。
本例在回撥裡面列印中止資訊,並把計數清零。
正常完成:
onStart() → onRunning() → onRunning() → onRunning()
RUNNING RUNNING RUNNING SUCCESS
中途中止:
onStart() → onRunning() → 收到中止要求 → onHalted()
RUNNING RUNNING
正常返回 SUCCESS 不會因此觸發 onHalted();它不是每次任務結束都執行的公共收尾函式。
onHalted() 返回 void,負責取消和清理,不向父節點返回 SUCCESS 或 FAILURE。後面的條件中止示例中,樹的 FAILURE 來自前置條件失敗及控制節點的執行規則。
如果接入真實導航,需要在這個回撥裡實現對應的取消或停止邏輯。
3.沿用配置建立持續任務工程
本篇直接複製整個 bt_ports 資料夾,再把副本重新命名為 bt_running,保留原來的資料傳遞工程供後續對照。
在系統檔案管理器中完成下面幾步:
- 開啟
bt_ports所在的父目錄,複製並貼上整個資料夾,將副本重新命名為bt_running,讓兩個工程位於同一層目錄。 - 在新副本
bt_running中,刪除複製過來的build和install資料夾(如果存在)。其中的構建快取和安裝結果屬於舊工程,新工程稍後會重新生成。 - 在 VSCode 中選擇 檔案 → 新建視窗,再選擇 檔案 → 開啟資料夾,開啟新的
bt_running。
使用的是前兩篇已經準備好的 BT 練習版配置,框架來源見 CMake工程模板。Ubuntu 22.04 可以繼續使用 CMake 3.22.1;Ninja、支援 C++20 的編譯器和 VSCode CMake Tools 的準備方式與前兩篇相同。複用 Humble 安裝的行為樹庫時,配置、執行和除錯程序仍需繼承 ROS 2 環境。
cmake、src、trees 目錄及其中的檔案都已隨工程複製過來,不需要重新建立。
稍後直接修改頂層專案名,並用本篇內容替換已有的 src/main.cpp 和 trees/mission.xml。參與本篇構建的目錄為:
bt_running/
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
│ ├── ProjectOptions.cmake
│ └── BehaviorTree.cmake
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
└── trees/
└── mission.xml
頂層 CMakeLists.txt 的專案名改為:
project(bt_running VERSION 1.0.0 LANGUAGES C CXX)
其他構建配置保持原樣,BT_XML_PATH 會自動指向這個工程的 trees/mission.xml。

3.1.src/main.cpp
用下面的完整程式替換 src/main.cpp 的原有內容,再儲存。
先保留 cancel_after_two_ticks = false,執行正常完成的情況。
#include "behaviortree_cpp_v3/bt_factory.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
class NavigationAllowed : public BT::ConditionNode
{
public:
NavigationAllowed(const std::string& name, const BT::NodeConfiguration& config)
: BT::ConditionNode(name, config)
{}
static BT::PortsList providedPorts()
{
return {BT::InputPort<bool>("allowed")};
}
BT::NodeStatus tick() override
{
auto allowed = getInput<bool>("allowed");
if (!allowed)
{
throw BT::RuntimeError("Cannot read input [allowed]: ", allowed.error());
}
std::cout << "[NavigationAllowed] allowed="
<< (allowed.value() ? "true" : "false") << '\n';
return allowed.value() ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
};
class NavigateToGoal : public BT::StatefulActionNode
{
public:
NavigateToGoal(const std::string& name, const BT::NodeConfiguration& 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()
{
// 先用 false 跑通;改为 true 后,在第二次根 tick 后禁止继续导航。(用于测试onHalted()触发)
const bool cancel_after_two_ticks = false;
try
{
BT::BehaviorTreeFactory factory;
factory.registerNodeType<NavigationAllowed>("NavigationAllowed");
factory.registerNodeType<NavigateToGoal>("NavigateToGoal");
auto blackboard = BT::Blackboard::create();
blackboard->set("navigation_allowed", true);
auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);
BT::NodeStatus result = BT::NodeStatus::RUNNING;
int tick_count = 0;
while (result == BT::NodeStatus::RUNNING)
{
std::cout << "--- tick " << ++tick_count << '\n';
// 从根驱动一次;执行到导航节点时,由基类按当前状态选择回调。
result = tree.tickRoot();
std::cout << "Root returned: " << BT::toStr(result) << '\n';
if (result != BT::NodeStatus::RUNNING)
{
break;
}
if (cancel_after_two_ticks && tick_count == 2)
{
blackboard->set("navigation_allowed", false);
std::cout << "[main] navigation is no longer allowed\n";
}
// 等待放在主循环中,节点回调本身不长时间阻塞。
tree.sleep(std::chrono::milliseconds(100));
}
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;
}
}
goal_、required_ticks_、completed_ticks_ 是節點物件的成員,能夠跨越多次回撥儲存當前任務的資料。
main() 中的 tick_count 則統計根節點執行次數,兩者含義不同。
3.2.tree.sleep() 的引數與作用
前一篇的 tickRootWhileRunning() 在庫內部等待並重復 tick。
本篇需要在兩次 tick 之間模擬條件變化,因此把迴圈寫在 main() 中,並直接使用 BT::Tree 的等待介面:
void sleep(std::chrono::system_clock::duration timeout);
作用:讓驅動樹的當前執行緒等待一段時間,為下一次 tick 留出間隔。唯一的引數 timeout 是最長等待時長;std::chrono::milliseconds(100) 構造一個表示 100 毫秒 的時長物件,可以傳給這個引數。這個等待也可以被庫的喚醒通知提前結束。
返回值:void。tree.sleep() 本身不會執行節點,也不會建立導航執行緒;等待結束後,主程式仍需呼叫 tree.tickRoot()。後者沒有引數,返回這一次根 tick 的 BT::NodeStatus。本例在結果為 RUNNING 時繼續迴圈,在成功或失敗時退出。
其餘註冊、埠和 Blackboard 介面沿用前兩篇的含義。例如 blackboard->set("navigation_allowed", true) 中,第一個引數是鍵名,第二個引數是布林值,模板型別由 true 推導為 bool;InputPort<int>("ticks", 3, "...") 的三個引數依次是埠名、預設值和說明。
3.3.trees/mission.xml
用下面的 XML 替換 trees/mission.xml 的原有內容,再儲存。
<root main_tree_to_execute="MainTree">
<BehaviorTree ID="MainTree">
<ReactiveSequence>
<NavigationAllowed allowed="{navigation_allowed}"/>
<NavigateToGoal goal="loading_area" ticks="3"/>
</ReactiveSequence>
</BehaviorTree>
</root>
普通節點埠仍然使用前一篇的寫法:條件節點從父程式寫入的 {navigation_allowed} 讀取布林值,導航節點從固定文本讀取目標和次數。
4.先觀察一次完整的導航
儲存全部檔案,在 CMake Tools 中選擇 Linux Debug,執行 Configure → Build,將執行目標設為 bt_running,再點選 Launch。本例的完整輸出為:
--- tick 1
[NavigationAllowed] allowed=true
[NavigateToGoal] start goal=loading_area
Root returned: RUNNING
--- tick 2
[NavigationAllowed] allowed=true
[NavigateToGoal] progress=1/3
Root returned: RUNNING
--- tick 3
[NavigationAllowed] allowed=true
[NavigateToGoal] progress=2/3
Root returned: RUNNING
--- tick 4
[NavigationAllowed] allowed=true
[NavigateToGoal] progress=3/3
[NavigateToGoal] arrived goal=loading_area
Root returned: SUCCESS
Tree result: SUCCESS

這組輸出對應前面的四次根 tick 表:第一次只有 start,表示進入 onStart();後面三次出現 progress,分別表示進入三次 onRunning()。
最後一次先列印 progress=3/3,隨後因為計數達到要求,列印 arrived 並返回 SUCCESS,所以主程式最後顯示 Tree result: SUCCESS。
這次完成的是我們約定的模擬任務,整個過程中不會呼叫 onHalted()。
主程式在 RUNNING 時繼續 tick,在 SUCCESS 或 FAILURE 時結束本次任務,並用 tree.sleep() 留出等待間隔。等待放在主迴圈裡,節點檢查本身保持簡短;把幾秒鐘的等待直接塞進 onRunning(),會同時拖慢整棵樹對條件變化的響應。
3.8.7 的 tickRoot() 在返回終態時還會重置根節點狀態,因此應儲存它的返回值作為本次任務結果。稍後檢視 rootNode()->status() 時可能已經是 IDLE,不能據此否定剛剛返回的 SUCCESS 或 FAILURE。
5.條件改變後中止正在執行的導航
在 main() 中將已有的設定改為:
const bool cancel_after_two_ticks = true;
儲存後 Build → Launch。主程式會在第二次根 tick 結束後,將 navigation_allowed 改成 false,模擬外部系統不再允許繼續導航。輸出為:
--- tick 1
[NavigationAllowed] allowed=true
[NavigateToGoal] start goal=loading_area
Root returned: RUNNING
--- tick 2
[NavigationAllowed] allowed=true
[NavigateToGoal] progress=1/3
Root returned: RUNNING
[main] navigation is no longer allowed
--- tick 3
[NavigationAllowed] allowed=false
[NavigateToGoal] halted goal=loading_area, progress=1
Root returned: FAILURE
Tree result: FAILURE
第三次根 tick 先檢查 NavigationAllowed。條件失敗後,ReactiveSequence 中止還在執行的導航,觸發 onHalted(),並將失敗結果返回給主程式。此時導航不會再收到第三次根 tick 對應的執行檢查,所以列印的進度仍是 1。
這次中止的先後關係為:
第二次根 tick 结束:导航仍为 RUNNING,已检查一次
↓
main() 将 Blackboard 中的 navigation_allowed 改为 false
↓ 此时只是更新数据
第三次根 tick:NavigationAllowed 读取新值,返回 FAILURE
↓
ReactiveSequence 中止正在运行的导航
↓
StatefulActionNode::halt() 调用 NavigateToGoal::onHalted()
↓
ReactiveSequence 返回 FAILURE,传回 main()
因此,寫入 false 本身沒有直接呼叫 onHalted();本例要等下一次根 tick 重新檢查條件,才會觸發中止。
onHalted() 中的日誌報告被中斷時的進度,隨後清零計數;它沒有返回 FAILURE,失敗結果由前置條件和父控制節點決定。

本例的取消只是清理模擬計數。對真實機器人,onHalted() 中還需要實現對應的取消請求或停止邏輯,行為樹本身不會替你完成裝置控制。
6.對照 Sequence 怎樣處理同一個變化
保持取消設定為 true,將 XML 中的 <ReactiveSequence> 和 </ReactiveSequence> 分別改為 <Sequence> 和 </Sequence>。
儲存 XML 後直接 Launch,無需手動重新編譯 C++。

這次雖然主程式仍會列印 navigation is no longer allowed,導航卻繼續執行到成功。
在本次任務的 RUNNING 期間,普通 Sequence 從正在執行的導航繼續,不會每次都重新檢查已經成功的前置條件。
| 本例中的行為 | Sequence |
ReactiveSequence |
|---|---|---|
| 正常導航期間檢查條件 | 開始時一次 | 每次根 tick |
| 第二次根 tick 後將條件改成 false | 導航繼續完成 | 下一次根 tick 中止導航 |
是否呼叫導航的 onHalted() |
否 | 是 |
| 這次任務的結果 | SUCCESS |
FAILURE |

這個差別發生在一次尚未結束的任務內。
普通 Sequence 結束並重置後,下一次任務仍會從前面的條件重新開始。
- Sequence:直接繼續導航,跳過前面已經成功的條件。
- ReactiveSequence:從第一個子節點重新執行,再檢查條件;如果條件變成 FAILURE,就中止正在執行的導航。
ReactiveSequence它每次都從第一個子節點開始執行,前一個成功了,才繼續下一個。 比如前面放的不是條件檢查,而是一個列印日誌的動作:
ReactiveSequence
├── PrintMessage 打印“准备导航”,然后返回 SUCCESS
└── NavigateToGoal 执行导航,未完成时返回 RUNNING
第一次 tick:打印“准备导航” → 启动导航
第二次 tick:又打印“准备导航” → 检查导航进度
第三次 tick:又打印“准备导航” → 检查导航进度
……
完成對照後,將 XML 恢復為 ReactiveSequence,再進行後面的觀察。
7.主程式主動退出時先停止樹
前面的中止由條件失敗觸發。
主程式也可以主動決定停止當前任務,並呼叫 tree.haltTree(),讓中止沿樹傳播。
保持 cancel_after_two_ticks = true,將 main() 中修改 navigation_allowed 的整個 if 程式碼塊替換為:
if (cancel_after_two_ticks && tick_count == 2)
{
std::cout << "[main] stop requested\n";
tree.haltTree();
std::cout << "Tree halted: " << BT::toStr(tree.rootNode()->status()) << '\n';
return 3;
}
#include "behaviortree_cpp_v3/bt_factory.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
class NavigationAllowed : public BT::ConditionNode
{
public:
NavigationAllowed(const std::string& name, const BT::NodeConfiguration& config)
: BT::ConditionNode(name, config)
{}
static BT::PortsList providedPorts()
{
return {BT::InputPort<bool>("allowed")};
}
BT::NodeStatus tick() override
{
auto allowed = getInput<bool>("allowed");
if (!allowed)
{
throw BT::RuntimeError("Cannot read input [allowed]: ", allowed.error());
}
std::cout << "[NavigationAllowed] allowed="
<< (allowed.value() ? "true" : "false") << '\n';
return allowed.value() ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
};
class NavigateToGoal : public BT::StatefulActionNode
{
public:
NavigateToGoal(const std::string& name, const BT::NodeConfiguration& 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();
completed_ticks_ = 0;
// 真实导航通常在这里发送一次请求,本例只保存模拟任务状态。
std::cout << "[NavigateToGoal] start goal=" << goal_ << '\n';
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;
}
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;
int completed_ticks_ = 0;
};
int main()
{
// 先用 false 跑通;改为 true 后,在第二次根 tick 后禁止继续导航。
const bool cancel_after_two_ticks = true;
try
{
BT::BehaviorTreeFactory factory;
factory.registerNodeType<NavigationAllowed>("NavigationAllowed");
factory.registerNodeType<NavigateToGoal>("NavigateToGoal");
auto blackboard = BT::Blackboard::create();
blackboard->set("navigation_allowed", true);
auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);
BT::NodeStatus result = BT::NodeStatus::RUNNING;
int tick_count = 0;
while (result == BT::NodeStatus::RUNNING)
{
std::cout << "--- tick " << ++tick_count << '\n';
result = tree.tickRoot();
std::cout << "Root returned: " << BT::toStr(result) << '\n';
if (result != BT::NodeStatus::RUNNING)
{
break;
}
if (cancel_after_two_ticks && tick_count == 2)
{
// blackboard->set("navigation_allowed", false);
// std::cout << "[main] navigation is no longer allowed\n";
std::cout << "[main] stop requested\n";
tree.haltTree();
std::cout << "Tree halted: " << BT::toStr(tree.rootNode()->status()) << '\n';
return 3;
}
// 等待放在主循环中,节点回调本身不长时间阻塞。
tree.sleep(std::chrono::milliseconds(100));
}
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;
}
}
儲存後 Build → Launch。程式會在第二次根 tick 後直接停止樹,輸出末尾為:
[main] stop requested
[NavigateToGoal] halted goal=loading_area, progress=1
Tree halted: IDLE

這次沒有再執行第三次根 tick,也沒有讓條件返回失敗。主程式主動停止任務,並列印停止後的根狀態 IDLE。示例用退出碼 3 表示主動停止;正常成功為 0,任務失敗為 1,異常報告為 2。
haltTree() 會重置節點狀態,但不會自動清空 Blackboard,也不會替你重置所有 C++ 成員。
本例在 onStart() 初始化本次進度,在 onHalted() 清理中斷時的進度,分別明確處理下一次任務的起點。
7.1.停止樹與檢視狀態的 API
上面的停止程式碼使用了三個無引數成員函式:
| 介面 | 所屬物件 | 返回值與作用 |
|---|---|---|
haltTree() |
BT::Tree |
返回 void,停止當前樹並將節點狀態重置為 IDLE;執行中的 StatefulActionNode 會進入 onHalted() |
rootNode() |
BT::Tree |
返回 BT::TreeNode*,指向根節點;空樹會返回 nullptr,本例已經成功建立樹,因此可以訪問根節點 |
status() |
BT::TreeNode |
返回 BT::NodeStatus,讀取節點當前儲存的狀態,不執行 tick |
tree.rootNode()->status() 先取得根節點指標,再讀取狀態,最後由 BT::toStr(...) 轉成字串用於列印。這裡在 haltTree() 後讀取,所以得到 IDLE。注意,3.8.7 的 tickRoot() 在返回 SUCCESS 或 FAILURE 時也會把根節點儲存的狀態重置為 IDLE;判斷一次 tick 的執行結果,應使用 tickRoot() 的返回值。
ReactiveSequence ← 根节点,rootNode() 指向它
├── NavigationAllowed ← 它的第一个子节点
└── NavigateToGoal ← 它的第二个子节点

8.在 VSCode 中觀察三個回撥
8.1.先觀察正常完成時怎樣從啟動進入執行檢查
恢復完整程式中原來的條件更新程式碼塊,將 cancel_after_two_ticks 改為 false,
XML 使用 ReactiveSequence。
#include "behaviortree_cpp_v3/bt_factory.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
class NavigationAllowed : public BT::ConditionNode
{
public:
NavigationAllowed(const std::string& name, const BT::NodeConfiguration& config)
: BT::ConditionNode(name, config)
{}
static BT::PortsList providedPorts()
{
return {BT::InputPort<bool>("allowed")};
}
BT::NodeStatus tick() override
{
auto allowed = getInput<bool>("allowed");
if (!allowed)
{
throw BT::RuntimeError("Cannot read input [allowed]: ", allowed.error());
}
std::cout << "[NavigationAllowed] allowed="
<< (allowed.value() ? "true" : "false") << '\n';
return allowed.value() ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
};
class NavigateToGoal : public BT::StatefulActionNode
{
public:
NavigateToGoal(const std::string& name, const BT::NodeConfiguration& 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();
completed_ticks_ = 0;
// 真实导航通常在这里发送一次请求,本例只保存模拟任务状态。
std::cout << "[NavigateToGoal] start goal=" << goal_ << '\n';
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;
}
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;
int completed_ticks_ = 0;
};
int main()
{
// 先用 false 跑通;改为 true 后,在第二次根 tick 后禁止继续导航。
const bool cancel_after_two_ticks = false;
try
{
BT::BehaviorTreeFactory factory;
factory.registerNodeType<NavigationAllowed>("NavigationAllowed");
factory.registerNodeType<NavigateToGoal>("NavigateToGoal");
auto blackboard = BT::Blackboard::create();
blackboard->set("navigation_allowed", true);
auto tree = factory.createTreeFromFile(BT_XML_PATH, blackboard);
BT::NodeStatus result = BT::NodeStatus::RUNNING;
int tick_count = 0;
while (result == BT::NodeStatus::RUNNING)
{
std::cout << "--- tick " << ++tick_count << '\n';
result = tree.tickRoot();
std::cout << "Root returned: " << BT::toStr(result) << '\n';
if (result != BT::NodeStatus::RUNNING)
{
break;
}
if (cancel_after_two_ticks && tick_count == 2)
{
blackboard->set("navigation_allowed", false);
std::cout << "[main] navigation is no longer allowed\n";
return 3;
}
// 等待放在主循环中,节点回调本身不长时间阻塞。
tree.sleep(std::chrono::milliseconds(100));
}
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;
}
}
選擇 linux-debug,先 Build,然後在下面幾行設定斷點,點選 CMake Tools 的 Debug:
| 斷點位置 | 暫停時看什麼 | 對應前面的講解 |
|---|---|---|
onStart() 中列印 start 的語句 |
goal_ 為 "loading_area",required_ticks_ 為 3,completed_ticks_ 為 0 |
第一次根 tick 只准備任務 |
onRunning() 中的 ++completed_ticks_ |
第一次暫停時計數為 0,按 F10 執行這一行後變成 1 |
每次執行檢查只把計數加一 |
onRunning() 中的 if (completed_ticks_ >= required_ticks_) |
三次到這裡時,計數依次為 1、2、3,要求始終為 3 |
前兩次條件不成立,第三次成立 |
斷點暫停在某一行時,先看執行這一行之前的值;F10 表示單步執行,F5 表示繼續執行到下一個斷點。
同一次 onRunning() 中,先執行加一,再到達 if 判斷,所以在這兩個位置看到的計數會不同。
第一次和第二次到達 if 時,按 F10 會跳過成功分支,執行最後的 return RUNNING。
第三次到達 if 時,計數已經為 3;繼續單步會進入列印 arrived 和 return SUCCESS 的分支。
它返回後,主程式拿到本次根 tick 的 SUCCESS,結束迴圈。
在回撥斷點暫停時,還可以展開 VSCode 的 呼叫堆疊(CALL STACK),檢視是誰呼叫了當前回調。
從當前回撥沿呼叫者向外看,可以對應到導航節點的執行、父控制節點,以及 main() 中的 tree.tickRoot(),幫助理解回撥是怎樣被庫呼叫的。



8.2.再觀察執行中的任務怎樣被中止
結束這次除錯,將 cancel_after_two_ticks 改為 true,儲存後 Build。
保留 onStart() 和 onRunning() 的斷點,並在 onHalted() 列印中止資訊的語句處增加斷點,再次點選 Debug。
這次先進入 onStart(),隨後只進入一次 onRunning(),將計數加到 1。
第二次根 tick 結束後,主程式把允許標誌改為 false;繼續執行,第三次根 tick 會因為條件失敗而進入 onHalted(),此時仍能看到目標為 loading_area、計數為 1。
在清零語句上單步後,計數變成 0。這次執行不會再走到 3 >= 3 的成功分支。
| 執行過程 | 實際觀察到的回撥 |
|---|---|
| 關閉取消 | onStart() 一次,onRunning() 三次,最後返回 SUCCESS |
| 開啟取消 | onStart() 一次,onRunning() 一次,隨後進入 onHalted(),本次根 tick 返回 FAILURE |

要觀察本篇的取消邏輯,使用條件變化或 haltTree() 的對照。
VSCode 的“停止除錯”控制的是除錯程序,不能用它來保證程式一定執行到 onHalted()。
全部觀察完成後,恢復最初的完整程式和 XML,再將取消設定改回 false。
執行時依次檢查正常完成、條件中止、普通 Sequence 對照、主動停止和無效次數,並結合斷點觀察回撥順序與節點狀態。
9.接入真實導航時替換什麼
本篇用 completed_ticks_ >= required_ticks_ 作為成功條件,是為了讓回撥過程容易觀察。
接入真實導航時,應把這個計數模擬換成導航系統的請求和結果檢查:
| 回撥 | 真實導航中對應的工作 |
|---|---|
onStart() |
傳送一次導航請求,儲存後續檢查需要的任務資訊 |
onRunning() |
檢查該請求的最新結果;仍在執行就返回 RUNNING,確認到達後返回 SUCCESS,任務失敗時返回 FAILURE |
onHalted() |
對仍在執行的請求發起取消,並處理當前任務需要的清理 |
成功條件由節點實現決定,再通過返回值告訴行為樹。
因此,理解本篇程式碼時,要把“庫根據狀態選擇哪個回撥”和“節點根據什麼條件報告成功”分開看:前者由 StatefulActionNode 提供,後者由我們編寫的導航節點實現。
10.當前學習進度
本篇補齊了持續動作的生命週期、主迴圈驅動和中止處理。
下一篇组合实际任务複用持續導航,把電量檢查、子樹資料對映、有限重試和多個 XML 檔案組織成一個完整任務。