Wiki
脚本与前后置条件
Existing English translations were reused. 39 current source blocks remain in Simplified Chinese, without triggering paid translation.
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,并区分“同一个值”“同一次更新”和“仍然足够新”。