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

官方文档:https://www.behaviortree.dev/groot/

目录

一、简介

1.1、什么是行为树?

1.2、行为树的主要优点

二、基本概念(Basic Concepts)

2.1行为树BT介绍

2.1.1基本概念

2.1.2 tick如何工作

2.1.3 节点类型

2.1.4 示例

2.1.4.1 第一个控制节点:序列

2.1.4.2 装饰器

2.1.4.3 第二个控制节点:Fallback

三、教程(基础)

3.1 你的第一个行为树

3.1.1 如何创建您自己的ActionNodes

3.1.2 使用XML动态创建树

3.2 黑板和端口

3.2.1 输入端口

3.2.2 输出端口

3.3 具有泛型类型的端口

3.3.1 Parsing a string解析字符串

3.3.2 例如

3.4 反应性和异步行为

3.4.1 StatefulActionNode

3.4.2 Sequence VS ReactiveSequence

3.4.3 事件驱动树?

3.5 使用子树组合行为

CPP节点代码

3.6 重新映射子树的端口

3.7 如何使用多个XML文件

手动加载多个文件(推荐)

使用“include”添加多个文件

3.8 将其他参数传递给节点

向构造函数添加参数(推荐)

使用“初始化”方法

3.9 脚本示例

3.10 记录器与观察器

3.10.1 TreeObserver类

3.10.2 如何唯一标识节点

3.10.3 示例(XML)

3.11 连接到Groot2 

四、教程(高级)

4.1 默认端口值

4.1.1 默认输入端口

4.1.2 默认输出端口

4.2 通过引用访问端口

4.2.1 零拷贝访问黑板

4.2.2 方法1:黑板条目作为共享指针

4.2.3 方法2:线程安全的castPtr(从4.5.1版开始推荐)

4.3 子树模型和自动映射

4.4 模拟和节点替换

4.4.1 完整示例

4.4.2 JSON格式

4.5 全局blackboard

4.5.1 Why a “global blackboard”

4.5.2 黑板层次

4.5.3 如何从树访问顶层黑板

4.5.4 完整示例

五、指南

5.1 脚本语言

5.1.1赋值运算符、字符串和数字

5.1.2 算术运算符和括号

5.1.3 按位运算符和十六进制数

5.1.4 逻辑和比较运算符

5.1.5 三元运算符 if-then-else

5.2 前置和后置条件

5.2.1 前置条件

5.2.2 后置条件

5.2.3 设计模式:状态和声明树

5.3 异步操作

5.3.1 并发性vs并发主义

5.3.2 异步与同步

5.3.3 避免阻塞树的执行

5.3.4 多线程的问题

5.3.5 高级示例:客户端/服务器通信

5.4 端口 VS 黑板

5.4.1 BT.CPP的目标

5.4.2总结:永远不要直接使用黑板

六、节点库

Decorators

Inverter

ForceSuccess

ForceFailure

Repeat

RetryUntilSuccessful

KeepRunningUntilFailure

Delay

RunOnce

PreCondition

SubTree

其他需要在C++中注册的装饰器

ConsumeQueue

SimpleDecoratorNode

Fallbacks

Fallback

ReactiveFallback

Sequences

Sequence

ReactiveSequence

SequenceWithMemory

七、Integration with ROS2

使用rclcpp_action的异步BT::Action

异步BT::Action使用rclcpp::Client(服务)


一、简介

1.1、什么是行为树?

行为树(Behavior Tree,BT)是一种在自主代理(如机器人或计算机游戏中的虚拟实体)中构建不同任务之间切换的方法。BT是创建复杂系统的一种非常有效的方式,这些系统既具有模块化又具有反应性。这些属性在许多应用中至关重要,这导致BT从计算机游戏编程传播到人工智能和机器人技术的许多分支。如果您已经熟悉有限状态机(FSM),您将很容易掌握大部分概念,但希望您会发现BT更具表达力,更容易推理。将树的节点看作是一组构建块。这些块是用C++实现的,并且是“可组合的”:换句话说,它们可以被“组装”来构建行为。

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

在上面的图片中,你可以看到我们正在以一个简单的序列排列这些动作;动作将按从左到右的顺序执行。要了解更多信息,请访问BT简介页面。

1.2、行为树的主要优点

它们本质上是分层的:我们可以组成复杂的行为,包括整个树作为一棵大树的子分支。例如,行为“获取啤酒”可以重用树“抓取对象”。它们的图形表示具有语义意义:更容易“阅读”BT并理解相应的工作流程。相比之下,FSM中的状态转换在文本和图形表示中都更难理解。它们更有表现力:准备使用ControlNode和DecoratorNode使表达更复杂的控制流成为可能。用户可以使用自己的自定义节点扩展“词汇表”。

二、基本概念(Basic Concepts)

2.1行为树BT介绍

2.1.1基本概念

一个称为“tick”的信号被发送到树的根节点,并在树中传播,直到到达叶子节点。任何接收到tick信号的TreeNode都会执行其回调。此回调必须返回

SUCCESS成功FAILURE失败RUNNING运行

RUNNING意味着操作需要更多的时间来返回有效的结果。

如果一个TreeNode有一个或多个子节点,它负责传播tick;每个Node类型可能有不同的规则,关于是否、何时以及子节点被tick的次数。

LeafNode,那些没有任何子节点的TreeNodes,是实际的命令,即行为树与系统其余部分交互的节点。动作节点是最常见的叶节点类型。

在面向服务的体系结构中,叶子将包含与执行实际操作的“服务器”通信的“客户端”代码。

2.1.2 tick如何工作

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

Sequence是最简单的ControlNode:它一个接一个地执行其子节点,如果它们都成功了,它也返回SUCCESS。

第一个刻度将“序列”节点设置为“运行”(橙子)。Sequence勾选第一个子项“OpenDoor”,最终返回SUCCESS。结果,第二个子项“Walk”和后来的“CloseDoor”被勾选。一旦最后一个子进程完成,整个序列将从RUNNING切换到SUCCESS。

2.1.3 节点类型

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

树节点的类型 子节点计数 笔记
控制节点 1…N 通常,根据其兄弟姐妹或/和自己的状态的结果勾选子节点。
装饰器节点 1 除其他外,它可能会改变子节点的结果或多次勾选它。
条件节点 0 不应更改系统。不得返回运行。
动作节点 0 这是“做某事”的节点

ActionNodes的上下文中,我们可以进一步区分同步和异步节点。前者自动执行,并阻塞树,直到返回SUCCESS或FAILURE。相反,异步操作可能会返回RUNNING,以表示该操作仍在执行。我们需要再次勾选它们,直到最终返回“成功”或“失败”。

2.1.4 示例

为了更好地理解BehaviorTrees是如何工作的,让我们关注一些实际的例子。为了简单起见,我们将不考虑当操作返回RUNNING时会发生什么。我们假设每个Action都是自动地同步执行的。

2.1.4.1 第一个控制节点:序列

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

让我们使用最基本和最常用的ControlNode来说明BT是如何工作的:SequenceNode。ControlNode的子节点总是有序的;在图形表示中,执行顺序是从左到右。
如果子项返回SUCCESS,则勾选下一个。
如果子项返回FAILURE,则不再勾选子项,并且Sequence返回FAILURE。
如果所有子进程都返回SUCCESS,则Sequence也返回SUCCESS。

2.1.4.2 装饰器

根据DecoratorNode的类型,此节点的目标可以是:
来转化它从子节点得到的结果;

停止执行子节点
重复触发的子节点,这取决于装饰的类型。

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

节点Inverter是一个Decorator,它反转其子节点返回的结果;因此,Inverter后跟名为isDoorOpen的节点等效于

如果子节点返回FAILURE,节点将重复触发子节点,最多为num_attempts次(本例中为5次)。
如果门是关着的,那就试着打开它。 最多尝试5次,否则放弃并返回失败。

2.1.4.3 第二个控制节点:Fallback

FallbackNodes,也被称为“选择器”,是可以表达的节点,顾名思义,回退策略,即如果孩子返回FAILURE,下一步该怎么做。

它按顺序勾选子项,并且:如果一个子项返回失败,进行下一个。如果子项返回SUCCESS,则不再触发子项,并且回退返回SUCCESS。如果所有子项都返回FAILURE,那么Fallback也返回FAILURE。

