行为树
1.基本概念
1.1 简介
行为树是控制“任务”执行流的分层节点树。
树节点的类型 | 子节点计数 | 笔记 |
---|---|---|
控制节点 | 1...N | 通常,根据其兄弟姐妹或/和自己的状态的结果勾选孩子。 |
装饰器节点 | 1 | 除其他外,它可能会改变孩子的结果或多次勾选它。 |
条件节点 | 0 | 不应更改系统。不得返回运行。 |
动作节点 | 0 | 这是“做某事”的节点 |
注:条件节点和动作节点均为叶子节点。
1.2 基础知识
我们需要写叶子节点(条件节点和动作节点),将他们用逻辑组装成一棵树。这棵树除了利用C++在运行的时候组装,也可以使用 XML 文件直接再部署的时候组装。
每个节点都有回调函数,可以通过回调函数执行我们的代码。
// The simplest callback you can wrap into a BT Action NodeStatus HelloTick() { std::cout << "Hello World/n"; return NodeStatus::SUCCESS; } // Allow the library to create Actions that invoke HelloTick() factory.registerSimpleAction("Hello", std::bind(HelloTick));
上述例子用的是 函数指针 ,通常我们使用继承的方式来定义树的节点。一般继承 TreeNode
, 特别的,我们也可以继承 ActionNodeBase
, ConditionNode
和 DecoratorNode
.
2.节点库
2.1 序列:( sequence )
勾选所有子节点,只有所有子节点军返回 SUCCESS 才返回 SUCCESS ,否则返回 FAILURE,过程中为 RUNNING。
共有三种类型,分别如下:
Type of ControlNode | Child returns FAILURE | Child returns RUNNING |
---|---|---|
Sequence | Restart | Tick again |
ReactiveSequence | Restart | Restart |
SequenceWithMemory | Tick again | Tick again |
-
"Restart"意思是从第一个子节点重新开始。
-
"Tick again" : 意思是下一次序列被tick的时候 ,从该节点开始tick,之前的SUCCESS节点不再重复。
Sequence适合狙击手逻辑,ReactiveSequence适用于连续tick ,但是要判断执行时间和tick帧率,SequenceWithMemory适合各个点只执行一次。
2.2 装饰节点 (Decorators)
一个装饰节点只有一个孩子,它可以控制是否,什么时候tick孩子节点多少次。
InvertNode : tick孩子节点一次,孩子节点失败返回SUCCESS,成功返回FAILURE,孩子节点运行则返回RUNNING。
ForceSuccessNode : tick孩子节点一次,孩子节点失败或者成功均返回SUCCESS,孩子节点运行则返回RUNNING。
ForceFailureNode : tick孩子节点一次,孩子节点失败或者成功均返回FAILURE,孩子节点运行则返回RUNNING。
RepeatNode : 最多tick孩子节点n次,(n作为数据输入),直到孩子节点返回失败,则该节点返回FAILURE,若孩子节点返回RUNNING ,则同样返回RUNNING。
RetryNode:最多tick孩子节点n次 , (n 作为数据输入),直到孩子节点返回成功,则该节点返回 SUCCESS ,若孩子节点返回RUNNING ,则同样返回RUNNING。
2.3 回退(FallBack)
可称之为“选择器”或“优先级”,用于尝试不同策略:1.子项返回FAILURE , 回退并勾选下一个子项。2.若最后一个子项也返回FAILURE,则所有子项停止并回退返回失败。 3.如果有子项返回SUCCESS,停止并返回SUCCESS。
有两个类型:
Type of ControlNode | Child returns RUNNING |
---|---|
Fallback | Tick again |
ReactiveFallback | Restart |
ReactiveFallback : 用于中断某异步子节点。
3.接口(Ports) 和 键值对(Blackboard)
Blackboard : 可以被所有节点共享的键值对(entry)。
Ports : 节点间相互通讯的接口。
Ports 通过 blackboard 的相同的键连接, 输入ports 可以读取 entry , 而输出 Ports 可以写入 entry。
Ports 的数量,名称和类型在编译阶段就需要被知道,ports之间的连接在XML部署的时候完成。
可以通过值储存任意的C++类型。
注:1.输出的时候在文件中用 setOutput(“A”,”B”);
其中A为接口,B为数据.
然后在 XML 中将接口与键值对的 键 相连接。A="{C}"
其中A为接口
2.输入时 InputPort<T>("A")
做声明
通过 getInput<T>("A")
获取键值对的值
输出是往接口输出 , 输入是从接口读入 。
3.1 Inputs ports
一个有效的输入应该是: 1. 节点将会读取的一段静态字符串 2. 由键定义的对应Blackboard条目的指针。
如下:
<SaySomething name="fist" message="hello world" /> #法一:一段静态字符串 <SaySomething name="second" message="{greetings}" /> #法二:条目“greeting”对应的在blackboard中的指针
注:条目“greeting”对应的值可能在运行期间发生改变。
ActionNode 例子:
// SyncActionNode (synchronous action) with an input port. class SaySomething : public SyncActionNode { public: //如果您的节点具有接口,则必须使用此构造函数标记 // If your Node has ports, you must use this constructor signature SaySomething(const std::string& name, const NodeConfig& config) : SyncActionNode(name, config) { } //必须定义此静态方法 // It is mandatory to define this STATIC method. static PortsList providedPorts() { // This action has a single input port called "message" return { InputPort<std::string>("message") }; } // Override the virtual function tick() NodeStatus tick() override { Optional<std::string> msg = getInput<std::string>("message"); // Check if optional is valid. If not, throw its error if (!msg) { throw BT::RuntimeError("missing required input [message]: ", msg.error() ); } //使用方法 value() 提取有效信息。 // use the method value() to extract the valid message. std::cout << "Robot says: " << msg.value() << std::endl; return NodeStatus::SUCCESS; } };
1.当自定义的节点具有输入输出端口时,这些端口必须是在静态方法中声明。
static MyCustomNode::PortsList providedPorts();
2.使用模板方法读取来自端口的输入。
TreeNode::getInput<T>(key)
, 建议在 tik() 中使用,在运行时周期性的进行更改。
3.失败返回的情况很多,具体要怎么操作需要我们自己去判断确定。
3.2 Output ports
Input ports 要起作用必须用Output Ports 对 blackboard进行写入,使用对应的条目进行读取。
以下是例程:
class ThinkWhatToSay : public SyncActionNode { public: ThinkWhatToSay(const std::string& name, const NodeConfig& config) : SyncActionNode(name, config) { } static PortsList providedPorts() { return { OutputPort<std::string>("text") }; } // This Action writes a value into the port "text" NodeStatus tick() override { // the output may change at each tick(). Here we keep it simple. setOutput("text", "The answer is 42" ); return NodeStatus::SUCCESS; } };
注:写入只需要 接口名 和 写入的数据 ,键值对对应的键名不需要在此声明,直接在xml文件中声明即可。
有时候处于调试目的,我们可以编写 Script
使用内置操作把静态值放入条目中。
<Script code=" the_answer:='The answer is 42' " />
3.3 整体架构
<root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence name="root_sequence"> <SaySomething message="hello" /> <ThinkWhatToSay text="{the_answer}"/> <SaySomething message="{the_answer}" /> </Sequence> </BehaviorTree> </root>
#include "behaviortree_cpp/bt_factory.h" // file that contains the custom nodes definitions #include "dummy_nodes.h" using namespace DummyNodes; int main() { BehaviorTreeFactory factory; factory.registerNodeType<SaySomething>("SaySomething"); factory.registerNodeType<ThinkWhatToSay>("ThinkWhatToSay"); auto tree = factory.createTreeFromFile("./my_tree.xml"); tree.tickWhileRunning(); return 0; } /* Expected output: Robot says: hello Robot says: The answer is 42 */
注:所有接口通过 键名 连接,前提是 类型相同 ,否则将会报错。
3.4 具有泛型类型的接口
BehaviorTree.CPP支持将字符串转化为各种普通类型,例如 int
long
double
bool
等。同时用户自己定义的数据类型也能很好地被支持。
比如:
// We want to use this custom type struct Position2D { double x; double y; };
为了允许 XML 从 string 实例化需要的类型,我们需要提供模板特例,比如:
Position2D BT::convertFromString<Position2D>(StringView)
其中如何进行操作取决于我们要怎么样去处理该字符串,比如:
// Template specialization to converts a string to Position2D. namespace BT { template <> inline Position2D convertFromString(StringView str) { // We expect real numbers separated by semicolons auto parts = splitString(str, ';'); if (parts.size() != 2) { throw RuntimeError("invalid input)"); } else{ Position2D output; output.x = convertFromString<double>(parts[0]); output.y = convertFromString<double>(parts[1]); return output; } } } // end namespace BT
StringView str
是C++11的一种类型,我们也可以使用 std::string
和 const char *
。
splitString
是BehaviorTree提供的一种简单函数,也可以使用其他函数,比如 boost::algorithm::split
。
convertFromString<double>()
是公共模板的特殊化。
例程:
class CalculateGoal: public SyncActionNode { public: CalculateGoal(const std::string& name, const NodeConfig& config): SyncActionNode(name,config) {} static PortsList providedPorts() { return { OutputPort<Position2D>("goal") }; } NodeStatus tick() override { Position2D mygoal = {1.1, 2.3}; setOutput<Position2D>("goal", mygoal); return NodeStatus::SUCCESS; } }; class PrintTarget: public SyncActionNode { public: PrintTarget(const std::string& name, const NodeConfig& config): SyncActionNode(name,config) {} static PortsList providedPorts() { // Optionally, a port can have a human readable description const char* description = "Simply print the goal on console..."; return { InputPort<Position2D>("target", description) }; } NodeStatus tick() override { auto res = getInput<Position2D>("target"); if( !res ) { throw RuntimeError("error reading port [target]:", res.error()); } Position2D target = res.value(); printf("Target positions: [ %.1f, %.1f ]/n", target.x, target.y ); return NodeStatus::SUCCESS; } };
static const char* xml_text = R"( <root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence name="root"> <CalculateGoal goal="{GoalPosition}" /> <PrintTarget target="{GoalPosition}" /> <Script code=" OtherGoal:='-1;3' " /> #直接采用内置操作将 otherGoal 条目放入 blackboard 中 <PrintTarget target="{OtherGoal}" /> </Sequence> </BehaviorTree> </root> )"; int main() { BT::BehaviorTreeFactory factory; factory.registerNodeType<CalculateGoal>("CalculateGoal"); factory.registerNodeType<PrintTarget>("PrintTarget"); auto tree = factory.createTreeFromText(xml_text); tree.tickWhileRunning(); return 0; } /* Expected output: Target positions: [ 1.1, 2.3 ] Converting string: "-1;3" Target positions: [ -1.0, 3.0 ] */
string 类型 和 自定义类型会在存储,读取过程中自动进行转化。(转换目的类型是我们的模板特例类型,库中自带了模板可直接使用,但是自定义类型要自己写convertFromString<T>()
。
4. XML
4.1 架构
<root BTCPP_format="4"> <BehaviorTree ID="MainTree"> <Sequence name="root_sequence"> <SaySomething name="action_hello" message="Hello"/> <OpenGripper name="open_gripper"/> <ApproachObject name="approach_object"/> <CloseGripper name="close_gripper"/> </Sequence> </BehaviorTree> </root>
树的第一个tag为 <root>.它至少应该包含1个tag.
这个tag应该有属性 <BehaviorTree>[ID].
每个树节点都应该有一个标签,特别的:
1.标签的名字用于 factory 中注册该节点。
2.Ports 时使用属性配置的。如 Saysomething message
在子女方面:ControlNode 包含1个到多个子项,DecoratorNode 包含1个子树,ActionNode 没有孩子。
4.2 Ports重新映射
<root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence name="root_sequence"> <SaySomething message="Hello"/> <SaySomething message="{my_message}"/> </Sequence> </BehaviorTree> </root>
可以在 xml 中使用 Ports 读取键值对。{key_name}
如例程中使用 message = “{my_message}” 来读取了blackboards 中的键值对。
4.3 紧凑(Compact)和显式(Explicit )的表示
以下种表示方式均正确:
<SaySomething name="action_hello" message="Hello World"/> <Action ID="SaySomething" name="action_hello" message="Hello World"/>
第一为紧凑,第二种为显式。
以下为第一个例程用显式语法表达:
<root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence name="root_sequence"> <Action ID="SaySomething" name="action_hello" message="Hello"/> <Action ID="OpenGripper" name="open_gripper"/> <Action ID="ApproachObject" name="approach_object"/> <Action ID="CloseGripper" name="close_gripper"/> </Sequence> </BehaviorTree> </root>
显然紧凑的更容易写,但是记录的TreeNode模型的信息较少。像 Groot 这样的工具需要显式的信息,我们可以通过 tag 来添加。如下:
<root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence name="root_sequence"> <SaySomething name="action_hello" message="Hello"/> <OpenGripper name="open_gripper"/> <ApproachObject name="approach_object"/> <CloseGripper name="close_gripper"/> </Sequence> </BehaviorTree> <!-- the BT executor don't require this, but Groot does --> <TreeNodeModel> <Action ID="SaySomething"> <input_port name="message" type="std::string" /> </Action> <Action ID="OpenGripper"/> <Action ID="ApproachObject"/> <Action ID="CloseGripper"/> </TreeNodeModel> </root>
4.4 子树 Subtrees
为简化代码的复杂性,我们可以将一棵树作为子树放到另一棵树下。如下:
<root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence> <Action ID="SaySomething" message="Hello World"/> <SubTree ID="GraspObject"/> </Sequence> </BehaviorTree> #简化起见没有加上属性 "name" <BehaviorTree ID="GraspObject"> <Sequence> <Action ID="OpenGripper"/> <Action ID="ApproachObject"/> <Action ID="CloseGripper"/> </Sequence> </BehaviorTree> </root>
可以看出整棵树都在 saysomething动作后面。
4.5 包含外部文件
可以使用标签轻松做到这一点。
<include path="relative_or_absolute_path_to_file">
例程如下:
<!-- file maintree.xml --> <root BTCPP_format="4" > <include path="grasp.xml"/> #这里 <BehaviorTree ID="MainTree"> <Sequence> <Action ID="SaySomething" message="Hello World"/> <SubTree ID="GraspObject"/> </Sequence> </BehaviorTree> </root>
<!-- file grasp.xml --> <root BTCPP_format="4" > <BehaviorTree ID="GraspObject"> <Sequence> <Action ID="OpenGripper"/> <Action ID="ApproachObject"/> <Action ID="CloseGripper"/> </Sequence> </BehaviorTree> </root>
如果要在ROS包下查找文件,可以使用以下语法:
<include ros_pkg="name_package" path="path_relative_to_pkg/grasp.xml"/>
5. 创建行为树
5.1 创建 ActionNode
推荐使用继承创建。
// Example of custom SyncActionNode (synchronous action) // without ports. class ApproachObject : public BT::SyncActionNode { public: ApproachObject(const std::string& name) : BT::SyncActionNode(name, {}) {} // You must override the virtual function tick() BT::NodeStatus tick() override { std::cout << "ApproachObject: " << this->name() << std::endl; return BT::NodeStatus::SUCCESS; } };
注意:必须重写 tick()虚函数。
TreeNode的任何实例都有name,意在人类可读,没有什么实际意义。
方法 tick() 是实际操作发生的地方,它必须返回 NodeStatus
我们也可以使用给函子依赖注入来创建给定的 TreeNode,(其实就是直接将某个类中的函数作为拿出来作为树节点)。
函子要求具有以下签名之一:
BT::NodeStatus myFunction() BT::NodeStatus myFunction(BT::TreeNode& self)
例如:
using namespace BT; // Simple function that return a NodeStatus BT::NodeStatus CheckBattery() { std::cout << "[ Battery: OK ]" << std::endl; return BT::NodeStatus::SUCCESS; } // We want to wrap into an ActionNode the methods open() and close() class GripperInterface { public: GripperInterface(): _open(true) {} NodeStatus open() { _open = true; std::cout << "GripperInterface::open" << std::endl; return NodeStatus::SUCCESS; } NodeStatus close() { std::cout << "GripperInterface::close" << std::endl; _open = false; return NodeStatus::SUCCESS; } private: bool _open; // shared information };
以上任意一个函子均可用于构建 SimpleActionNode
。(注意:不同类型的节点我们创建的方式也不同)。
5.2 使用 XML 动态创建树
以my_tree.xml
为文件命名。
<root BTCPP_format="4" > <BehaviorTree ID="MainTree"> <Sequence name="root_sequence"> <CheckBattery name="check_battery"/> <OpenGripper name="open_gripper"/> <ApproachObject name="approach_object"/> <CloseGripper name="close_gripper"/> </Sequence> </BehaviorTree> </root>
还需要将自定义的树节点注册到 BehaviorTreeFactory
中,然后从文件或者文本中加载XML。
5.3 注册
XML 中使用的标识符的名字必须与树节点中注册的标识符一致。
#include "behaviortree_cpp/bt_factory.h" // file that contains the custom nodes definitions #include "dummy_nodes.h" using namespace DummyNodes; int main() { // We use the BehaviorTreeFactory to register our custom nodes BehaviorTreeFactory factory; //推荐使用的继承法创建 // The recommended way to create a Node is through inheritance. factory.registerNodeType<ApproachObject>("ApproachObject"); //以下两种行为均为指针函子创建的节点的注册方式,注意和上述方式的不同 // Registering a SimpleActionNode using a function pointer. // Here we prefer to use a lambda,but you can use std::bind too // factory.registerSimpleCondition("CheckBattery", std::bind(&CheckBattery(), this)); factory.registerSimpleCondition("CheckBattery", [&](){ return CheckBattery(); }); // You can also create SimpleActionNodes using methods of a class. //实际上也就调用回调函数的地方加了个类名 GripperInterface gripper; factory.registerSimpleAction("OpenGripper", [&](){ return gripper.open(); } ); factory.registerSimpleAction("CloseGripper", [&](){ return gripper.close(); } // Trees are created at deployment-time (i.e. at run-time, but only // once at the beginning). // IMPORTANT: when the object "tree" goes out of scope, all the // TreeNodes are destroyed auto tree = factory.createTreeFromFile("./my_tree.xml"); // To "execute" a Tree you need to "tick" it. // The tick is propagated to the children based on the logic of the tree. // In this case, the entire sequence is executed, because all the children // of the Sequence return SUCCESS. tree.tickWhileRunning(); return 0; } /* Expected output: * [ Battery: OK ] GripperInterface::open ApproachObject: approach_object GripperInterface::close */
6. 反应式(Reactive) 和 异步行为(Asynchronous)
异步操作 : 一个需要很长时间才能完成,并且会返回运行未满足标准。
按理说它应该具有以下要求:
它不应该阻塞方法太多时间,应尽快返回执行流。
如果调用 halt()
函数,应尽快终止该异步操作。
6.1 有状态异步操作 (StatefulAsyncAction)
此模式在 请求-答复模式中特别有用,当操作向另一个进程发送异步请求时 , 需要定期检查是否已收到回复,根据该回复它将返回成功或者失败。
StatefulAsyncAction 的派生类必须重写以下虚拟方法 , 而不是tick()。
-
NodeStatus onStart():当节点处于空闲状态时调用。 它可能会立即成功或失败,或者返回正在运行。在后一种情况下, 下次 tick 收到答复时,该方法将被执行。
-
NodeStatus onRunning():当节点处于 RUNNING 状态时调用。 返回新状态
onRunning
。 -
void onHalted():当该节点被树上的另一个节点中断时执行。
例程:
// Custom type struct Pose2D { double x, y, theta; }; namespace chr = std::chrono; class MoveBaseAction : public BT::StatefulAsyncAction { public: // Any TreeNode with ports must have a constructor with this signature MoveBaseAction(const std::string& name, const BT::NodeConfig& config) : StatefulAsyncAction(name, config) {} // It is mandatory to define this static method. static BT::PortsList providedPorts() { return{ BT::InputPort<Pose2D>("goal") }; } // this function is invoked once at the beginning. BT::NodeStatus onStart() override; // If onStart() returned RUNNING, we will keep calling // this method until it return something different from RUNNING BT::NodeStatus onRunning() override; // callback to execute if the action was aborted by another node void onHalted() override; private: Pose2D _goal; chr::system_clock::time_point _completion_time; }; //------------------------- BT::NodeStatus MoveBaseAction::onStart() { if ( !getInput<Pose2D>("goal", _goal)) { throw BT::RuntimeError("missing required input [goal]"); } printf("[ MoveBase: SEND REQUEST ]. goal: x=%f y=%f theta=%f/n", _goal.x, _goal.y, _goal.theta); // We use this counter to simulate an action that takes a certain // amount of time to be completed (200 ms) _completion_time = chr::system_clock::now() + chr::milliseconds(220); return BT::NodeStatus::RUNNING; } BT::NodeStatus MoveBaseAction::onRunning() { //假设我们一直检查是否收到了答复 // Pretend that we are checking if the reply has been received // you don't want to block inside this function too much time. std::this_thread::sleep_for(chr::milliseconds(10)); // Pretend that, after a certain amount of time, // we have completed the operation if(chr::system_clock::now() >= _completion_time) { std::cout << "[ MoveBase: FINISHED ]" << std::endl; return BT::NodeStatus::SUCCESS; } return BT::NodeStatus::RUNNING; } void MoveBaseAction::onHalted() { printf("[ MoveBase: ABORTED ]"); }
6.2 Sequence VS ReactiveSequence
例程:
<root BTCPP_format="4"> <BehaviorTree> <Sequence> <BatteryOK/> <SaySomething message="mission started..." /> <MoveBase goal="1;2;3"/> <SaySomething message="mission completed!" /> </Sequence> </BehaviorTree> </root>
int main() { BT::BehaviorTreeFactory factory; factory.registerSimpleCondition("BatteryOK", std::bind(CheckBattery)); factory.registerNodeType<MoveBaseAction>("MoveBase"); factory.registerNodeType<SaySomething>("SaySomething"); auto tree = factory.createTreeFromText(xml_text); // Here, instead of tree.tickWhileRunning(), // we prefer our own loop. std::cout << "--- ticking/n"; status = tree.tickWhileRunning(); std::cout << "--- status: " << toStr(status) << "/n/n"; while(status == NodeStatus::RUNNING) { // Sleep to avoid busy loops. // do NOT use other sleep functions! // Small sleep time is OK, here we use a large one only to // have less messages on the console. tree.sleep(std::chrono::milliseconds(100)); std::cout << "--- ticking/n"; status = tree.tickOnce(); std::cout << "--- status: " << toStr(status) << "/n/n"; } return 0; }
Expected output:
--- ticking [ Battery: OK ] --- status: RUNNING --- ticking Robot says: mission started... --- status: RUNNING --- ticking [ MoveBase: SEND REQUEST ]. goal: x=1.0 y=2.0 theta=3.0 --- status: RUNNING --- ticking --- status: RUNNING --- ticking [ MoveBase: FINISHED ] Robot says: mission completed! --- status: SUCCESS
显然在这个 Sequence 中 , 我们的 BattertOK 只跑了一次,然后便一直在 tick MoveBase .
而如果使用 ReactiveSequence ,则会每次都跑 BattertOK 。
例程:
<root> <BehaviorTree> <ReactiveSequence> <BatteryOK/> <Sequence> <SaySomething message="mission started..." /> <MoveBase goal="1;2;3"/> <SaySomething message="mission completed!" /> </Sequence> </ReactiveSequence> </BehaviorTree> </root>
--- ticking [ Battery: OK ] Robot says: mission started... --- status: RUNNING --- ticking [ Battery: OK ] [ MoveBase: SEND REQUEST ]. goal: x=1.0 y=2.0 theta=3.0 --- status: RUNNING --- ticking [ Battery: OK ] --- status: RUNNING --- ticking [ Battery: OK ] [ MoveBase: FINISHED ] Robot says: mission completed! --- status: SUCCESS
注:即使每次从头开始,中间的 MoveBase 节点也一直在运行,这就是异步操作的效果,如果不是异步操作,则由于运行时间短于 tick() ,必定失败。
注: 推荐使用 tree.sleep()
而不是 std::this_thread::sleep_for()
,因为前者在树中该方法被调用的时候能够被打断 TreeNode::emitStateChanged()
.
7. 子树组合
通过在XML插入小树组合成为大树能使我们的代码复用性提高。
例程:
XML
<root BTCPP_format="4"> <BehaviorTree ID="MainTree"> <Sequence> <Fallback> <Inverter> <IsDoorClosed/> </Inverter> <SubTree ID="DoorClosed"/> #!!!!!!!!!!!!!!!!!!!!!! </Fallback> <PassThroughDoor/> </Sequence> </BehaviorTree> <BehaviorTree ID="DoorClosed"> <Fallback> <OpenDoor/> <RetryUntilSuccessful num_attempts="5"> <PickLock/> </RetryUntilSuccessful> <SmashDoor/> </Fallback> </BehaviorTree> </root>
CPP:
class CrossDoor { public: void registerNodes(BT::BehaviorTreeFactory& factory); // SUCCESS if _door_open == true BT::NodeStatus isDoorClosed(); // SUCCESS if _door_open == true BT::NodeStatus passThroughDoor(); // After 3 attempts, will open a locked door BT::NodeStatus pickLock(); // FAILURE if door locked BT::NodeStatus openDoor(); // WILL always open a door BT::NodeStatus smashDoor(); private: bool _door_open = false; bool _door_locked = true; int _pick_attempts = 0; }; // Helper method to make registering less painful for the user void CrossDoor::registerNodes(BT::BehaviorTreeFactory &factory) { factory.registerSimpleCondition( "IsDoorClosed", std::bind(&CrossDoor::isDoorClosed, this)); factory.registerSimpleAction( "PassThroughDoor", std::bind(&CrossDoor::passThroughDoor, this)); factory.registerSimpleAction( "OpenDoor", std::bind(&CrossDoor::openDoor, this)); factory.registerSimpleAction( "PickLock", std::bind(&CrossDoor::pickLock, this)); factory.registerSimpleCondition( "SmashDoor", std::bind(&CrossDoor::smashDoor, this)); } int main() { BehaviorTreeFactory factory; CrossDoor cross_door; cross_door.registerNodes(factory); // In this example a single XML contains multiple <BehaviorTree> // To determine which one is the "main one", we should first register //为了确定哪颗树是中心树,我们需要将它首先加入工厂。 // the XML and then allocate a specific tree, using its ID factory.registerBehaviorTreeFromText(xml_text); auto tree = factory.createTree("MainTree"); // helper function to print the tree printTreeRecursively(tree.rootNode()); tree.tickWhileRunning(); return 0; }
8.重映射子树端口
为了避免在一棵非常大的树中,各种树之间发生冲突,我们需要将树的接口显式地连接到那些子树中。
注:整个过程在 XML 文件中实现。
注: move_goal 和 result 为 接口 ; move_result 和 target 为 keyname
所以是:子树的 keyname
对应主树的 接口 ; 子树的 接口 对应主树的的 keyname
XML
<root BTCPP_format="4"> <BehaviorTree ID="MainTree"> <Sequence> <Script script=" move_goal='1;2;3' " /> <SubTree ID="MoveRobot" target="{move_goal}" #!!!!!!!!!!!!!!! result="{move_result}" /> #!!!!!!!!!!!!!! <SaySomething message="{move_result}"/> </Sequence> </BehaviorTree> <BehaviorTree ID="MoveRobot"> <Fallback> <Sequence> <MoveBase goal="{target}"/> <Script script=" result:='goal reached' " /> </Sequence> <ForceFailure> <Script script=" result:='error' " /> </ForceFailure> </Fallback> </BehaviorTree> </root>
cpp
int main() { BT::BehaviorTreeFactory factory; factory.registerNodeType<SaySomething>("SaySomething"); factory.registerNodeType<MoveBaseAction>("MoveBase"); factory.registerBehaviorTreeFromText(xml_text); auto tree = factory.createTree("MainTree"); // Keep ticking until the end tree.tickWhileRunning(); // let's visualize some information about the current state of the blackboards. std::cout << "/n------ First BB ------" << std::endl; tree.subtrees[0]->blackboard->debugMessage(); std::cout << "/n------ Second BB------" << std::endl; tree.subtrees[1]->blackboard->debugMessage(); return 0; } /* Expected output: ------ First BB ------ move_result (std::string) move_goal (Pose2D) ------ Second BB------ [result] remapped to port of parent tree [move_result] [target] remapped to port of parent tree [move_goal] */
注:并没有做什么改变,我们这里只是检查一下 blackboard 的值。
9. 使用多个XML文件
随着子树的增加,使用多个 XML 文件会很简单。
例程
subtree_A.xml:
<root> <BehaviorTree ID="SubTreeA"> <SaySomething message="Executing Sub_A" /> </BehaviorTree> </root>
subtree_B.xml:
<root> <BehaviorTree ID="SubTreeB"> <SaySomething message="Executing Sub_B" /> </BehaviorTree> </root>
9.1 手动在 CPP 中加载多个文件
假设一个 main_tree.xml ,它应该包含另外两个XML文件中的两个子树,则我们有两种方式来创建,分别是修改CPP文件和修改 XML 文件:
<root> <BehaviorTree ID="MainTree"> <Sequence> <SaySomething message="starting MainTree" /> <SubTree ID="SubTreeA" /> <SubTree ID="SubTreeB" /> </Sequence> </BehaviorTree> <root>
需要手动添加多个文件
int main() { BT::BehaviorTreeFactory factory; factory.registerNodeType<DummyNodes::SaySomething>("SaySomething"); //找到文件夹下的所有 XML 文件并注册他们 // Find all the XML files in a folder and register all of them. // We will use std::filesystem::directory_iterator std::string search_directory = "./"; using std::filesystem::directory_iterator; for (auto const& entry : directory_iterator(search_directory)) { if( entry.path().extension() == ".xml") { //注册!!!!!!!!!!!!!! factory.registerBehaviorTreeFromFile(entry.path().string()); } } // This, in our specific case, would be equivalent to // factory.registerBehaviorTreeFromFile("./main_tree.xml"); // factory.registerBehaviorTreeFromFile("./subtree_A.xml"); // factory.registerBehaviorTreeFromFile("./subtree_B.xml"); //创建主树,其他子树将会被自动添加到主树中 // You can create the MainTree and the subtrees will be added automatically. std::cout << "----- MainTree tick ----" << std::endl; auto main_tree = factory.createTree("MainTree"); main_tree.tickWhileRunning(); // ... or you can create only one of the subtrees std::cout << "----- SubA tick ----" << std::endl; auto subA_tree = factory.createTree("SubTreeA"); subA_tree.tickWhileRunning(); return 0; } /* Expected output: Registered BehaviorTrees: - MainTree - SubTreeA - SubTreeB ----- MainTree tick ---- Robot says: starting MainTree Robot says: Executing Sub_A Robot says: Executing Sub_B ----- SubA tick ---- Robot says: Executing Sub_A
9.2 在XML添加文件
也可以在XML文件中显式的添加我们的构建子树。
<root BTCPP_format="4"> <include path="./subtree_A.xml" /> #!!!!!!!!!!!!!!!!!!!!!!!!!! <include path="./subtree_B.xml" /> #!!!!!!!!!!!!!!!!!!!!!!!!!!! <BehaviorTree ID="MainTree"> <Sequence> <SaySomething message="starting MainTree" /> <SubTree ID="SubTreeA" /> <SubTree ID="SubTreeB" /> </Sequence> </BehaviorTree> <root>
然后就可以像往常一样创建树:
factory.createTreeFromFile("main_tree.xml")
10. 向函数中添加其他参数(argument)
目前我们被迫的使用了
MyCustomNode(const std::string& name, const NodeConfig& config);
构造函数,但是我们希望能够使用其他参数。
注:理论上可以用 blackboard
来实现这一点,但若满足以下所有条件,则强烈建议不要使用它:
1.参数在部署时是已知的。
2.参数在运行时不会更改。
3.不需要从XML中设置参数。
10.1 向构造函数添加参数(参数)
实例
我们以 Action_A为例自定义节点。向其中传入三个额外的参数,不限于内置类型。
// Action_A has a different constructor than the default one. class Action_A: public SyncActionNode { public: // additional arguments passed to the constructor Action_A(const std::string& name, const NodeConfig& config, int arg_int, std::string arg_str ): SyncActionNode(name, config), _arg1(arg_int), _arg2(arg_str) {} // this example doesn't require any port static PortsList providedPorts() { return {}; } // tick() can access the private members NodeStatus tick() override; private: int _arg1; std::string _arg2; };
然后进行注册:
BT::BehaviorTreeFactory factory; factory.registerNodeType<Action_A>("Action_A", 42, "hello world"); // If you prefer to specify the template parameters // factory.registerNodeType<Action_A, int , std::string>("Action_A", 42, "hello world");
10.2 使用“初始化”方法
有时候需要将不同的值传递给节点类型的单个实例,我们需要考虑以下模式:
实际上是利用内部函数传到了类成员变量上。
class Action_B: public SyncActionNode { public: // The constructor looks as usual. Action_B(const std::string& name, const NodeConfig& config): SyncActionNode(name, config) {} // We want this method to be called ONCE and BEFORE the first tick() void initialize( int arg_int, const std::string& arg_str_ ) { _arg1 = arg_int; _arg2 = arg_str_; } // this example doesn't require any port static PortsList providedPorts() { return {}; } // tick() can access the private members NodeStatus tick() override; private: int _arg1; std::string _arg2; };
我们注册和初始化Action_B的方式是不同的:
BT::BehaviorTreeFactory factory; // Register as usual, but we still need to initialize factory.registerNodeType<Action_B>( "Action_B" ); // Create the whole tree. Instances of Action_B are not initialized yet auto tree = factory.createTreeFromText(xml_text); // visitor will initialize the instances of auto visitor = [](TreeNode* node) { if (auto action_B_node = dynamic_cast<Action_B*>(node)) { action_B_node->initialize(69, "interesting_value"); } }; // Apply the visitor to ALL the nodes of the tree tree.applyVisitor(visitor);
11. 脚本
使用脚本允许我们快速地 读取/写入 blackboard
变量。
11.1 赋值运算符、字符串和数字
实例
param_A := 42 #将数字 42 分配给黑板入口param_A。 param_B = 3.14 #将数字 3.14 分配给黑板入口param_B。 message = 'hello world' #将字符串“hello world”分配给黑板条目消息。
运算符“:=”和“=”之间的区别在于前者 如果不存在,可能会在黑板中创建一个新条目,而后者会抛出异常 , 如果黑板不包含条目,则为例外。
还可以使用分号添加多个 单个脚本中的命令。、
A:= 42; B:=24
11.2 算术运算符和括号
实例
param_A := 7 param_B := 5 param_B *= 2 #结果为 10 param_C := (param_A * 3) + param_B #结果为 31
支持以下运算符:
算子 | 分配运算符 | 描述 |
---|---|---|
+ | += | 加 |
- | -= | 减去 |
* | *= | 乘 |
/ | /= | 分 |
注意:加法运算符是唯一也适用于字符串(用于连接两个字符串)的运算符。
11.3 按位运算符和十六进制数
仅当值可以转换为一个整数时使用。
实例
value:= 0x7F val_A:= value & 0x0F #0x0F(或15) val_B:= value | 0xF0 #0xFF(或255)
二元运算符 | 描述 |
---|---|
| | 按位或 |
& | 按位和 |
^ | 按位异或 |
一元运算符 | 描述 |
---|---|
~ | 否定 |
11.4 逻辑和比较运算符
返回布尔值的运算符。
val_A := true val_B := 5 > 3 val_C := (val_A == val_B) val_D := (val_A && val_B) || !val_C
运营商 | 描述 |
---|---|
对/假 | 布尔 值。铸件分别为 1 和 0 |
&& | 逻辑和 |
|| | 逻辑或 |
! | 否定 |
== | 平等 |
!= | 不等式 |
< | 少 |
<= | 不太平等 |
> | 大 |
>= | 更大的平等 |
11.5 三元运算符if-then-else
val_B = (val_A > 1) ? 42 : 24
11.6 实例
脚本语言演示,包括如何使用枚举 表示整数值。
xml
<root > <BehaviorTree> <Sequence> <Script code=" msg:='hello world' " /> <Script code=" A:=THE_ANSWER; B:=3.14; color:=RED " /> <Precondition if="A>B && color!=BLUE" else="FAILURE"> <Sequence> <SaySomething message="{A}"/> <SaySomething message="{B}"/> <SaySomething message="{msg}"/> <SaySomething message="{color}"/> </Sequence> </Precondition> </Sequence> </BehaviorTree> </root>
c++
int main() { // Simple tree: a sequence of two asynchronous actions, // but the second will be halted because of the timeout. BehaviorTreeFactory factory; factory.registerNodeType<SaySomething>("SaySomething"); enum Color { RED=1, BLUE=2, GREEN=3 }; // We can add these enums to the scripting language factory.registerScriptingEnums<Color>(); // Or we can do it manually factory.registerScriptingEnum("THE_ANSWER", 42); auto tree = factory.createTreeFromText(xml_text); tree.tickWhileRunning(); return 0; }
产出
Robot says: 42.000000 Robot says: 3.140000 Robot says: hello world Robot says: 1.000000
12 前置和后置条件
可以再 tick()之前或者之后运行的脚本
所有节点都支持前置和后置条件,并且 不需要对C++代码进行任何修改。
12.1 前置条件
名字 | 描述 |
---|---|
_skipIf | 如果条件为 true,则跳过此节点的执行 |
_failureIf | 如果条件为真true,跳过并返回失败 |
_successIf | 如果条件为 true, 跳过并返回成功 |
_while | 与 _skipIf 相同,但如果条件变为 false,也可能中断正在运行的节点。 |
实例
之前的版本:
<Fallback> <Inverter> <IsDoorClosed/> </Inverter> <OpenDoor/> </Fallback>
现在我们可以用下例替换掉 IsDoorOpen
<OpenDoor _skipIf="!door_closed"/>
12.2 后置条件
名字 | 描述 |
---|---|
_onSuccess | 如果节点返回成功,则执行此脚本 |
_onFailure | 如果节点返回失败,则执行此脚本 |
_onHalted | 如果正在运行的节点已停止,则执行 |
_while | 如果节点返回成功或失败,则执行脚本 |
则是之前的例程,我们用脚本来更改。
原本XML
<Fallback> <Sequence> <MoveBase goal="{target}"/> <SetBlackboard output_key="result" value="0" /> </Sequence> <ForceFailure> <SetBlackboard output_key="result" value="-1" /> </ForceFailure> </Fallback>
新加内容:
<MoveBase goal="{target}" _onSuccess="result:=OK" _onFailure="result:=ERROR"/>
12.3 设计模式:error_code
行为树相较于状态机来说,困难在于要根据Action的结果执行不同的策略。因为行为树限制于返回成功和失败,可能不够直观。
解决方案是将结果/错误代码存储在 blackboard中 ,但这在 3.X 版本中很麻烦。
前提条件可以帮助我们实现更具可读性的代码,如下所示:
在上面的树中,我们向MoveBase添加了一个输出端口进行返回,我们将根据error_code
的值来选取不同的分支。
12.4 设计模式:states and declarative trees
行为树如果没有状态,我们将很难推理逻辑。
使用状态可以使我们的树更容易理解。例如,我们可以在特定状态进入某一个树的分支。
当且仅当目前状态为 DO_LANDING 才会进入节点,同时当altitude下降到一定值后状态会切到 LANDED 。
注:这种模式另一个作用是,我们制作的节点更具声明性,即更容易将此特定节点/子树移动到树的不同部分。
13. 异步操作
清晰 异步"Asynchronous" Actions
和 同步 "Synchronous" Actions
的区别。
清晰 并发Concurrency
和 并行性Parallelism
的区别。
13.1 并发 与 并行
并发是指两个或多个任务可以在重叠的时间段内启动、运行和完成。 这并不一定意味着它们会在同一时刻运行。
并行性是指任务在不同的线程中同时运行,例如,在多核处理器上。
BT.CPP 同时执行所有节点,换句话说:
-
树的执行引擎是单线程的。
-
所有方法均按顺序执行。
tick()
-
如果任何方法被阻塞,整个执行流将被阻塞。
tick()
我们通过“并发”和异步执行来实现反应行为。
换句话说,需要很长时间才能执行的操作应该尽快返回状态“正在运行”。同时我们需要再次勾选该节点,以了解状态是否更改(轮询)。
异步节点可以将此长时间执行委托给另一个进程 (使用进程间通信)或其他线程。
13.2 异步 和 同步
通常,异步节点是:
-
勾选时,可能会返回“正在运行”而不是“成功”或“失败”。
-
可以在调用
halt()
时尽快停止。
通常,方法halt()必须由开发人员实现。
当树执行返回 RUNNING 的异步操作时, 该状态通常向后传播,并考虑整个树处于“正在运行”状态。
在下面的示例中,“ActionE”是异步且正在运行;
当 一个节点返回正在运行时,通常,它的父节点也返回正在运行。
让我们考虑一个简单的“SleepNode”。一个好的入门模板是 有状态异步操作。
using namespace std::chrono; // Example of Asynchronous node that uses StatefulActionNode as base class class SleepNode : public BT::StatefulAsyncAction { public: SleepNode(const std::string& name, const BT::NodeConfig& config) : BT::StatefulAsyncAction(name, config) {} static BT::PortsList providedPorts() { // amount of milliseconds that we want to sleep return{ BT::InputPort<int>("msec") }; } NodeStatus onStart() override { int msec = 0; getInput("msec", msec); if( msec <= 0 ) { // No need to go into the RUNNING state return NodeStatus::SUCCESS; } else { // once the deadline is reached, we will return SUCCESS. deadline_ = system_clock::now() + milliseconds(msec); return NodeStatus::RUNNING; } } /// method invoked by an action in the RUNNING state. NodeStatus onRunning() override { if ( system_clock::now() >= deadline_ ) { return NodeStatus::SUCCESS; } else { return NodeStatus::RUNNING; } } void onHalted() override { // nothing to do here... std::cout << "SleepNode interrupted" << std::endl; } private: system_clock::time_point deadline_; };
在上面的代码中:
-
当第一次勾选 SleepNode时,将执行该方法
onStart()
。 如果睡眠时间为 0,这可能会立即返回 SUCCESS,否则将返回 RUNNING。 -
我们应该继续循环打勾树
onRunning()
。这将调用可能再次返回 RUNNING 或最终返回 SUCCESS 的方法。 -
另一个节点可能会触发信号。在这种情况下,该方法将被停止。
halt() onHalted()
。
13.3 避免阻止树的执行
SleepNode 的错误实现如下:
// This is the synchronous version of the Node. Probably not what we want. class BadSleepNode : public BT::ActionNodeBase { public: BadSleepNode(const std::string& name, const BT::NodeConfig& config) : BT::ActionNodeBase(name, config) {} static BT::PortsList providedPorts() { return{ BT::InputPort<int>("msec") }; } NodeStatus tick() override { //单线程无限睡眠,卡住啦!!!!!!! int msec = 0; getInput("msec", msec); // This blocking function will FREEZE the entire tree :( std::this_thread::sleep_for( milliseconds(msec) ); return NodeStatus::SUCCESS; } void halt() override { // No one can invoke this method because I froze the tree. // Even if this method COULD be executed, there is no way I can // interrupt std::this_thread::sleep_for() } };
13.4 多线程的问题
在早期,生成一个新线程看起来像是构建异步操作的好解决方案。
但其实它并不好,原因有很多:
-
以线程安全的方式访问
blackboard
更难(稍后会详细介绍)。 -
可能不需要。
-
我们仍然有责任“以某种方式”
halt()
停止该线程并在某种情况下快速停止该线程 该方法被调用。
出于这个原因,通常不鼓励用户使用基类BT::ThreadedAction
。让我们再来看看SleepNode。
// This will spawn its own thread. But it still has problems when halted class BadSleepNode : public BT::ThreadedAction { public: BadSleepNode(const std::string& name, const BT::NodeConfig& config) : BT::ActionNodeBase(name, config) {} static BT::PortsList providedPorts() { return{ BT::InputPort<int>("msec") }; } NodeStatus tick() override { //当前代码结束了,但我开的线程仍在运行,并且无法去停止 // This code runs in its own thread, therefore the Tree is still running. // This seems good but the thread still can't be aborted int msec = 0; getInput("msec", msec); std::this_thread::sleep_for( std::chrono::milliseconds(msec) ); return NodeStatus::SUCCESS; } // The halt() method can not kill the spawned thread :( };
正确的版本是:
// I will create my own thread here, for no good reason class ThreadedSleepNode : public BT::ThreadedAction { public: ThreadedSleepNode(const std::string& name, const BT::NodeConfig& config) : BT::ActionNodeBase(name, config) {} static BT::PortsList providedPorts() { return{ BT::InputPort<int>("msec") }; } NodeStatus tick() override { // This code run in its own thread, therefore the Tree is still running. int msec = 0; getInput("msec", msec); using namespace std::chrono; const auto deadline = system_clock::now() + milliseconds(msec); //一直检查,并且设置一个最大时间 // periodically check isHaltRequested() // and sleep for a small amount of time only (1 millisecond) while( !isHaltRequested() && system_clock::now() < deadline ) { std::this_thread::sleep_for( std::chrono::milliseconds(1) ); } return NodeStatus::SUCCESS; } // The halt() method will set isHaltRequested() to true // and stop the while loop in the spawned thread. };
这看起来比我们一开始实现的版本更复杂。 在某些情况下,此模式仍然有用,但必须记住,引入多线程使事情变得更加复杂,默认情况下应避免使用BT::StatefulActionNode
。
13.5 高级示例:客户端/服务器通信
我们通常使用BT.CPP 在不同的进程中执行实际任务。
在 ROS 中执行此操作的典型(也是推荐的)方法是使用ActionLib。
ActionLib 提供了正确实现异步行为所需的 API:
-
用于启动操作的非阻塞函数。
-
一种监视操作当前执行状态的方法。
-
检索结果或错误消息的方法。
-
抢占/中止正在执行的操作的能力。
这些操作都不是“阻塞”的,因此我们不需要生成自己的线程。
官方文档
官方代码