BehaviorTree.CPP基础知识

Introduction to BTs

与有限状态机不同,行为树是控制“任务”执行流的分层节点树。

基本概念

  • 一个称为“tick”的信号被发送到树的根部,并通过树传播,直到它到达一个叶节点。

  • 接收tick信号的树节点执行其回调。此回调必须返回

    • SUCCESS,
    •  FAILURE or
    • RUNNING,如果操作是异步的,并且需要更多时间才能完成。
    • 如果TreeNode有一个或多个子节点,则它负责根据其状态,外部参数或前一个同级的结果来勾选它们。

The LeafNodes, those TreeNodes which don't have any children, are the actual commands, i.e. the place where the behavior tree interacts with the rest of the system.

Actions nodes are the most common type of LeafNodes.

LeafNodes,那些没有任何子节点的树节点,是实际的命令,即行为树与系统其余部分交互的地方。

Actions nodes 是最常见的叶节点类型。

注意

tick”一词通常用作动词(勾选/被勾选),它的意思是

“调用 ”的回调。tick()TreeNode

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

tick的工作原理

为了在心理上想象勾选树是如何工作的,请考虑下面的示例。

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

  1. 第一个刻度线将序列节点设置为 RUNNING(橙色)。

  2. 序列勾选第一个子项“检测对象”,最终返回 SUCCESS。

  3. 结果,第二个子项“抓取对象”被勾选,整个序列从“正在运行”切换到“成功”。

节点类型

Type of TreeNode

Children Count

Notes

ControlNode

1...N

Usually, ticks a child based on the result of its siblings or/and its own state.

通常,根据孩子的兄弟姐妹或/和自己的状态的结果来勾选孩子。

DecoratorNode

1

Among other things, it may alter the result of the children or tick it multiple times.

除此之外,它可能会改变孩子的结果或多次勾选它。

ConditionNode

0

Should not alter the system. Shall not return RUNNING.

不应更改系统。不应返回“运行”。

ActionNode

0

It can alter the system.

它可以改变系统。

ActionNode的上下文中,我们可以进一步区分同步节点和异步节点。

前者以原子方式执行并阻塞树,直到返回“SUCCESS”或“FAILURE”。

相反,异步操作可能会返回 RUNNING 以传达操作仍在执行。

我们需要再次tick它们,直到最终返回SUCCESS或FAILURE。

例子

为了更好地理解行为树的工作原理,让我们关注一些实际的例子。为了简单起见,我们不会考虑当操作返回 RUNNING 时会发生什么。

我们将假设每个操作都是以原子和同步方式执行的。

第一个控制节点:Sequence

让我们来说明 BT 如何使用最基本和最常用的控制节点: SequenceNode

控制节点的子节点总是有序的;在图形表示中,执行顺序是从左到右。

总之:

  • If a child returns SUCCESS, tick the next one.

  • If a child returns FAILURE,则不会tick更多子项,并且Sequence将返回FAILURE。

  • If all the children return SUCCESS, then the Sequence returns SUCCESS too.

你有没有发现这个bug?

如果action GrabBeer 失败,冰箱的门将保持打开状态,因为跳过了最后一个操作“关闭冰箱”。

Decorators 装饰

根据DecoratorNode的类型,此节点的目标可以是:

  • 以转换从child那里收到的结果。

  • to halt the execution of the child.

  • to repeat ticking the child, depending on the type of Decorator.以重复ticking孩子,具体取决于装饰器的类型。

节点Inverter是一个Decorator,用于反转其子节点返回的结果;因此,An Inverter followed by the node called isDoorOpen is therefore equivalent to

"Is the door closed?".

如果子节点返回 FAILURE,则节点 Retry 将重复勾选子节点最多 num_attempts 次(在本例中为 5 次)。

显然,右侧的分支意味着:

If the door is closed, then try to open it.
Try up to 5 times, otherwise give up and return FAILURE.最多尝试5次,否则放弃并返回FAILURE。