三、教程(基础)

3.1 你的第一个行为树

3.1.1 如何创建您自己的ActionNodes

创建TreeNode的默认(和推荐)方法是继承。



// 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;
  }
};

如你所见:任何TreeNode实例都有一个
name
。这个标识符是人类可读的,它不需要是唯一的。方法tick()是实际Action发生的地方。它必须始终返回
NodeStatus
,即RUNNING、SUCCESS或FAILURE。

或者,我们可以使用依赖注入来创建一个给定函数指针(即“functor”)的TreeNode。functor必须具有以下标识:


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
};

3.1.2 使用XML动态创建树

让我们考虑以下名为my_tree.xml的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>

我们必须首先将自定义TreeNodes注册到
BehaviorTreeFactory
中,然后从文件或文本加载XML。XML中使用的标识符必须与用于注册TreeNodes的标识符一致。属性“name”表示实例的名称;它是可选的。
 



#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注册我们的自定义节点
  BehaviorTreeFactory factory;
  // The recommended way to create a Node is through inheritance. 通过继承创建节点
  factory.registerNodeType<ApproachObject>("ApproachObject");
 
  // Registering a SimpleActionNode using a function pointer.使用函数指针注册一个简单节点
  // You can use C++11 lambdas or std::bind
  factory.registerSimpleCondition("CheckBattery", [&](TreeNode&) { return CheckBattery(); });
 
  //You can also create SimpleActionNodes using methods of a class 还可以使用类的方法创建SimpleActionNodes
  GripperInterface gripper;
  factory.registerSimpleAction("OpenGripper", [&](TreeNode&){ return gripper.open(); } );
  factory.registerSimpleAction("CloseGripper", [&](TreeNode&){ 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
*/

3.2 黑板和端口

正如我们前面所解释的,自定义TreeNodes可以用于执行任意简单或复杂的软件。他们的目标是提供一个具有更高抽象级别的接口。因此,它们在概念上与函数没有区别。与函数类似,我们希望:

将参数/参数传递给节点(输入);从节点中获取某种信息(输出)。一个节点的输出可以是另一个节点的输入。

BehaviorTree.CPP提供了一个基本的通过端口的并行机制,它使用简单,但也灵活且类型安全。

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

一个“黑板”是一个简单的键/值存储共享的所有节点的树。Blackboard的“条目”是一个键/值对。输入端口可以读取黑板中的条目,而输出端口可以写入条目。

3.2.1 输入端口

有效输入可以是:由Node读取和解析的静态字符串,或者指向黑板条目的“指针”,由一个键标识。

假设我们想要创建一个名为
SaySomething
的Node,它应该在
std::cout
上打印给定的字符串。为了传递这个字符串,我们将使用一个名为message的输入端口。



<SaySomething name="first"   message="hello world" />
<SaySomething name="second" message="{greetings}" />

可按如下方式实现ActionNode节点
SaySomething



// 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  {
    Expected<std::string> msg = getInput<std::string>("message");
    // Check if expected is valid. If not, throw its error
    if (!msg)    {
      throw BT::RuntimeError("missing required input [message]: ", 
                              msg.error() );
    }
    // use the method value() to extract the valid message.
    std::cout << "Robot says: " << msg.value() << std::endl;
    return NodeStatus::SUCCESS;
  }
};

当自定义TreeNode有输入和/或输出端口时,这些端口必须在静态方法中声明:


static MyCustomNode::PortsList providedPorts();

可以使用模板方法
message
读取来自端口
TreeNode::getInput<T>(key)
的输入。

通常建议在
getInput()
中调用方法
tick()
,而不是在类的构造函数中。C++代码应该期望输入的实际值在运行时改变,因此,它应该定期更新。

3.2.2 输出端口

一个指向黑板入口的输入端口只有在另一个节点已经在同一个入口中写了“something”时才有效。
ThinkWhatToSay
是使用输出端口将字符串写入条目的节点示例。



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;
  }
};

或者,在大多数情况下,出于调试目的,可以使用名为
Script
的内置操作将静态值写入条目。


<Script code=" the_answer:='The answer is 42' " />

我们将在有关BT.CPP中新脚本语言的教程中详细讨论Action Script

3.3 具有泛型类型的端口

在前面的教程中,我们介绍了输入和输出端口,其中端口的类型是
std::string

接下来,我们将展示如何将泛型C++类型分配给端口。

3.3.1 Parsing a string解析字符串

BehaviorTree.CPP支持将字符串自动转换为常见类型,如
int

long

double

bool

NodeStatus
等。也可以轻松支持用户定义的类型。

例如:



// We want to use this custom type
struct Position2D 
{ 
  double x;
  double y; 
};

为了允许XML加载器从字符串实例化
Position2D
,我们需要提供一个模板专门化
BT::convertFromString<Position2D>(StringView)

如何将
Position2D
序列化为字符串取决于您;在本例中,我们简单地用一个分隔符分隔两个数字



// 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
是std::string_view的C++11版本。你可以选择
std::string

const char*
。这个库提供了一个简单的
splitString
函数。可以随意使用另一个,比如boost::algorithm::split。我们可以使用专业化
convertFromString<double>()

3.3.2 例如

正如我们在上一个教程中所做的那样,我们可以创建两个自定义Action,一个将写入端口,另一个将从端口读取。



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 ]
", target.x, target.y );
      return NodeStatus::SUCCESS;
    }
};

我们现在可以像往常一样连接输入/输出端口,指向黑板的同一条目。
下一个例子中的树是一个包含4个动作的序列:

使用操作
Position2D
在入口GoalPosition中存储一个值
CalculateGoal

呼叫
PrintTarget
。输入“target”将从Blackboard条目GoalPosition读取。

使用内置的操作
Script
将字符串“-1 3”分配给关键字Goal。从字符串到
Position2D
的转换将自动完成。

再次
PrintTarget
。输入“target”将从条目“目标”中读取。



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' " />
            <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 ]
*/

3.4 反应性和异步行为

下一个示例显示了
SequenceNode

ReactiveSequence
之间的差异。

我们将实现一个Asynchronous Action,即一个需要很长时间才能完成的动作,当不满足完成条件时,它将返回RUNNING。

异步操作具有以下要求:

它不应该在方法
tick()
中阻塞太多时间。执行流程应该尽快返回。如果调用了
halt()
方法,应该尽快中止它。

3.4.1 StatefulActionNode

StatefulNode是实现异步操作的首选方式。

当你的代码包含一个请求-应答模式时,它特别有用,即当动作向另一个进程发送一个异步请求,并定期检查是否收到应答时。根据该回复,它可能返回SUCCESS或FAILURE。

如果您不是与外部进程通信,而是执行一些需要很长时间的计算,您可能希望将其拆分为小的“块”,或者您可能希望将该计算移动到另一个线程(请参阅AsyncThreadedAction教程)。

StatefulNode的派生类必须覆盖以下虚方法,而不是
tick()


NodeStatus onStart()
:在Node处于 IDLE 状态时调用。它可能会立即成功或失败,也可能返回RUNNING。在后一种情况下,下一次接收到 tick 时,将执行方法
onRunning

NodeStatus onRunning()
:当Node处于RUNNING状态时调用。返回新状态。
void onHalted()
:当这个节点被树中的另一个节点中止时调用。

让我们创建一个名为MoveBaseAction的虚拟节点:



// Custom type
struct Pose2D{
    double x, y, theta;
};
 
namespace chr = std::chrono;
 
class MoveBaseAction : public BT::StatefulActionNode{
  public:
    // Any TreeNode with ports must have a constructor with this signature
    MoveBaseAction(const std::string& name, const BT::NodeConfig& config)
      : StatefulActionNode(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
",
         _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 ]");
}

3.4.2 Sequence VS ReactiveSequence

下面的例子应该使用一个简单的
 SequenceNode



 <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
";
  auto status = tree.tickOnce();
  std::cout << "--- status: " << toStr(status) << "

";
 
  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
";
    status = tree.tickOnce();
    std::cout << "--- status: " << toStr(status) << "

";
  }
 
  return 0;
}

预期输出:



--- ticking
[ Battery: OK ]
Robot says: mission started...
[ 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

您可能已经注意到,当调用
executeTick()
时,
MoveBase
第一次和第二次返回RUNNING,最后第三次返回SUCCESS。
BatteryOK
 只执行一次。如果我们使用
ReactiveSequence
,当子进程
MoveBase
返回RUNNING时,序列将重新启动,条件
BatteryOK
将再次执行。如果在任何时候,
BatteryOK
 返回 FAILURE,
MoveBase
操作将被中断(具体来说是停止)。



 <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...
[ 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

3.4.3 事件驱动树?

我们使用命令
tree.sleep()
而不是
std::this_thread::sleep_for()
是有原因的!

方法
Tree::sleep()
应该是首选的,因为它可以被树中的一个节点在“发生变化”时中断。

当调用方法
Tree::sleep()
时,
TreeNode::emitStateChanged()
将被中断。

3.5 使用子树组合行为

我们可以通过将较小的和可重用的行为插入到较大的行为中来构建大规模的行为。换句话说,我们希望创建分层的行为树,并使我们的树可组合。这可以通过在XML中定义多个树并使用节点SubTree将一个树包含到另一个树中来实现。

这个例子的灵感来自于一篇关于行为树的流行文章。它也是第一个使用
Decorators

Fallback
的实际示例。

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



<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>

期望的行为是:

如果门是开着的,
PassThroughDoor
。如果门已关闭,请尝试
OpenDoor
,或尝试
PickLock
最多5次,最后尝试
SmashDoor
。如果
DoorClosed
子树中的至少一个操作成功,则
PassThroughDoor


Sequence
(顺序节点):
必须 所有子节点都成功,整个 Sequence 才成功。如果中间任一节点失败,立即停止并返回失败。


Fallback
(选择节点):

尝试以下选项,只要有一个成功,Fallback 就成功。选项1:
<Inverter><IsDoorClosed/></Inverter>
选项2:
<SubTree ID="DoorClosed"/>


Inverter
的作用就是:把子节点的返回结果反过来

实际门状态
IsDoorClosed
返回

Inverter
Fallback 的行为
门开着
FAILURE

SUCCESS
成功!不执行 DoorClosed
门关着
SUCCESS

FAILURE
失败 → 执行
DoorClosed

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;
}

3.6 重新映射子树的端口

在CrossDoor的例子中,我们看到
SubTree
从它的父树的角度看起来像一个单一的叶子节点。为了避免在非常大的树中名称冲突,任何树和子树都使用不同的Blackboard实例。出于这个原因,我们需要显式地将树的端口连接到其子树的端口。您不需要修改C++实现,因为这种重新映射完全在XML定义中完成。

例如:

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



<root BTCPP_format="4">
 
    <BehaviorTree ID="MainTree">
        <Sequence>
            <Script code=" 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 code=" result:='goal reached' " />
            </Sequence>
            <ForceFailure>
                <Script code=" result:='error' " />
            </ForceFailure>
        </Fallback>
    </BehaviorTree>
 
</root>

我们有一个
MainTree
,它包含一个名为
MoveRobot
的子树。我们想要将
MoveRobot
子树中的端口与
MainTree
中的其他端口“连接”(即“重新映射”)。这是通过上面示例中使用的语法完成的。



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 << "
------ First BB ------" << std::endl;
  tree.subtrees[0]->blackboard->debugMessage();
  std::cout << "
------ 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]

*/

3.7 如何使用多个XML文件

在我们介绍的例子中,我们总是从一个XML文件创建一个完整的树及其子树。但是随着子树数量的增加,使用多个文件会很方便。

File subtree_A.xml:



<root>
    <BehaviorTree ID="SubTreeA">
        <SaySomething message="Executing Sub_A" />
    </BehaviorTree>
</root>

File subtree_B.xml:



<root>
    <BehaviorTree ID="SubTreeB">
        <SaySomething message="Executing Sub_B" />
    </BehaviorTree>
</root>

手动加载多个文件(推荐)

让我们考虑一个文件main_tree.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");
 
  // 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

使用“include”添加多个文件

如果您希望将树的信息移动到XML本身中,则可以修改main_tree.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>

您可能注意到了,我们在main_tree.xml中包含了两个相对路径,告诉
BehaviorTreeFactory
在哪里可以找到所需的依赖项。路径相对于main_tree. xml。现在我们可以像往常一样创建树:


factory.createTreeFromFile("main_tree.xml")

3.8 将其他参数传递给节点

到目前为止,在我们探索的每个示例中,我们都“被迫”提供具有以下签名的构造函数


MyCustomNode(const std::string& name, const NodeConfig& config);

在某些情况下,需要传递额外的参数,参数,指针,引用等给我们类的构造函数。

在本教程的其余部分中,我们将只使用”arguments”这个词。即使从理论上讲,这些参数可以使用Input Ports传递,但在以下情况下,这将是错误的方式:

参数在部署时(构建树时)是已知的。参数在运行时不会更改。不需要从XML设置参数。

如果所有这些条件都满足,则非常不鼓励使用ports或blackboard。

向构造函数添加参数(推荐)

考虑以下名为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");

使用“初始化”方法

如果出于任何原因,您需要向Node类型的各个实例传递不同的值,您可能需要考虑其他模式:



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);

3.9 脚本示例

在我们的脚本语言中,变量是黑板上的条目。在这个例子中,我们使用节点Script来设置这些变量,并观察我们可以在SaySomething中将它们作为输入端口访问。支持的类型是数字(整数和实数),字符串和注册的ENUMS。



<root BTCPP_format="4">
  <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>

我们期望以下黑板条目包含:msg:字符串“hello world”   A:别名THE_ANSWER对应的整数值。B:真实值3.14   C:对应于枚举RED的整数值。
Robot says: 42.000000
Robot says: 3.140000
Robot says: hello world
Robot says: 1.000000

C++代码是:



enum Color{
  RED = 1,
  BLUE = 2,
  GREEN = 3
};
 
int main(){
  BehaviorTreeFactory factory;
  factory.registerNodeType<DummyNodes::SaySomething>("SaySomething");
 
  // We can add these enums to the scripting language.
  // Check the limits of magic_enum
  factory.registerScriptingEnums<Color>();
 
  // Or we can manually assign a number to the label "THE_ANSWER".
  // This is not affected by any range limitation
  factory.registerScriptingEnum("THE_ANSWER", 42);
 
  auto tree = factory.createTreeFromText(xml_text);
  tree.tickWhileRunning();
  return 0;
}

3.10 记录器与观察器

CPP提供了一种在运行时将记录器添加到树中的方法,通常是在树创建之后和开始标记之前。“logger”是一个类,每当TreeNode更改其状态时都会调用一个回调;它是所谓的观察者模式的非侵入式实现。更具体地说,将调用的回调函数是:



  virtual void callback(
    BT::Duration timestamp, // When the transition happened
    const TreeNode& node,   // the node that changed its status
    NodeStatus prev_status, // the previous status
    NodeStatus status);     // the new status

3.10.1 TreeObserver类

有时候,特别是在实现单元测试时,知道某个Node返回SUCCESS或FAILURE的次数是很方便的。例如,我们要检查在某些条件下,一个分支被执行,而另一个分支没有被执行。
TreeObserver
是一个简单的logger实现,它为树的每个节点收集以下统计信息:



struct NodeStatistics  {
    // Last valid result, either SUCCESS or FAILURE
    NodeStatus last_result;
    // Last status. Can be any status, including IDLE or SKIPPED
    NodeStatus current_status;
    // count status transitions, excluding transition to IDLE
    unsigned transitions_count;
    // count number of transitions to SUCCESS
    unsigned success_count;
    // count number of transitions to FAILURE
    unsigned failure_count;
    // count number of transitions to SKIPPED
    unsigned skip_count;
    // timestamp of the last transition
    Duration last_timestamp;
  };

3.10.2 如何唯一标识节点

由于观察者允许我们收集特定节点的统计数据,因此我们需要一种方法来唯一地识别该节点:可以使用两种机制:


TreeNode::UID()
是对应于树的深度优先遍历的唯一编号。
TreeNode::fullPath()
旨在成为特定节点的唯一但人类可读的标识符。

我们使用术语“path”,因为一个典型的字符串值可能看起来像这样:


 first_subtree/nested_subtree/node_name

