(为免误导,特免责声明如下:本文所有内容,只是基于个人当前理解和实际做法,后面亦会有更正和修订,但任何版本都不免有个人能力有限、理解有误或者工作环境不同的状况,故文中内容仅供参考。任何人都可以借鉴或者直接使用代码片段,但对任何直接引用或者借鉴产生的技术问题等后果,作者不承担任何责任。)
前置与后置条件Pre and Post conditions
- 在节点前后的条件判断和简单任务,可以大大的增加控制的灵活性。
- BT.CPP 4.x 开始引入
- 是一种脚本scripts,在节点真正的tick()前,或者tick()完后执行。
- 前置与后置条件适合所有节点,而不需要修改C++代码(在xml文件或者文本里)
- 他们应该是比较短小的语句和判断条件,如果太长则最好另外考虑是否采用这种形式来实现。
前置条件
名称 | 描述 |
---|---|
_skipIf | 如果if条件为真,则跳过不执行这个节点 |
_failureIf | 如果if里的条件为真,跳过不执行这个节点并返回失败 |
_successIf | 如果if里的条件为真,跳过不执行这个节点并返回成功 |
_while | 和_skipIf相同,但如果条件为假,则可以中断正在running的node |
- 例子
前面例子描述怎么用fallback来构建if-then-else 的逻辑结构。有了前置条件,新的语法可以更加紧凑。
原来的xml文件(上图左边树)
<Fallback>
<Inverter>
<IsDoorClosed/>
</Inverter>
<OpenDoor/>
</Fallback>
为了替代定制的条件判断节点 IsDoorOpen,现在我们可以存储一个布尔变量到黑板入口:door_closed, 那么xml文件就可以变为:
<OpenDoor _skipIf="!door_closed"/>
后置条件
名称 | 描述 |
---|---|
_onSuccess | 如果节点的tick()返回的是成功,则执行后面的脚本 |
_onFailure | 如果节点的tick()返回的是失败,则执行后面的脚本 |
_post | 不管节点的tick()返回的是成功还是 失败,都执行后面的脚本(如果还是running则不执行) |
_onHalted | 如果节点在running状态被终止(halt)则执行后面的脚本 |
- 示例
在关于子树的教程中,我们看到黑板里的特定变量是如何根据MoveBase的结果进行写入的。
上图的左侧,是 BT.CPP 3.x的实现,右侧是用后置条件极大的简化了,另外新语法支持枚举。
原来的版本:
<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"/>
设计模式的变更: 错误码
相比于状态机,行为树有一个比较纠结的地方:就是当需要根据某个行为的结果来执行不同的策略时应该采用什么模式。
因为行为树被限定在了成功和失败,那就没那么直观了。
在3.x版本里面的解决方案是把结果/错误码放在黑板了,但这样太繁杂笨重了。
前置条件可以使得实现更具有可读性,就是这样:
上面的树中,我们给MoveBase增加了一个输出端口,然后根据该error_code的数值选择性的执行这个顺序控制节点的第二或者第三个分支。
设计模式的变更:状态和声明性树
虽然行为树是为了摆脱状态的蹂躏(有点夸张),但事实是:没有状态来表达,有时候很难去在应用表达逻辑推理的过程。
而使用状态能使得树更加简单,比如我们当且仅当机器人(子系统)处在某个状态的时候才执行某个子树。
看看这个节点和他的前后置条件
这个节点只有在状态=DO_LANDING (着陆中)的时候会执行,一旦高度值足够小,状态会改成LANDED(已着陆),那么节点就会被跳过不再执行了。
注意:DO_LANDING 和LANDED是枚举类型,不是字符串。
- 提示:
这种模式的副产品(随带好处)是让我们的节点变得更加具有声明性,那就是它可以更容易的移动这个特定的节点/子树到树的其他地方。