系列文章目录
Behavoir Tree(BT树)–基本概念
Behavoir Tree(BT树)–c++实现
Behavoir Tree(BT树)–c++实现第二节
最近学习BT树进行任务调度。记录和分享一下BT书的相关知识。文章大部分翻译自参考文献 与有限状态机不同的是,行为树是 控制“任务”执行流程的分层节点树。 # 基本概念 - 一个称为“ tick ”的信号被发送到树的根部并在树中传播,直到它到达叶节点。 - 接收到tick信号的 TreeNode 会执行它的回调callback。此回调必须返回 成功SUCCESS, 失败FAILURE或 正在运行RUNNING,如果操作是异步的并且需要更多时间来完成。 - 如果 TreeNode 有一个或多个子节点,它负责根据其状态、外部参数或前一个兄弟节点的结果对它们进行触发。 - LeafNodes叶节点,那些没有任何子节点的 TreeNodes,是实际的命令,即行为树与系统其余部分交互的地方 。动作节点是最常见的叶节点类型。 - 节点类型 **ControlNodes**是可以有1到N 个子节点的节点。一旦收到tick,该tick可以传播给一个或多个子节点。
DecoratorNodes类似于ControlNode,但它只能有一个子节点。
ActionNodes是叶节点,没有子节点。用户应该实现自己的ActionNodes来执行实际任务。
ConditionNodes等效于ActionNodes,但它们始终是原子的,即它们不能返回RUNNING。它们不应该改变系统的状态。
简单举例
为了更好地理解BehaviorTrees如何工作,让我们关注一些实际的例子。为简单起见,我们不会考虑操作返回RUNNING时会发生什么。
我们假设每个Action都是原子地和同步地执行的.
ControlNode:Sequence序列
让我们来说明BT如何使用最基本和最常用的ControlNode:SequenceNode。
ControlNode的子节点总是有序的 ; 是由ControlNode来考虑这个顺序。
在图形表示中,执行顺序是从左到右。
简而言之:
-
如果子节点返回SUCCESS,请触发下一个。
-
如果子节点返回FAILURE,则不再触发子节点并且向Sequence返回FAILURE。
-
如果所有子节点都返回SUCCESS,则Sequence也返回SUCCESS。
Decorators装饰器
依据装饰节点的不同,该节点的作用可能是: -
传递/转换从子节点收到的结果
-
停止子节点的执行
-
重复触发子节点
图片里Inverter就是一个装饰节点,它将子节点的结果反转。图中的Inverter+子节点也可以表述为:“Is the door closed?”. Retry节点重复激发子节点num_attempts(图片里是5)次。
左侧sequence节点的含义为:
“如果门关上,尝试打开。
尝试五次。如果不成功就会放弃并返回失败”
第二个ControlNode:Fallback选择器
Fallback,顾名思义,即处理子节点失败的情况。
它的触发顺序为: -
若子节点失败,出发下一个
-
若子节点成功,不在触发下一个,并返回成功
-
若所有子节点均失败,Fallback节点返回失败。
这个例子可以解释为:
门开了吗?
如果没有,请尝试打开门。
门没开,如果您有钥匙,请解锁并打开门。
否则,砸门。
如果这些操作中的任何一个成功,则进入房间。
节点类型
sequence nodes 序列节点
上文已经介绍过,子节点返回success,sequence node依次触发下一个子节点;子节点返回failure,sequence node失败。
现有三种sequence node框架:sequence、sequenceStar、ReactiveSequence。
他们的相同点是:
- 触发第一个子节点之前,节点的状态变为running
- 子节点返回success,则触发下一个子节点
- 若最后一个子节点返回success,所有子节点停止,并返回success
他们的不同点是:
节点类型 | 子节点failure | 子节点running |
---|---|---|
Sequence | Restart | Tick again |
ReactiveSequence | Restart | Restart |
SequenceStar | Tick again | Tick again |
其中,“Restart”指整个序列从第一个子节点重新开始;“Tick again”指下次触发时,从当前子节点触发。先前返回success的子节点,不再触发。
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 将被多次调用,如果它变为false(即“FAILURE”),则 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伪代码示例
这个框图实现了让机器人依次巡逻A\B\C,并且每个点只巡逻一次。
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 nodes 选择节点
类似地,fallnack node也有两种类型:Fallback和ReactiveFallback
相同点是:
- 触发第一个子节点之前,节点的状态变为running
- 子节点返回failure,则触发下一个子节点
- 若最后一个子节点返回failure,所有子节点停止,并返回failure
- 若任一子节点返回success,停止所有子节点,该节点返回success
他们的不同点是:
节点类型 | 子节点running |
---|---|
Fallback | Tick again |
ReactiveFallback | Restart |
其中,“Restart”指整个序列从第一个子节点重新开始;“Tick again”指下次触发时,从当前子节点触发。先前返回failure的子节点,不再触发。
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 示例伪代码
当前面的条件之一将其状态从 FAILURE 更改为 SUCCESS时,可以使用此 ControlNode中断异步子节点。这个例子中,角色最多睡8个小时。如果他睡够了,那么结点“areYouRested?”返回success,异步节点"timeout"和“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 Nodes 装饰节点
一个装饰器节点只有一个子节点。它可以有以下几类:
InverterNode
反转子节点结果。但是子节点返回running时,同样返回running。
ForceSuccessNode
子节点返回running时,同样返回running;否则一律返回success。
ForceFailureNode
子节点返回running时,同样返回running;否则一律返回failure。
RepeatNode
只要子节点返回成功,触发子节点至N次。若子节点返回failure,中断循环并返回failure。
RetryNode
只要子节点返回失败,触发子节点至N次。若子节点返回failure,中断循环并返回success。
XML格式
XML Schema基础
下面是和一个简单任务树示例
<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>
- 树的第一个tag是,必须有[main_tree_to_execute]属性。它至少含有一个的tag
- tag 必须有[ID]属性
- 当BT树含有多个BehavoirTree时 ,[main_tree_to_execute]属性是必须的,否则就是可选的
- 每个节点都用一个tag表示。特别的:tag的名字是注册在Treenode的ID;[name]属性是该实例的名字,是可选的;Ports通过属性来定义。例子中,SaySomething的动作要求有message的输入port。
Ports 映射 和 指向 Blackboards entries的指针
输入输出port可以被重映射,通过使用Blackboards entry的名字,或者说使用BB的key/value对中的key。
一个BB key可以用{key_name}表示
<root main_tree_to_execute = "MainTree" >
<BehaviorTree ID="MainTree">
<Sequence name="root_sequence">
<SaySomething message="Hello"/>
<SaySomething message="{my_message}"/>
</Sequence>
</BehaviorTree>
</root>
在这个例子中,sequence第一个子节点打印“Hello”,第二个子节点读和写在 the entry of the blackboard 叫做“my_message”的值。
紧凑/清晰的格式
<SaySomething name="action_hello" message="Hello World"/>
<Action ID="SaySomething" name="action_hello" message="Hello World"/>
这两种写法都对,前者是紧凑格式syntax "compact"后者是清晰格式syntax “explicit”.
有些工具例如Groot要求清晰格式或者紧凑格式+额外的信息。例如:
<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>