换句话说,路径包含子树层次结构中节点位置的信息。“node_name”是在XML中分配的name属性,或者是自动分配的,使用Node注册,后跟“::“和UID。

3.10.3 示例(XML)



<root BTCPP_format="4">
  <BehaviorTree ID="MainTree">
    <Sequence>
     <Fallback>
       <AlwaysFailure name="failing_action"/>
       <SubTree ID="SubTreeA" name="mysub"/>
     </Fallback>
     <AlwaysSuccess name="last_action"/>
    </Sequence>
  </BehaviorTree>
 
  <BehaviorTree ID="SubTreeA">
    <Sequence>
      <AlwaysSuccess name="action_subA"/>
      <SubTree ID="SubTreeB" name="sub_nested"/>
      <SubTree ID="SubTreeB" />
    </Sequence>
  </BehaviorTree>
 
  <BehaviorTree ID="SubTreeB">
    <AlwaysSuccess name="action_subB"/>
  </BehaviorTree>
</root>

您可能会注意到,有些节点具有XML属性“name”,而其他节点则没有。UID – fullPath对的对应列表为:



1 -> Sequence::1
2 -> Fallback::2
3 -> failing_action
4 -> mysub
5 -> mysub/Sequence::5
6 -> mysub/action_subA
7 -> mysub/sub_nested
8 -> mysub/sub_nested/action_subB
9 -> mysub/SubTreeB::9
10 -> mysub/SubTreeB::9/action_subB
11 -> last_action

3.11 连接到Groot2 

四、教程(高级)

4.1 默认端口值

4.1.1 默认输入端口

让我们考虑一个节点初始化多个端口。我们使用自定义类型Point2D,但对于简单类型也是如此,例如
int

double

string



  static PortsList providedPorts()  {
    return { 
      BT::InputPort<Point2D>("input"),
      BT::InputPort<Point2D>("pointA", Point2D{1, 2}, "default value is x=1, y=2"),
      BT::InputPort<Point2D>("pointB", "3,4",         "default value is x=3, y=4"),
      BT::InputPort<Point2D>("pointC", "{point}",     "point by default to BB entry {point}"),
      BT::InputPort<Point2D>("pointD", "{=}",         "point by default to BB entry {pointD}") 
    };
  }

第一个(
input
)没有默认值,必须在XML中提供值或黑板条目。


BT::InputPort<Point2D>("pointA", Point2D{1, 2}, "...");

如果实现了模板特化
convertFromString<Point2D>()
,我们也可以使用它。换句话说,如果我们的convertFromString需要两个逗号分隔的值,则以下语法应该是等效的:



BT::InputPort<Point2D>("pointB", "3,4", "...");
// should be equivalent to:
BT::InputPort<Point2D>("pointB", Point2D{3, 4}, "...");

或者,我们可以定义端口应该指向的默认黑板条目。


BT::InputPort<Point2D>("pointC", "{point}", "...");

如果端口的名称和黑板条目相同,则可以使用“{=}”



BT::InputPort<Point2D>("pointD", "{=}", "...");
// equivalent to:
BT::InputPort<Point2D>("pointD", "{pointD}", "...");

4.1.2 默认输出端口

输出端口更有限,只能指向黑板条目。当两个名称相同时,您仍然可以使用“{=}”。



  static PortsList providedPorts()
  {
    return { 
      BT::OutputPort<Point2D>("result", "{target}", "point by default to BB entry {target}");
    };
  }

4.2 通过引用访问端口

4.2.1 零拷贝访问黑板

如果您遵循了教程,您应该已经知道Blackboard使用值语义,即方法
getInput

setOutput
将值从黑板复制/复制到黑板。

在某些情况下,可能需要使用引用语义,即直接访问存储在Blackboard中的对象。当对象是:

复杂的数据结构;复制成本高; 不可复制。

例如,推荐使用引用语义的Node是
LoopNode
decorator,它“就地”修改对象的向量。

4.2.2 方法1:黑板条目作为共享指针

为了简单起见,我们将考虑一个复制成本很高的对象,称为Pointcloud。

假设我们有一个像这样的简单BT:



 <root BTCPP_format="4" >
    <BehaviorTree ID="SegmentCup">
       <Sequence>
           <AcquirePointCloud  cloud="{pointcloud}"/>
           <SegmentObject  obj_name="cup" cloud="{pointcloud}" obj_pose="{pose}"/>
       </Sequence>
    </BehaviorTree>
</root>

AcquirePointCloud将写入黑板条目
pointcloud
。SegmentObject将从该条目读取。在这种情况下,推荐的端口类型为:



PortsList AcquirePointCloud::providedPorts()
{
    return { OutputPort<std::shared_ptr<Pointcloud>>("cloud") };
}
 
PortsList SegmentObject::providedPorts()
{
    return { InputPort<std::string>("obj_name"),
             InputPort<std::shared_ptr<Pointcloud>>("cloud"),
             OutputPort<Pose3D>("obj_pose") };
}

方法
getInput

setOutput
可以像往常一样使用,并且仍然具有值语义。但由于要复制的对象是
shared_ptr
,因此我们实际上是通过引用访问pointcloud实例。

4.2.3 方法2:线程安全的castPtr(从4.5.1版开始推荐)

使用
shared_ptr
方法时,最值得注意的问题是它不是线程安全的。如果一个自定义的异步Node有自己的线程,那么实际的对象可能会被其他线程同时访问。为了防止这个问题,我们提供了一个不同的API,其中包括一个锁定机制。首先,在创建端口时,我们可以使用普通的
Pointcloud
,而不需要将其包装在
std::shared_ptr
中:



PortsList AcquirePointCloud::providedPorts()
{
    return { OutputPort<Pointcloud>("cloud") };
}
 
PortsList SegmentObject::providedPorts()
{
    return { InputPort<std::string>("obj_name"),
             InputPort<Pointcloud>("cloud"),
             OutputPort<Pose3D>("obj_pose") };
}

要通过指针/引用访问Pointcloud实例,请执行以下操作:



// inside the scope below, as long as "any_locked" exists, a mutex protecting 
// the instance of "cloud" will remain locked
if(auto any_locked = getLockedPortContent("cloud"))
{
  if(any_locked->empty())
  {
    // the entry in the blackboard hasn't been initialized yet.
    // You can initialize it doing:
    any_locked.assign(my_initial_pointcloud);
  }
  else if(Pointcloud* cloud_ptr = any_locked->castPtr<Pointcloud>())
  {
    // Succesful cast to Pointcloud* (original type).
    // Modify the pointcloud instance, using cloud_ptr
  }
}

4.3 子树模型和自动映射

不幸的是,当在多个位置使用相同的子树时,我们可能会发现自己复制和粘贴相同的长XML标记。

考虑这样一个案例:


<SubTree ID="MoveRobot" target="{move_goal}"  frame="world" result="{error_code}" />

我们不想每次都复制和粘贴这三个XML属性
target

frame

result
,除非它们的值不同。

为了避免这种情况,我们可以在
<TreeNodesModel>
中定义它们的默认值。



<TreeNodesModel>
    <SubTree ID="MoveRobot">
      <input_port  name="target"  default="{move_goal}"/>
      <input_port  name="frame"   default="world"/>
      <output_port name="result"  default="{error_code}"/>
    </SubTree>
  </TreeNodesModel>

如果在XML中指定,这些重新映射的黑板条目的值将被覆盖。在下面的例子中,我们覆盖了“frame”的值,但保留了默认的重映射。


<SubTree ID="MoveRobot" frame="map" />

当子树和父树中的条目名称相同时,可以使用属性 
_autoremap


<SubTree ID="MoveRobot" target="{target}"  frame="{frame}" result="{result}" />

可替换为:


<SubTree ID="MoveRobot" _autoremap="true" />

我们仍然可以覆盖一个特定的值,并自动映射其他值


<SubTree ID="MoveRobot" _autoremap="true" frame="world" />

属性
_autoremap="true"
将自动重新映射子树中的所有条目,除非它们的名称以下划线(字符“_”)开头。这可能是一种将子树中的条目标记为“私有”的方便方法。

4.4 模拟和节点替换

