BehaviorTree 行为树详解 完整版 (参考官方文档)

开源 0

行为树

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 , ConditionNodeDecoratorNode.

2.节点库

2.1 序列:( sequence )

勾选所有子节点,只有所有子节点军返回 SUCCESS 才返回 SUCCESS ,否则返回 FAILURE,过程中为 RUNNING。

共有三种类型,分别如下:

Type of ControlNodeChild returns FAILUREChild returns RUNNING
SequenceRestartTick again
ReactiveSequenceRestartRestart
SequenceWithMemoryTick againTick 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 ControlNodeChild returns RUNNING
FallbackTick again
ReactiveFallbackRestart

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::stringconst 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_goalresult 为 接口 ; move_resulttargetkeyname

所以是:子树的 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_; };

在上面的代码中:

  1. 当第一次勾选 SleepNode时,将执行该方法onStart()。 如果睡眠时间为 0,这可能会立即返回 SUCCESS,否则将返回 RUNNING。

  2. 我们应该继续循环打勾树onRunning()。这将调用可能再次返回 RUNNING 或最终返回 SUCCESS 的方法。

  3. 另一个节点可能会触发信号。在这种情况下,该方法将被停止。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:

  1. 用于启动操作的非阻塞函数。

  2. 一种监视操作当前执行状态的方法。

  3. 检索结果或错误消息的方法。

  4. 抢占/中止正在执行的操作的能力。

这些操作都不是“阻塞”的,因此我们不需要生成自己的线程。

官方文档

官方代码

也许您对下面的内容还感兴趣: