Wiki
持續任務與中止
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.讓導航任務跨越多次 tick
對應 BehaviorTree.CPP 4.x 官方文檔:Reactive and Asynchronous behaviors。實現細節參考 4.10.0 的 action_node.cpp、reactive_sequence.cpp 和 bt_factory.h。
前兩篇實踐分別完成了最小行為樹和節點間的數據傳遞。 如果還沒有運行,可以先完成它們,再進入本篇。 本篇仍使用模擬數據,集中解決一個問題:導航還沒結束時,節點怎樣立即返回,後面怎樣繼續檢查,以及外部條件變化後怎樣中止它。
先明確本篇的模擬規則:程序用計數器表示導航進度,約定檢查三次就算任務完成。 目標暫時用 loading_area 這樣的字符串表示。
這段程序沒有控制機械人移動,也沒有讀取機械人的實際位置;日誌中的 arrived 表示滿足了我們設定的模擬完成條件。
先用這個簡單規則看清行為樹的調用過程,後面再説明真實導航應替換哪一部分。
本例中的 ticks="3" 表示執行三次 onRunning(),不是三秒,也不是主程序只調用三次 tickExactlyOnce()。
開始任務還要單獨調用一次 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.tickExactlyOnce();
tickExactlyOnce() 沒有參數,返回這一次根 tick 的 BT::NodeStatus。
本例中,條件允許導航時,調用會沿着下面的路徑傳遞:
main() 调用 tree.tickExactlyOnce()
↓
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()。
這些規則對應 4.10.0 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.tickExactlyOnce(),都會先檢查 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.沿用配置建立持續任務工程
先完成本系列 節點之間傳數據 中的 bt4_ports 工程。本篇複製這個文件夾,將副本重命名為 bt4_running,分別保存數據傳遞與持續任務兩個練習。
在系統文件管理器中完成下面幾步:
- 打開
bt4_ports所在的父目錄,複製並粘貼整個文件夾,將副本重命名為bt4_running,讓兩個工程位於同一層目錄。 - 在新副本
bt4_running中,刪除複製過來的build和install文件夾(如果存在)。其中的構建緩存和安裝結果屬於舊工程,新工程稍後會重新生成。 - 在 VSCode 中選擇 文件 → 新建窗口,再選擇 文件 → 打開文件夾,打開新的
bt4_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。參與本篇構建的目錄為:
bt4_running/
├── CMakeLists.txt
├── CMakePresets.json
├── cmake/
│ ├── ProjectOptions.cmake
│ └── BehaviorTree.cmake
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
└── trees/
└── mission.xml
頂層 CMakeLists.txt 的項目名改為:
project(bt4_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/bt_factory.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
class NavigationAllowed : public BT::ConditionNode
{
public:
NavigationAllowed(const std::string& name, const BT::NodeConfig& 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::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()
{
// 先用 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.tickExactlyOnce();
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() 的參數與作用
前一篇的 tickWhileRunning() 在庫內部等待並重復 tick。
本篇需要在兩次 tick 之間模擬條件變化,因此把循環寫在 main() 中,並直接使用 BT::Tree 的等待接口:
void sleep(std::chrono::system_clock::duration timeout);
作用:讓驅動樹的當前線程等待一段時間,為下一次 tick 留出間隔。唯一的參數 timeout 是最長等待時長;std::chrono::milliseconds(100) 構造一個表示 100 毫秒 的時長對象,可以傳給這個參數。這個等待也可以被庫的喚醒通知提前結束。
返回值:void。tree.sleep() 本身不會執行節點,也不會創建導航線程;等待結束後,主程序仍需調用 tree.tickExactlyOnce()。後者沒有參數,返回這一次根 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 BTCPP_format="4" 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,將運行目標設為 bt4_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(),會同時拖慢整棵樹對條件變化的響應。
4.10.0 的 tickExactlyOnce() 在返回終態時還會重置根節點狀態,因此應保存它的返回值作為本次任務結果。稍後查看 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 怎樣處理同一個變化。 展示本節 v4.10.0 工程的對應操作或運行結果。
這個差別發生在一次尚未結束的任務內。
普通 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/bt_factory.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
class NavigationAllowed : public BT::ConditionNode
{
public:
NavigationAllowed(const std::string& name, const BT::NodeConfig& 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::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();
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.tickExactlyOnce();
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。注意,4.10.0 的 tickExactlyOnce() 在返回 SUCCESS 或 FAILURE 時也會把根節點保存的狀態重置為 IDLE;判斷一次 tick 的執行結果,應使用 tickExactlyOnce() 的返回值。
ReactiveSequence ← 根节点,rootNode() 指向它
├── NavigationAllowed ← 它的第一个子节点
└── NavigateToGoal ← 它的第二个子节点
8.在 VSCode 中觀察三個回調
8.1.先觀察正常完成時怎樣從啓動進入運行檢查
恢復完整程序中原來的條件更新代碼塊,將 cancel_after_two_ticks 改為 false,
XML 使用 ReactiveSequence。
#include "behaviortree_cpp/bt_factory.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string>
class NavigationAllowed : public BT::ConditionNode
{
public:
NavigationAllowed(const std::string& name, const BT::NodeConfig& 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::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();
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.tickExactlyOnce();
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.tickExactlyOnce(),幫助理解回調是怎樣被庫調用的。





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 文件組織成一個完整任務。