有时候,特别是在实现集成和单元测试时,需要有一种机制,允许我们用“测试”版本(模拟)快速替换特定的节点或整个节点类。从4.1版本开始,我们引入了一种新的机制,称为“替换规则”,使这个过程更容易。它由
BehaviorTreeFactory
类中的其他方法组成,这些方法应该在节点注册之后和实际树实例化之前调用。例如,给定XML


<SaySomething name="talk" message="hello world"/>

我们可能想用另一个名为TestMessage的节点替换这个节点:

相应的替换是通过命令完成的:


factory.addSubstitutionRule("talk", "TestMessage");

第一个参数包含将与
TreeNode::fullPath
匹配的字符串。有关fullPath的详细信息,请查看上一个教程。

TestNode
是一个操作,可以配置为:

返回一个特定的状态,SUCCESS或FAILURE同步或异步;在后一种情况下,应指定超时。后置条件脚本,通常用于模拟OutputPort。

这个简单的虚拟节点不会覆盖100%的情况,但可以作为许多替换规则的默认解决方案。

4.4.1 完整示例

在这个例子中,我们将看到如何:我们可以使用替换规则将一个节点替换为另一个节点。如何使用内置的
TestNode
。匹配的例子。如何在运行时使用JSON文件传递这些规则。



<root BTCPP_format="4">
  <BehaviorTree ID="MainTree">
    <Sequence>
      <SaySomething name="talk" message="hello world"/>
        <Fallback>
          <AlwaysFailure name="failing_action"/>
          <SubTree ID="MySub" name="mysub"/>
        </Fallback>
        <SaySomething message="before last_action"/>
        <Script code="msg:='after last_action'"/>
        <AlwaysSuccess name="last_action"/>
        <SaySomething message="{msg}"/>
    </Sequence>
  </BehaviorTree>
 
  <BehaviorTree ID="MySub">
    <Sequence>
      <AlwaysSuccess name="action_subA"/>
      <AlwaysSuccess name="action_subB"/>
    </Sequence>
  </BehaviorTree>
</root>

C++代码:



int main(int argc, char** argv)
{
  BT::BehaviorTreeFactory factory;
  factory.registerNodeType<SaySomething>("SaySomething");
 
  // We use lambdas and registerSimpleAction, to create
  // a "dummy" node, that we want to substitute to a given one.
 
  // Simple node that just prints its name and return SUCCESS
  factory.registerSimpleAction("DummyAction", [](BT::TreeNode& self){
    std::cout << "DummyAction substituting: "<< self.name() << std::endl;
    return BT::NodeStatus::SUCCESS;
  });
 
  // Action that is meant to substitute SaySomething.
  // It will try to use the input port "message"
  factory.registerSimpleAction("TestSaySomething", [](BT::TreeNode& self){
    auto msg = self.getInput<std::string>("message");
    if (!msg)
    {
      throw BT::RuntimeError( "missing required input [message]: ", msg.error() );
    }
    std::cout << "TestSaySomething: " << msg.value() << std::endl;
    return BT::NodeStatus::SUCCESS;
  });
 
  //----------------------------
  // pass "no_sub" as first argument to avoid adding rules
  bool skip_substitution = (argc == 2) && std::string(argv[1]) == "no_sub";
 
  if(!skip_substitution)
  {
    // we can use a JSON file to configure the substitution rules
    // or do it manually
    bool const USE_JSON = true;
 
    if(USE_JSON)
    {
      factory.loadSubstitutionRuleFromJSON(json_text);
    }
    else {
      // Substitute nodes which match this wildcard pattern with TestAction
      factory.addSubstitutionRule("mysub/action_*", "TestAction");
 
      // Substitute the node with name [talk] with TestSaySomething
      factory.addSubstitutionRule("talk", "TestSaySomething");
 
      // This configuration will be passed to a TestNode
      BT::TestNodeConfig test_config;
      // Convert the node in asynchronous and wait 2000 ms
      test_config.async_delay = std::chrono::milliseconds(2000);
      // Execute this postcondition, once completed
      test_config.post_script = "msg ='message SUBSTITUED'";
 
      // Substitute the node with name [last_action] with a TestNode,
      // configured using test_config
      factory.addSubstitutionRule("last_action", test_config);
    }
  }
 
  factory.registerBehaviorTreeFromText(xml_text);
 
  // During the construction phase of the tree, the substitution
  // rules will be used to instantiate the test nodes, instead of the
  // original ones.
  auto tree = factory.createTree("MainTree");
  tree.tickWhileRunning();
 
  return 0;
}

4.4.2 JSON格式

JSON文件,相当于
USE_JSON == false
时执行的分支:



{
  "TestNodeConfigs": {
    "MyTest": {
      "async_delay": 2000,
      "return_status": "SUCCESS",
      "post_script": "msg ='message SUBSTITUED'"
    }
  },
 
  "SubstitutionRules": {
    "mysub/action_*": "TestAction",
    "talk": "TestSaySomething",
    "last_action": "MyTest"
  }
}

正如你所看到的,有两个主要部分:TestNodeConfigs,其中设置了一个或多个TestNode的参数和名称。指定实际规则的SubstitutionRules。

4.5 全局blackboard

4.5.1 Why a “global blackboard”

正如在前面的教程中所描述的,BT.CPP坚持使用“作用域黑板”的重要性,以隔离每个子树,因为它们是独立的函数/例程,在编程语言中。尽管如此,在某些情况下,可能需要一个真正的“全局”黑板,可以直接从每个子树访问,而无需重新映射。这对于以下情况是有意义的:

无法共享的单例对象和全局对象,如1998中所述机器人的全局状态。在行为树之外写入/读取的数据,即在执行tick的主循环中。

此外,由于黑板是一个通用的键/值存储,其中的值可以包含任何类型,它是一个完美的数据结构,以实现在文献中称为“世界模型”,即一个地方,环境的状态,机器人和任务可以与行为树共享。

4.5.2 黑板层次

考虑一个有两个子树的简单树,像这样:

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

3个子树中的每一个都有自己的黑板;这些黑板之间的父/子关系与树完全相同,即BB 1是BB 2和BB 3的父。这些单独黑板的生命周期与它们各自的子树相关联。我们可以这样实现一个外部的“全局黑板”:



auto global_bb = BT::Blackboard::create();
auto maintree_bb = BT::Blackboard::create(global_bb);
auto tree = factory.createTree("MainTree", maintree_bb);

这将创建以下黑板层次结构:

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

实例
global_bb
位于行为树的“外部”,如果对象
tree
被销毁,它将继续存在。此外,它可以很容易地使用
set

get
方法访问。

4.5.3 如何从树访问顶层黑板

所谓“顶层黑板”,我们指的是位于根或层次结构的黑板。在上面的代码中,
global_bb
成为顶层黑板。从BT.CPP的4.6版本开始,引入了一种新的语法来访问顶级黑板,而无需重新映射,方法是将前缀
@
添加到条目的名称中。


<PrintNumber val="{@value}" />

端口val将在顶级黑板中搜索条目值,而不是本地黑板。

4.5.4 完整示例



 <BehaviorTree ID="MainTree">
    <Sequence>
      <PrintNumber name="main_print" val="{@value}" />
      <SubTree ID="MySub"/>
    </Sequence>
  </BehaviorTree>
 
  <BehaviorTree ID="MySub">
    <Sequence>
      <PrintNumber name="sub_print" val="{@value}" />
      <Script code="@value_sqr := @value * @value" />
    </Sequence>
  </BehaviorTree>

C++代码:



class PrintNumber : public BT::SyncActionNode{
public:
  PrintNumber(const std::string& name, const BT::NodeConfig& config)
    : BT::SyncActionNode(name, config)
  {}
  
  static BT::PortsList providedPorts()  {
    return { BT::InputPort<int>("val") };
  }
 
  NodeStatus tick() override  {
    const int val = getInput<int>("val").value();
    std::cout << "[" << name() << "] val: " << val << std::endl;
    return NodeStatus::SUCCESS;
  }
};
 