但。。。

你有没有发现这个bug?

如果isDoorOpen返回失败,我们有期望的行为。但是,如果它返回 SUCCESS,则左分支将失败,并且整个序列将被中断

我们稍后将看到如何改进这棵树。

第二个控制节点:Fallback

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

它按顺序ticks the children,and:

  • If a child returns FAILURE,tick下一个。

  • If a child returns SUCCESS,则不会tick更多children,并且“Fallback”将返回“SUCCESS”。

  • If all the children return FAILURE,则Fallback也会返回FAILURE。

在下一个示例中,您可以看到Sequences和Fallbacks是如何组合的:

Is the door open?

If not, try to open the door.

Otherwise, if you have a key, unlock and open the door.

Otherwise, smash the door.

If any of these actions succeeded, then enter the room.

门开着吗?

如果没有,请尝试打开门。

否则,如果您有钥匙,请解锁并打开门。

否则,砸门。

如果这些操作中的任何一个成功,则进入房间。

“Fetch me a beer”重温

我们现在可以改进“Fetch Me a Beer”的例子,如果啤酒不在冰箱里,它就会把门打开。

我们使用颜色“绿色”来表示返回成功的节点,使用“红色”表示返回失败的节点。黑色节点尚未执行。

让我们创建一个替代树,即使 GrabBeer 返回失败,它也会关冰箱门。

这两棵树最终都会关闭冰箱的门,但是:

  • 左边的树总是会返回成功,无论我们是否真的抓住了啤酒。

  • 如果啤酒在那里,右侧的树将返回成功,否则为失败。

如果GrabBeer返回“成功”,一切都按预期工作。

开始

BehaviorTree.CPP是一个C++库,可以轻松集成到您喜欢的分布式中间件中,例如ROSSmartSoft

您可以静态地将其链接到您的应用程序(例如游戏)中。

这些是您需要首先了解的主要概念。

Nodes vs Trees

用户必须创建他/她自己的 ActionNodes and ConditionNodes (LeafNodes);此库可帮助您轻松地将它们组合成树。

将LeafNodes视为构建复杂系统所需的构建块。

根据定义,您的自定义节点是(或应该是)高度可重用的。但是,在开始时,可能需要一些包装接口来调整旧代码。

The tick() callbacks

任何TreeNode都可以被看作是一种调用callback的机制,即运行一段代码。此回调的作用取决于您。

在以下大多数教程中,我们的操作将仅在控制台上打印消息或休眠一定时间,以模拟长时间计算。

在生产代码中,特别是在模型驱动开发和基于组件的软件工程中,Action/Condition可能会与系统的其他组件或服务进行通信。

继承与依赖注入

若要创建自定义TreeNode,应从正确的类继承。

例如,若要创建自己的synchronous Action,应从类“ SyncActionNode”继承。

或者,该库提供了一种机制来创建一个 TreeNode,将函数指针传递给包装器(依赖关系注入)。

这种方法减少了代码中的样板文件数量;作为参考,请看第一个教程

Dataflow, Ports and Blackboard

第二和第个教程中详细介绍了Ports。

目前,重要的是要知道:

  • Blackboard是由树的所有节点共享的键/值存储。

  • Ports是节点可用于在彼此之间交换信息的机制。

  • Ports使用Blackboard的相同键“连接”。

  • 节点的数量,名称和端口类型必须在编译时知道(C++);端口之间的连接在部署时完成 (XML)。

在运行时使用 XML 格式加载树

尽管该库是用C++编写的,但树本身可以在运行时编写,更具体地说,是在部署时编写的,因为它在开始时只执行一次以实例化 Tree。

此处详细介绍了 XML 格式。

Sequences

Sequences会ticks其所有子项,只要它们返回“成功”。如果任何子项返回 FAILURE,则该Sequences将中止。

目前,该框架提供三种类型的节点:

  • Sequence

  • SequenceStar

  • ReactiveSequence

它们共享以下规则:

  • 在勾选第一个子节点之前,节点状态将变为 RUNNING

  • 如果一个child返回“SUCCESS”,它就会ticks下一个child

  • 如果最后一个子项也返回 SUCCESS,则所有child都将停止,序列将返回“成功”。

要了解这三个控制节点的区别,请参阅下表:

Type of ControlNode

Child returns FAILURE

Child returns RUNNING

Sequence

Restart

Tick again

ReactiveSequence

Restart

Restart

SequenceStar

Tick again

Tick again

  • "Restart“表示从列表的第一个child重新启动整个sequence。

  • "Tick again“意味着下次顺序tick时,再次勾选同一个child。已返回“SUCCESS”的上一个同级child不会再次ticked。

Sequence

此树表示计算机游戏中狙击手的行为。

查看伪代码

    status = RUNNING;
    // _index is a private member

    while(_index < number_of_children)
    {
        child_status = child[_index]->tick();

        if( child_status == SUCCESS ) {
            _index++;
        }
        else if( child_status == RUNNING ) {
            // keep same index
            return RUNNING;
        }
        else if( child_status == FAILURE ) {
            HaltAllChildren();
            _index = 0;
            return FAILURE;
        }
    }
    // all the children returned success. Return SUCCESS too.
    HaltAllChildren();
    _index = 0;
    return SUCCESS;

ReactiveSequence

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

让我们看另一个例子:

ApproachEnemy是一个异步操作,它返回 RUNNING,直到它最终完成。

该条件将被调用多次,如果它变为假(即“失败”),则停止。isEnemyVisible ApproachEnemy

查看伪代码

    status = RUNNING;

    for (int index=0; index < number_of_children; index++)
    {
        child_status = child[index]->tick();

        if( child_status == RUNNING ) {
            return RUNNING;
        }
        else if( child_status == FAILURE ) {
            HaltAllChildren();
            return FAILURE;
        }
    }
    // all the children returned success. Return SUCCESS too.
    HaltAllChildren();
    return SUCCESS;

SequenceStar

当您不想再次tick已经返回成功的子节点时,请使用此控制节点。

示例:

这是一个巡逻代理/机器人,必须只访问位置A,B和C一次。如果操作“GoTo(B)”失败,则不会再次勾选“GoTo(A) ”。

另一方面,isBatteryOK必须在每一个tick时进行检查,因此其父级必须是 .ReactiveSequence

查看伪代码

    status = RUNNING;
    // _index is a private member

    while( index < number_of_children)
    {
        child_status = child[index]->tick();

        if( child_status == SUCCESS ) {
            _index++;
        }
        else if( child_status == RUNNING || 
                 child_status == FAILURE ) 
        {
            // keep same index
            return child_status;
        }
    }
    // all the children returned success. Return SUCCESS too.
    HaltAllChildren();
    _index = 0;
    return SUCCESS;

Fallback 备用计划,回退

此节点系列在其他框架中称为“Selector”或“Priority”(选择器“或“优先级)。

他们的目的是尝试不同的策略,直到我们找到一个“有效”的策略。

目前,该框架提供两种类型的节点:

  • Fallback

  • ReactiveFallback

它们共享以下规则:

  • 在tick第一个子节点之前,节点状态将变为 RUNNING

  • 如果一个子项返回 FAILURE,则fallback会勾选下一个子项。

  • 如果最后一个子项也返回失败,则所有子项都将停止,fallback将返回 FAILURE

  • 如果孩子返回“SUCCESS”,它将停止并返回“SUCCESS”。所有的孩子都halted.

要了解这两个控制节点的区别,请参阅下表:

Type of ControlNode

Child returns RUNNING

Fallback

Tick again

ReactiveFallback

