与有限状态机不同,行为树是一个分层节点树, 它控制着决策的流程,以及 "任务"(或 "Action" )的执行。
树的叶节点是实际的指令,即我们的协同组件与系统其余部分交互的地方。
例如,在面向服务的体系结构中,叶节点将包含与执行操作的 "服务器" 通信的 "客户端" 代码。
在下面的例子中,我们可以看到在一个 Sequence 中执行两个 Action,DetectObject
和 GraspObject
。
树的其它节点,不是叶节点,控制 "执行流程"。
为了更好地理解这个控制流是如何发生的,请设想一个名为 "tick" 的信号。它在树的根部执行,并通过分支传播,直到到达一个或多个叶子节点。
注意
单词 tick 经常被用作动词(勾选/被勾选),它的意思是 "调用一个
TreeNode
的回调tick()
"。
当一个 TreeNode
被勾选,它返回的 NodeStatus
应为下述之一:
- SUCCESS
- FAILURE
- RUNNING
顾名思义,前两个告诉父节点它们的操作是成功还是失败。
当异步节点的执行未完成并且需要更多时间来返回有效结果时,将返回 RUNNING 。
异步节点可以停止。
节点的结果将回传给它的父节点,父节点将决定下一步应勾选哪个子节点,或者可以将结果返回给自己的父节点。
节点类型
ControlNode 是可以有1到N个子节点的节点。一旦收到 tick,该 tick 可以传播到一个或多个子节点。
DecoratorNode 与 ControlNode 相似,但只能有一个子节点。
ActionNode 是叶节点,没有任何子节点。用户应实现自己的 ActionNode 来执行实际任务。
ConditionNode 与 ActionNode 等价,但它们始终是原子和同步的,即它们不得返回 RUNNING。它们不应改变系统的状态。
示例
为了更好地了解行为树的工作原理,让我们关注一些实例。为了简单起见,我们将不考虑 Action 返回 RUNNING 时发生的情况。
我们将假定每个 Action 都是原子且同步执行的。
第一个 ControlNode:Sequence
让我们用最基本和最常用的 ControlNode:SequenceNode,来说明行为树是如何工作的。
ControlNode 的子节点始终是有序的;在图形表示中,执行顺序是从左到右。
简而言之:
- 如果一个子节点返回 SUCCESS,则勾选下一个。
- 如果一个子节点返回 FAILURE,则不再勾选子节点,并且该 Sequence 返回 FAILURE。
- 如果所有子节点都返回 SUCCESS,则该 Sequence 也返回 SUCCESS。
您发现错误了吗?
如果 Action GrabBeer 失败,则由于跳过了最后一个 Action CloseFridge,冰箱的门将保持打开状态。
Decorators
根据 DecoratorNode 的类型,该节点的目标可以是:
- 转换从子节点那里收到的结果,
- 停止子节点的执行,
- 根据 Decorator 的类型重复勾选子节点。
您可以创建自己的 Decorator 来扩展语法。
节点 Inverter 是一个 Decorator,它对子节点返回的结果进行取反。因此,后面跟有称为 DoorOpen 的节点的 Inverter 等效于
"Is the door closed?".
如果子节点 OpenDoor 返回 FAILURE,节点 Retry 将对该子节点重复勾选N次(本例中为3次)。
显然,右侧的分支表示:
If the door is closed, then try to open it.
Try up to 3 times, otherwise give up and return FAILURE.
但...
您发现错误了吗?
如果 DoorOpen 返回 FAILURE,则我们会执行期望的行为。但是,如果返回 SUCCESS,则左分支失败,整个 Sequence 被中断。
稍后我们将看到如何改进此树。
第二个 ControlNode:Fallback
FallbackNode(也称为 "Selectors" ),顾名思义,是可以表达 Fallback 策略的节点,即,如果一个子节点返回 FAILURE,下一步该怎么做。
它按顺序勾选子节点,并且:
- 如果一个子节点返回 FAILURE,则勾选下一个。
- 如果一个子节点返回 SUCCESS,则不会再勾选任何子节点,并且该 Fallback 返回 SUCCESS。
- 如果所有子节点都返回 FAILURE,那么该 Fallback 也返回 FAILURE。
在下一个示例中,您将看到如何组合 Sequence 和 Fallback:
门开着吗?
如果没有,请尝试打开门。
否则,如果您有钥匙,请开锁并打开门。
否则,砸门。
如果这些操作中的任何一个成功,请进入房间。
重新回到 "Fetch me a beer"
现在,我们可以改进 "Fetch Me a Beer" 示例,如果啤酒不在冰箱内,则该示例将门保持打开状态。
我们使用 "绿色" 表示返回 SUCCESS 的节点,使用 "红色" 表示返回 FAILURE 的节点。黑色节点表示尚未执行。
让我们创建一个替代树,即使 GrabBeer 返回 FAILURE 时,它也会关闭门。
这两棵树最终都会关闭冰箱的门,但是:
- 不管我们是否真的拿过啤酒,左侧的树都将始终返回 SUCCESS。
- 如果有啤酒,则右侧的树将返回 SUCCESS,否则将返回 FAILURE。
如果 GrabBeer 返回 SUCCESS,则一切正常。