int main(){
  BehaviorTreeFactory factory;
  factory.registerNodeType<PrintNumber>("PrintNumber");
  factory.registerBehaviorTreeFromText(xml_main);
 
  // No one will take the ownership of this blackboard
  auto global_bb = BT::Blackboard::create();
  // "MainTree" will own maintree_bb
  auto maintree_bb = BT::Blackboard::create(global_bb);
  auto tree = factory.createTree("MainTree", maintree_bb);
 
  // we can interact directly with global_bb
  for(int i = 1; i <= 3; i++)  {
    // write the entry "value"
    global_bb->set("value", i);
    // tick the tree
    tree.tickOnce();
    // read the entry "value_sqr"
    auto value_sqr = global_bb->get<int>("value_sqr");
    // print 
    std::cout << "[While loop] value: " << i 
              << " value_sqr: " << value_sqr << "

";
  }
  return 0;
}

输出量:



[main_print] val: 1
[sub_print] val: 1
[While loop] value: 1 value_sqr: 1
 
[main_print] val: 2
[sub_print] val: 2
[While loop] value: 2 value_sqr: 4
 
[main_print] val: 3
[sub_print] val: 3
[While loop] value: 3 value_sqr: 9

前缀“@”在输入/输出端口或脚本语言中使用时都有效。子树中不需要重新映射。当在主循环中直接访问黑板时,不需要前缀“@”。

五、指南

5.1 脚本语言

Behavior Tree 4.X引入了一个简单但强大的新概念:XML中的脚本语言。实现的脚本语言具有熟悉的语法;它允许用户快速读取/写入黑板的变量。学习脚本如何工作的简单方法是使用内置的action Script,这在第二个教程中介绍

5.1.1赋值运算符、字符串和数字



param_A := 42
param_B = 3.14
message = 'hello world'

第一行将数字42分配给黑板条目param_A。第二行将数字3.14分配给黑板条目param_B。第三行将字符串“hello world”分配给黑板输入消息。

操作符“:=”和“=”之间的区别是,前者可以在黑板中创建一个新的条目,如果它不存在,而后者将抛出一个异常,如果黑板不包含该条目。

5.1.2 算术运算符和括号



param_A := 7
param_B := 5
param_B *= 2
param_C := (param_A * 3) + param_B


param_B
的结果值为10,
param_C
为31。

Operator操作者 Assign Operator指定操作员 Description描述
+ += Add添加
-= Subtract减去
* *= Multiply繁殖
/ /= Divide鸿沟

注意,加法运算符是唯一一个也可以处理字符串的运算符(用于连接两个字符串)。

5.1.3 按位运算符和十六进制数

这些运算符仅在值可以转换为整数时才起作用。将其与字符串或真实的数字一起使用将导致异常。



value:= 0x7F
val_A:= value & 0x0F
val_B:= value | 0xF0


val_A
的值是0x 0 F(或15);
val_B
是0xFF(或255)。

Binary Operators二元操作符 Description描述
| Bitwise or位or
& Bitwise and位and
^ Bitwise xor逐位xor
Unary Operators一元运算符 Description描述
~ Negate否定

5.1.4 逻辑和比较运算符



val_A := true
val_B := 5 > 3
val_C := (val_A == val_B)
val_D := (val_A && val_B) || !val_C
Operators运营商 Description描述
true/false真/假 Booleans. Castable to 1 and 0 respectively布尔值。可分别浇注到1和0
&& Logic and逻辑和
|| Logic or逻辑或
! Negation否定
== Equality平等
!= Inequality不平等
< Less少
<= Less equal少相等
> Greater更大
>= Greater equal大于等于

5.1.5 三元运算符 if-then-else


val_B = (val_A > 1) ? 42 : 24


<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>


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

5.2 前置和后置条件

利用上一个教程中介绍的脚本语言的强大功能,BT.CPP 4.x引入了Pre和Post Conditions的概念,即可以在Node的实际tick()之前或之后运行的脚本。所有节点都支持前置条件和后置条件,不需要在C++代码中进行任何修改。

脚本的目标不是编写复杂的代码,而只是提高树的可读性,并减少在非常简单的用例中对自定义C++节点的需求。如果您的脚本变得太长,您可能需要重新考虑使用它们的决定。

5.2.1 前置条件

Name名称 Description描述
_skipIf 如果条件为真,则跳过此节点的执行
_failureIf 如果条件为真,则跳过并返回FAILURE
_successIf 如果条件为真,则跳过并返回SUCCESS
_while 与_skipIf相同,但如果条件变为false,也可以中断RUNNING Node。

在前面的教程中,我们看到了如何使用回退在树中构建if-then-else逻辑

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



<Fallback>
    <Inverter>
        <IsDoorClosed/>
    </Inverter>
    <OpenDoor/>
</Fallback>

如果我们不使用自定义ConditionNode IsDoorOpen,而是可以在名为
door_closed
的条目中存储布尔值,则XML可以重写为:


<OpenDoor _skipIf="!door_closed"/>

5.2.2 后置条件

名称 描述
_onSuccess 如果节点返回SUCCESS,则执行此脚本
_onFailure 如果节点返回FAILURE,则执行此脚本
_post 如果节点返回SUCCESS或FAILURE,则执行此脚本
_onHalted 如果RUNNING节点停止,则执行脚本

在关于子树的教程中,我们看到了如何根据MoveBase的结果编写特定的黑板变量。

在左侧,您可以看到此逻辑如何在BT.CPP 3.x中实现,以及使用post条件是多么简单。此外,新语法支持枚举。

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

上一版本:



<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"/>

与状态机相比,行为树可能会遇到的一个问题是,应该根据Action的结果执行不同的策略。由于BT仅限于成功和失败,这可能是不直观的。一个解决方案是将结果/错误代码存储在黑板中,但在3.X版本中这很麻烦。前置条件可以帮助我们实现更可读的代码,就像这样:

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

在上面的树中,我们向MoveBase添加了一个输出端口返回,并根据
error_code
的值有条件地采用Sequence的第二个或第三个分支。

5.2.3 设计模式:状态和声明树

即使行为树的承诺是将我们从国家的暴政中解放出来,但事实是,有时很难对没有国家的应用程序进行推理。使用状态可以使我们的树更容易。例如,只有当机器人(或子系统)处于特定状态时,我们才能取树的某个分支。考虑此节点及其前/后条件:

只有当状态等于DO_LANDING时才会执行此节点,并且一旦
altitude
的值足够小,状态就会更改为LANDING。注意DO_LANDING和LANDED是枚举,而不是字符串

5.3 异步操作

在设计反应式行为树时,重要的是要理解两个主要概念:

我们所说的“异步”动作VS“同步”动作。在BT.CPP的上下文中,并发和并发主义之间的区别。

5.3.1 并发性vs并发主义

并发是指两个或多个任务可以在重叠的时间段内启动、运行和完成。这并不一定意味着它们会同时运行。

并行性是指任务在不同线程中同时运行,例如,在多核处理器上。

BT.CPP并发执行所有节点。换句话说:

树执行引擎是单线程的。所有的
tick()
方法都是顺序执行的。如果任何一个
tick()
方法被阻塞,那么整个执行流程都将被阻塞。

我们通过“并发”和异步执行来实现响应式行为。换句话说,需要很长时间才能执行的Action应该尽快返回状态RUNNING。这告诉树执行器动作已经开始,需要更多的时间来返回状态SUCCESS或FAILURE。我们需要再次勾选该节点,以了解状态是否发生了变化(轮询)。异步节点可以将这个长时间执行委托给另一个进程(使用进程间通信)或另一个线程。

5.3.2 异步与同步

一般来说,异步节点是一个:

勾选时,可能会返回RUNNING而不是SUCCESS或FAILURE。当方法
halt()
被调用时,可以尽可能快地停止。

通常,方法halt()必须由开发人员实现。当你的树执行一个返回RUNNING的异步操作时,该状态通常会向后传播,整个树被认为处于RUNNING状态。在下面的示例中,“RUNE”是异步的,并且是RUNNING;当一个节点是RUNNING时,通常它的父节点也返回RUNNING。

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

让我们考虑一个简单的“SleepNode”。一个很好的入门模板是StatefulNode。



using namespace std::chrono;
 
// Example of Asynchronous node that uses StatefulActionNode as base class
class SleepNode : public BT::StatefulActionNode{
  public:
    SleepNode(const std::string& name, const BT::NodeConfig& config)
      : BT::StatefulActionNode(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()
方法。

5.3.3 避免阻塞树的执行



// 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()
    }
};

5.3.4 多线程的问题