Restart

  • "Restart"表示从列表的第一个子级重新启动整个回退。

  • "Tick again"意味着下次回退被勾选时,同一个子项再次被勾选。已经返回失败的前一个兄弟姐妹不会再次勾选。

Fallback

在这个例子中,我们尝试不同的策略来打开这扇门。首先(和一次)检查门是否打开。

查看伪代码

    // index is initialized to 0 in the constructor
    status = RUNNING;

    while( _index < number_of_children )
    {
        child_status = child[index]->tick();

        if( child_status == RUNNING ) {
            // Suspend execution and return RUNNING.
            // At the next tick, _index will be the same.
            return RUNNING;
        }
        else if( child_status == FAILURE ) {
            // continue the while loop
            _index++;
        }
        else if( child_status == SUCCESS ) {
            // Suspend execution and return SUCCESS.
            HaltAllChildren();
            _index = 0;
            return SUCCESS;
        }
    }
    // all the children returned FAILURE. Return FAILURE too.
    index = 0;
    HaltAllChildren();
    return FAILURE;

ReactiveFallback

当您想要中断asynchronous(异步) child时,如果上述条件之一的状态从“失败”更改为“成功”,则使用此控制节点。

在以下示例中,角色将休眠长达 8 小时。如果他/她已经完全休息,则节点将返回 SUCCESS 和异步节点,并将被中断。areYouRested?Timeout (8 hrs)Sleep

查看伪代码

    status = RUNNING;

    for (int index=0; index < number_of_children; index++)
    {
        child_status = child[index]->tick();

        if( child_status == RUNNING ) {
            // Suspend all subsequent siblings and return RUNNING.
            HaltSubsequentSiblings();
            return RUNNING;
        }

        // if child_status == FAILURE, continue to tick next sibling

        if( child_status == SUCCESS ) {
            // Suspend execution and return SUCCESS.
            HaltAllChildren();
            return SUCCESS;
        }
    }
    // all the children returned FAILURE. Return FAILURE too.
    HaltAllChildren();
    return FAILURE;

Decorators

Decorators是只能有一个子级的节点。

由装饰师决定是否,何时以及多少次child应该被tick。

InverterNode  逆变器

勾选子项一次,如果子项失败,则返回“成功”,如果子项成功,则返回“失败”。

如果子节点返回“正在运行”,则此节点也会返回“正在运行”。

ForceSuccessNode  强制成功

如果子节点返回“RUNNING”,则此节点也会返回“RUNNING”。

否则,它始终返回SUCCESS

ForceFailureNode  强制失败

如果子节点返回“RUNNING”,则此节点也会返回“RUNNING”。

否则,它将始终返回FAILURE。

RepeatNode   重复

在child返回 SUCCESS的前提下,Tick the child 最多 N 次(其中 N 作为Input Port传递)

如果child返回FAILURE,则中断循环,在这种情况下,也返回FAILURE。

如果子节点返回“RUNNING”,则此节点也会返回“RUNNING”。

RetryNode 重试

在child返回 FAILURE的前提下,Tick the child 最多 N 次(其中 N 作为Input Port传递)

如果子项返回“SUCCESS”,则中断循环,在这种情况下,也返回“SUCCESS”。

如果子节点返回“RUNNING”,则此节点也会返回“RUNNING”。

The XML format

XML 架构的基础知识

在第一个教程中,介绍了这棵简单的树。

 <root main_tree_to_execute = "MainTree" >
     <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>

您可能会注意到:

  • <BehaviorTree>树的第一个标记是<root> 。它应该包含 1 个或多个 标记 。

  • <BehaviorTree>标记应具有属性 [ID]

  • <root>标记应包含属性 [main_tree_to_execute]

  • <BehaviorTree>如果文件包含多个 ,则该属性[main_tree_to_execute]是必需的,否则为可选。

  • 每个树节点由单个标记表示。In particular:

    • 标记的名称是用于在factory中注册树节点的 ID

    • 该属性[name]引用实例的名称,并且是可选的

    • 端口是使用属性配置的。在前面的示例中,操作需要输入端口 。SaySomethingmessage

  • In terms of number of children:

    • ControlNodes包含 1 到 N 个子项

    • DecoratorNodes和子树仅包含 1 个子项

    • ActionNodes并且没有 child.。ConditionNodes