在这个库的早期(版本1.x),生成一个新线程看起来是构建异步Action的一个很好的解决方案。以线程安全的方式调用黑板比较困难(稍后会详细介绍)。你可能不需要。人们认为这将神奇地使Action“异步”,但他们忘记了当调用
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 runs 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
实现的版本更复杂。这种模式在某些情况下仍然很有用,但您必须记住,引入多线程会使事情变得更加复杂,默认情况下应该避免。

5.3.5 高级示例:客户端/服务器通信

通常,使用BT.CPP的人在不同的进程中执行实际任务。在ROS中,一个典型的(也是推荐的)方法是使用RISTOLib。MySQL Lib提供了我们正确实现异步行为所需的那种API:

一个非阻塞函数来启动Action。一种监视Action当前执行状态的方法。检索结果或错误消息的方法。抢占/中止正在执行的操作的能力。

这些操作都不是“阻塞”的,因此我们不需要生成自己的线程。更一般地说,我们可以假设开发人员有自己的进程间通信,在BT执行器和实际的服务提供者之间有客户端/服务器关系。

5.4 端口 VS 黑板

BT.CPP是行为树的唯一实现(据我们所知),它引入了输入/输出端口的概念,作为黑板的替代品。更具体地说,端口是一个接口,它为黑板添加了一个间接层和额外的语义。要理解为什么推荐使用Ports而不鼓励直接使用Blackboard,我们应该首先了解BehaviorTree.CPP的一些核心原则。

5.4.1 BT.CPP的目标

模型驱动开发

本文的目的不是解释什么是模型驱动开发。但是,简而言之,我们想要构建节点的“模型”,即某种元信息,告诉我们节点如何与树的其余部分交互。模型对于开发人员(自文档化)和外部工具(如可视化编辑器,Groot 2是一个著名的例子)或静态分析器都很重要。我们认为,节点之间的数据流的描述必须是模型的一部分。此外,我们希望清楚地表达黑板条目是被写入(输出),读取(输入)还是两者兼而有之。

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

在其他实现中(或者如果有人不恰当地使用这个库……)知道这两个节点是否通信并相互依赖的唯一方法是:检查代码:这是我们想要避免的。或阅读文档:但不保证文档是准确和最新的。

相反,如果输入/输出端口是模型的一部分,则节点的意图及其与其他节点的关系变得更加明确:

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

节点可组合性和子树范围

理想情况下,我们希望提供一个平台,允许行为设计师构建树(即,“组成节点”)。但是当直接使用Blackboard时,名称冲突将立即成为一个问题。想想常见的名字,如
goal

results

target

value
等。例如,节点GraspObject和MoveBase可能由不同的人开发,他们都从黑板上读取条目
target
。不幸的是,它们有不同的含义,类型本身也不同:前者需要3D姿势,而后者需要2D姿势。端口提供一个间接级别,也称为“重映射”,如图2所示。这意味着,无论在定义Port时使用哪个名称(该名称被“硬编码”到C++实现中),您都可以将其“重映射”到XML中的不同黑板条目,而无需修改源代码。子树重映射也是如此,在1996中有解释。作为黑板的一个美化的地图的全球变量,它的规模非常差。这就是为什么全局变量在编程中是禁忌的原因!端口重新映射提供了解决此问题的方法。

5.4.2总结:永远不要直接使用黑板



// example code in your tick()
getInput("goal", goal);
setOutput("result", result);

尽可能避免这些:



// example code in your tick()
config().blackboard->get("goal", goal);
config().blackboard->set("result", result);

这两种代码在技术上都可以工作,但后者(直接访问黑板)被认为是不好的做法,并且非常不鼓励:第二个版本的问题,即直接访问黑板:

名称“目标”和“结果”是硬编码的。若要更改它们,必须重新编译应用程序。相反,端口可以在运行时重新映射,只修改XML。要知道一个Node是否读取或写入黑板的一个或多个条目,唯一的方法是检查代码。最新的文档是一种解决方案,但端口模型是自文档化的。
BehaviorTreeFactory
不知道正在访问这些黑板条目。相比之下,当使用端口时,我们能够内省端口如何相互通信,并在部署时检查连接是否正确。使用子树时,它可能无法按预期工作。模板特化
convertFromString()
将无法正常工作。

六、节点库

Decorators

decorator是一个必须有一个子节点的节点。由decorator来决定是否,何时以及多少次子节点应该触发。

Inverter

触发子节点一次,如果子节点失败,则返回SUCCESS,如果子节点成功,则返回FAILURE。如果子节点返回RUNNING,则此节点也返回RUNNING。

ForceSuccess

如果子节点返回RUNNING,则此节点也返回RUNNING。否则,它总是返回SUCCESS。

ForceFailure

如果子节点返回RUNNING,则此节点也返回RUNNING。否则,它总是返回FAILURE。

Repeat

勾选子对象最多N次(在其一次勾选内),其中N作为Input Port
num_cycles
传递,只要子对象返回SUCCESS。如果孩子总是返回SUCCESS,则在重复N次后返回SUCCESS。如果子进程返回FAILURE,则重新启动循环,在这种情况下,也返回FAILURE。如果子节点返回RUNNING,则此节点也返回RUNNING,重复将继续,而不会在Repeat节点的下一个tick上增加。

RetryUntilSuccessful

勾选子对象最多N次,其中N作为Input Port
num_attempts
传递,只要子对象返回FAILURE。在孩子总是返回FAILURE的情况下,在N次尝试后返回FAILURE。如果子进程返回SUCCESS,则执行循环,在这种情况下,也返回SUCCESS。如果子节点返回RUNNING,则此节点也返回RUNNING,并且尝试将继续进行,而不会在RetryUntilSuccessful节点的下一个tick上递增。

KeepRunningUntilFailure

KeepRunningUntilFailure节点总是返回FAILURE(子节点中的FAILURE)或RUNNING(子节点中的SUCCESS或RUNNING)。

Delay

在指定的时间过去后勾选子项。延迟被指定为输入端口
delay_msec
。如果子节点返回RUNNING,则此节点也返回RUNNING,并将在Delay节点的下一个tick上tick子节点。否则,返回子节点的状态。

RunOnce

当您只想执行子节点一次时,将使用RunOnce节点。如果子进程是异步的,它将一直触发,直到返回SUCCESS或FAILURE。在第一次执行之后,您可以将Input Port
 then_skip
的值设置为:TRUE(默认值),以后将跳过该节点。FALSE, 永远同步返回子对象返回的相同状态。

PreCondition

参见脚本语言简介

SubTree

参见使用子树组合行为。

其他需要在C++中注册的装饰器

ConsumeQueue

只要队列不为空,就执行子节点。在每次迭代中,从“队列”中弹出一个类型为T的项,并将其插入“popped_item”中。空队列将返回SUCCESS例如,使用
factory.registerNodeType<ConsumeQueue<Pose2D>>("ConsumeQueue");
Cf注册。ex04_waypoints.cpp。

SimpleDecoratorNode


void BehaviorTreeFactory::registerSimpleDecorator("MyDecorator", tick_function, ports)
注册一个简单的装饰器节点,它在内部使用
SimpleDecoratorNode
,其中
tick_function
是一个签名为
std::function<NodeStatus(NodeStatus, TreeNode&)>
的函数,ports是一个类型为
PortsList
的变量。

Fallbacks

这一系列节点在其他框架中被称为“Selector”或“Priority”。他们的目的是尝试不同的策略,直到我们找到一个“工作”。目前框架提供了两种节点:

Fallback
ReactiveFallback

它们共享以下规则:在勾选第一个子节点之前,节点状态变为RUNNING。如果子级返回FAILURE,则回退标记下一个子级。如果最后一个子进程也返回FAILURE,则停止所有子进程,回退返回FAILURE。如果一个子进程返回SUCCESS,它将停止并返回SUCCESS。所有的孩子都停了下来。

要了解这两个ControlNode的不同之处,请参阅下表:

Type of ControlNode控制节点类型 Child returns RUNNING儿童回返
Fallback回退 Tick again再打勾
ReactiveFallback Restart重启

“Restart”意味着整个回退从列表的第一个子级重新启动。“再次勾选”意味着下一次勾选回退时,同一个子对象再次勾选。以前的兄弟姐妹,已经返回失败,不再打勾。