端口重新映射和指向黑板条目的指针

如第二个教程中所述,输入/输出端口可以使用黑板中的条目名称(换句话说,BB的键/值对的键)重新映射。

BB 密钥使用以下语法表示:。{key_name}

在以下示例中:

  • Sequence的第一个child打印“你好”,

  • 第二个child读写黑板条目中包含的值,称为“my_message”;

 <root main_tree_to_execute = "MainTree" >
     <BehaviorTree ID="MainTree">
        <Sequence name="root_sequence">
            <SaySomething message="Hello"/>
            <SaySomething message="{my_message}"/>
        </Sequence>
     </BehaviorTree>
 </root>

Compact vs Explicit representation紧凑与显式表示

以下两种语法都有效:

 <SaySomething               name="action_hello" message="Hello World"/>
 <Action ID="SaySomething"   name="action_hello" message="Hello World"/>

我们将前一种语法称为“compact紧凑”,将后者称为“explicit显式”。用显式语法表示的第一个示例将变为:

 <root main_tree_to_execute = "MainTree" >
     <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 这样的工具需要显式语法或其他信息。可以使用 标记 添加此信息。<TreeNodeModel>

要使树的紧凑版本与 Groot 兼容,必须按如下方式修改 XML:

 <root main_tree_to_execute = "MainTree" >
     <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>

可用于显式版本的 XML 架构

您可以在此处下载 XML 架构behaviortree_schema.xsd

Subtrees

正如我们在本教程中看到的,可以在另一棵树中包含一个子树,以避免在多个位置“复制和粘贴”同一棵树并降低复杂性。

假设我们要将几个操作封装到行为树“GraspObject”中(作为可选,为了简单起见,省略了属性 [name])。

 <root main_tree_to_execute = "MainTree" >

     <BehaviorTree ID="MainTree">
        <Sequence>
           <Action  ID="SaySomething"  message="Hello World"/>
           <SubTree ID="GraspObject"/>
        </Sequence>
     </BehaviorTree>

     <BehaviorTree ID="GraspObject">
        <Sequence>
           <Action ID="OpenGripper"/>
           <Action ID="ApproachObject"/>
           <Action ID="CloseGripper"/>
        </Sequence>
     </BehaviorTree>  
 </root>

我们可能会注意到,因为整个树“GraspObject”是在“SaySomething”之后执行的。

包括外部文件

从版本2.4开始。

您可以采用类似于C++中#include的方式包含外部文件。我们可以使用 标记轻松完成此操作:

  <include path="relative_or_absolute_path_to_file">

使用前面的示例,我们可以将两个行为树拆分为两个文件:

 <!-- file maintree.xml -->

 <root main_tree_to_execute = "MainTree" >

     <include path="grasp.xml"/>

     <BehaviorTree ID="MainTree">
        <Sequence>
           <Action  ID="SaySomething"  message="Hello World"/>
           <SubTree ID="GraspObject"/>
        </Sequence>
     </BehaviorTree>
  </root>
 <!-- file grasp.xml -->

 <root main_tree_to_execute = "GraspObject" >
     <BehaviorTree ID="GraspObject">
        <Sequence>
           <Action ID="OpenGripper"/>
           <Action ID="ApproachObject"/>
           <Action ID="CloseGripper"/>
        </Sequence>
     </BehaviorTree>  
 </root>

针对 ROS 用户的注意事项

如果要在 ROS 包中查找文件,可以使用以下语法:

<include ros_pkg="name_package" path="path_relative_to_pkg/grasp.xml"/>

Introduction to BT - BehaviorTree.CPP

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值