Fallback

在这个例子中,我们尝试不同的策略来打开门。先检查一下门是否开着。

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

ReactiveFallback

如果前面的条件之一将其状态从FAILURE更改为SUCCESS,则需要中断异步子进程时,将使用此ControlNode。在下面的示例中,角色将睡眠长达8小时。如果他/她已经完全休息,则节点
areYouRested?
将返回SUCCESS,并且异步节点
Timeout (8 hrs)

Sleep
将被中断。

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

Sequences

一个Sequence只要返回SUCCESS,就会勾选它的所有子元素。如果任何子进程返回FAILURE,则序列中止。目前框架提供了三种节点:

Sequence
SequenceWithMemory
ReactiveSequence

它们共享以下规则:

在勾选第一个子节点之前,节点状态变为RUNNING。
如果一个子进程返回SUCCESS,它会勾选下一个子进程。
如果最后一个子进程也返回SUCCESS,则停止所有子进程,序列返回SUCCESS。

要了解这三个ControlNode的不同之处,请参阅下表:

控制节点类型 Child returns FAILURE Child returns RUNNING
Sequence Restart Tick again
ReactiveSequence Restart Restart
SequenceWithMemory Tick again Tick again

“Restart”意味着整个序列从列表的第一个孩子开始重新启动。“再次勾选”意味着下一次勾选序列时,同一个子对象再次勾选。以前返回SUCCESS的兄弟不再打勾。

Sequence

这棵树代表了电脑游戏中狙击手的行为。

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

ReactiveSequence

此节点对于连续检查Conditions特别有用;但是用户在使用异步子节点时也应该小心,以确保它们不会比预期更频繁地被勾选。

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


ApproachEnemy
是一个异步操作,返回RUNNING,直到它最终完成。条件
isEnemyVisible
将被多次调用,如果它变为假(即“FAILURE”),则停止
ApproachEnemy

SequenceWithMemory

当您不想再次勾选已返回SUCCESS的子级时,请使用此ControlNode。这是一个巡逻代理/机器人,必须只访问位置a、B和C一次。如果操作GoTo(B)失败,GoTo(A)将不再被勾选。另一方面,isBatteryOK必须在每个tick都被检查,因此其父节点必须是
ReactiveSequence

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

七、Integration with ROS2

BehaviorTree.CPP经常用于机器人和ROS生态系统。我们提供了一组现成的包装器,可用于快速实现与ROS 2交互的TreeNodes:BehaviorTree.ROS 2在系统架构方面,我们应该记住:

你应该有一个集中的“协调者”ROS节点,负责行为的执行。这将被进一步称为“任务规划器”,它将与BT.CPP一起实现
系统的所有其他元素都应该是“面向服务”的组件,并且应该将任何业务逻辑和决策委托给任务规划器。

使用rclcpp_action的异步BT::Action

与ROS交互的推荐方法是通过rclcpp_action。

他们的API是异步的,也就是说,用户不必担心创建单独的线程。
它们可以被中止,这是实现
TreeNode::halt()
和构建反应行为所需的功能。

让我们考虑一下,例如,在官方C++教程中描述的“Fibonacci”动作客户端:



// let's define these, for brevity
using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;

要创建调用此ROS操作的BT操作,请执行以下操作:



#include <behaviortree_ros2/bt_action_node.hpp>
 
using namespace BT;
 
class FibonacciAction: public RosActionNode<Fibonacci>{
public:
  FibonacciAction(const std::string& name,
                  const NodeConfig& conf,
                  const RosNodeParams& params)
    : RosActionNode<Fibonacci>(name, conf, params)
  {}
 
  // The specific ports of this Derived class
  // should be merged with the ports of the base class,
  // using RosActionNode::providedBasicPorts()
  static PortsList providedPorts()  {
    return providedBasicPorts({InputPort<unsigned>("order")});
  }
 
  // This is called when the TreeNode is ticked and it should
  // send the request to the action server
  bool setGoal(RosActionNode::Goal& goal) override   {
    // get "order" from the Input port
    getInput("order", goal.order);
    // return true, if we were able to set the goal correctly.
    return true;
  }
  
  // Callback executed when the reply is received.
  // Based on the reply you may decide to return SUCCESS or FAILURE.
  NodeStatus onResultReceived(const WrappedResult& wr) override  {
    std::stringstream ss;
    ss << "Result received: ";
    for (auto number : wr.result->sequence) {
      ss << number << " ";
    }
    RCLCPP_INFO(node_->get_logger(), ss.str().c_str());
    return NodeStatus::SUCCESS;
  }
 
  // Callback invoked when there was an error at the level
  // of the communication between client and server.
  // This will set the status of the TreeNode to either SUCCESS or FAILURE,
  // based on the return value.
  // If not overridden, it will return FAILURE by default.
  virtual NodeStatus onFailure(ActionNodeErrorCode error) override  {
    RCLCPP_ERROR(node_->get_logger(), "Error: %d", error);
    return NodeStatus::FAILURE;
  }
 
  // we also support a callback for the feedback, as in
  // the original tutorial.
  // Usually, this callback should return RUNNING, but you
  // might decide, based on the value of the feedback, to abort
  // the action, and consider the TreeNode completed.
  // In that case, return SUCCESS or FAILURE.
  // The Cancel request will be send automatically to the server.
  NodeStatus onFeedback(const std::shared_ptr<const Feedback> feedback)  {
    std::stringstream ss;
    ss << "Next number in sequence received: ";
    for (auto number : feedback->partial_sequence) {
      ss << number << " ";
    }
    RCLCPP_INFO(node_->get_logger(), ss.str().c_str());
    return NodeStatus::RUNNING;
  }
};

您可能会注意到,Action客户端的BT版本比原来的版本更简单,因为大多数样板文件都在
BT::RosActionNode
包装器中。当注册这个节点时,我们需要使用
rclcpp::Node
传递
BT::RosNodeParams
和其他参数:



// in main()
  BehaviorTreeFactory factory;
 
  auto node = std::make_shared<rclcpp::Node>("fibonacci_action_client");
  // provide the ROS node and the name of the action service
  RosNodeParams params; 
  params.nh = node;
  params.default_port_value = "fibonacci";
  factory.registerNodeType<FibonacciAction>("Fibonacci", params);

异步BT::Action使用rclcpp::Client(服务)

类似的包装器可用于ROS服务客户端。将使用异步接口。下面的例子是基于官方教程。



#include <behaviortree_ros2/bt_service_node.hpp>
 
using AddTwoInts = example_interfaces::srv::AddTwoInts;
using namespace BT;
 
 
class AddTwoIntsNode: public RosServiceNode<AddTwoInts>
{
  public:
 
  AddTwoIntsNode(const std::string& name,
                  const NodeConfig& conf,
                  const RosNodeParams& params)
    : RosServiceNode<AddTwoInts>(name, conf, params)
  {}
 
  // The specific ports of this Derived class
  // should be merged with the ports of the base class,
  // using RosServiceNode::providedBasicPorts()
  static PortsList providedPorts()
  {
    return providedBasicPorts({
        InputPort<unsigned>("A"),
        InputPort<unsigned>("B")});
  }
 
  // This is called when the TreeNode is ticked and it should
  // send the request to the service provider
  bool setRequest(Request::SharedPtr& request) override
  {
    // use input ports to set A and B
    getInput("A", request->a);
    getInput("B", request->b);
    // must return true if we are ready to send the request
    return true;
  }
 
  // Callback invoked when the answer is received.
  // It must return SUCCESS or FAILURE
  NodeStatus onResponseReceived(const Response::SharedPtr& response) override
  {
    RCLCPP_INFO(node_->get_logger(), "Sum: %ld", response->sum);
    return NodeStatus::SUCCESS;
  }
 
  // Callback invoked when there was an error at the level
  // of the communication between client and server.
  // This will set the status of the TreeNode to either SUCCESS or FAILURE,
  // based on the return value.
  // If not overridden, it will return FAILURE by default.
  virtual NodeStatus onFailure(ServiceNodeErrorCode error) override
  {
    RCLCPP_ERROR(node_->get_logger(), "Error: %d", error);
    return NodeStatus::FAILURE;
  }
};

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...