游戏AI之行为树(中)
介绍了共有的功能,接下来说三个主要的行为树节点的原型:
Composite
Decorator
Leaf
![](http://img.manew.com/data/attachment/forum/201603/16/104731mhs2odxx822k55ag.gif.thumb.jpg)
Composite
合成节点
合成节点可以有一个或多个子节点。它们处理子节点的顺序可以是从第一个到最后一个,或者某些特定的合成节点的随机顺序,在某一阶段会根据它的子节点的处理结果向它的父节点返回success或者failure,通常这取决于它的子节点的success或者failure(译者:这里提到了三层节点)。当它在处理子节点时,会向它的父节点持续发送running。
最常用的合成节点是Sequence节点,它按照顺序运行每一个子节点,如果任何一个子节点返回了failure,它返回failure;如果所有子节点返回成功状态,它才返回成功。
Decorator
修饰节点
修饰节点同合成节点相似,可以拥有子节点,与之不同点在于,它有且只有一个子节点。它的功能就是:将子节点的结果传递给父节点,停止子节点;或者重复执行子节点,这取决于具体的修饰节点类型。
一个常用的修饰节点的用法就是Inverter(反相器),它只是把子节点的结果反相。当它的子节点返回了失败,它给它的父节点返回成功,反之亦反。
Leaf
叶子节点
它是最底层的节点类型,不能拥有子节点。
但叶子节点是最强大的节点类型,因为它在游戏中被你定义和实现,来做具体游戏或具体角色的检测或者动作,让你的行为树真正做一些事情。
举一个例子,和之前类似,是一个行走的行为。一个Walk节点会让角色行走到指定的地点,然后根据行走的结果来返回成功或者失败。
因为你可以定义你自已的叶子节点(经常是少量代码),放在合成节点和修饰节点以下,使你可以做出很强大的行为树,可以有很复杂的层次和智能优先级的行为,这非常优秀。
用游戏代码去类比,可以将合成和修饰节点当作函数、分支结构和循环结构,还有其它编程语言的结构,用它们来定义你代码的逻辑。而叶子节点就像游戏中具体的代码逻辑,会让你的AI角色做一些实际上的事情或者检测它们的状态或场景。
叶子节点可以带参数。例如Walk节点就可以接收一个角色将走向的坐标。
这些参数可以取自保存在行为树空间中的变量。例如一个目标点可以被一个“获取安全地点”节点来决定,保存在变量中,然后Walk节点可以利用这个值来定义目标地点。这是通过使用一个节点之间共享的空间来保存和修改任意的长驻的变量来实现的,它让行为树变得无比强大。
另一个叶子节点的大类型是调用其它的行为树的节点,将已存在的行为树的数据空间传递给被调用的行为树。
这一点很重要,因为这允许你将行为树深度模块化来创建可以无限重用的行为树,可能会用到空间中一个特定的变量来操作。例如,一个“闯入建筑”的行为可能需要一个“目标建筑”的变量来进行操作,所以父树可以在空间设置这个变量,然后通过一个子树的叶子节点调用另一棵子树。
Composite Nodes
合成节点
接下来我们会讨论在行为树中见到的最常用的合成节点。也有其它的,但我们只包含这些基本的,已经可以让你写出很复杂的行为树了。
Sequences
序列
行为树中应用的最简单的合成节点,它的名称已经说明了一切。一个序列节点会顺序访问每个子节点,从第一个开始,如果它返回成功,那么访问下一个,依次类推。如果任何子节点失败了,它立即向它的你节点返回失败;如果最后一个子节点也成功,序列节点会向它的父节点返回成功。
你必须明白这个类型的节点在行为树中有着广泛的应用。最明显的一个用法是,定义一系列必须全部完成的任务,任何一个节点的失败都意味着后续节点的处理都是无用的。
For example:
例如:
![](http://img.manew.com/data/attachment/forum/201603/16/104835t7368zq88zal6k66.gif.thumb.jpg)
这个序列很明显,让角色穿过一扇门,然后关上身后的门。事实上,这些节点可能有点抽象,在实际生产环境中会使用一些参数。Walk(地点),Open(是否开着),Walk(地点),Close(是否开着);
处理顺序如下:
Sequence -> Walk to Door (success) -> Sequence (running) -> Open Door (success) -> Sequence (running) -> Walk through Door (success) -> Sequence (running) -> Close Door (success) -> Sequence (success) -> at which point the sequence returns success to its own parent.
。。。
如果角色没有走到门前,可能路被挡住了,那么尝试去开门也没有意义了,更不用说穿过门。序列节点在行走失败的时候已经返回失败了,然后这个序列节点的父节点可以很好地处理这个失败。
上述的序列节点让角色完成一系列的动作,因为这似乎是行为树的唯一用途,所以你可能不会想到除了让角色完成一系列“事情”之外,还有很多不同的方法来使用序列节点。想一下下面的例子:
![](http://img.manew.com/data/attachment/forum/201603/16/104942kzrmern1ssopfho3.gif.thumb.jpg)
在上面的例子中,我们没有用一系列的动作而用了检测。子节点检测角色是否饿了,是否有食物,是否在安全地点,只有这些检测都成功了之后,角色才会吃食物。这样使用序列节点让你可以在执行一个动作之前执行一个或多个检测,就像代码里的if条件,电路里的与门。因为需要所有子节点都成功,而且子节点可以是任意合成节点、修饰节点和叶子节点的组合,你可以在你的AI中创建非常强大的条件判断。
思考下面的例子,用到了上文提到的反相器:
![](http://img.manew.com/data/attachment/forum/201603/16/105033y4znogy5hmhvlu1i.gif.thumb.jpg)
与上一例子功能相同,我们展示了如何使用反相器来将任何检测取反,这样你得到了一个非门。这意味着你可以暴力地剪掉一堆节点来测试角色或者游戏的一些逻辑。
Selector
选择器
选择器与序列节点正好相反。序列节点的作用是“与”,需要所有子节点都成功才返回成功,而选择器只要有一个子节点返回了成功,它就返回成功,而且不再处理后续的节点。它先处理第一个节点,如果失败了,就处理第二个,如果再失败了,就第三个…直到有一个成功,那么选择器会立即返回成功。如果所有子节点都失败了,它才返回失败。这表示选择器是一个“或”门,或者作为一个条件判断用来判断多个条件中是否有一个真的。
它最大的优点在于它可以代表多种不同的动作组合,按照最希望的到最不希望排列优先级,如果任何一支成功了它就返回成功。它可以包含很多的结果,利用它可以快速构建出很复杂的行为树。
让我再看一下之前的进门序列案例,让它变得更复杂一点,加入一个选择器来解决。
![](http://img.manew.com/data/attachment/forum/201603/16/105033dvd44dnktilzwqs2.gif.thumb.jpg)
如你所看到的,我们可以智能地解决上锁的门,仅仅用了少数几个节点。
当选择器在处理时发生了什么呢?
首先,它先处理“开门”节点,最希望的动作就是直接开门,毫无疑问。如果顺利开门了,那选择器成功,知道了这个动作已经成功完成。那么就没有必要处理后面的子节点了。
但是,如果因为有人锁上了,开不了门,那“开门”节点会失败,将失败状态返回给选择器。这时选择器会执行第二个节点(或者第二希望执行的动作),来尝试打开门锁。
这里我们创建了另一个序列(必须全部完成才会向选择器返回成功),先打开门锁,然后尝试打开门。
如果开锁也失败了(可能AI没有锁匙,或者没有开锁技巧,或是已经撬开了锁,但发现门是固定的根本打不开?),那么它会向选择器返回失败,然后它会尝试第三种做法,把门暴力地撞开。
如果角色不够强壮,那他可能又要失败了。这时没有更多的动作组合,那这个选择器就返回失败,相应地它的父节点也返回失败,放弃穿过门的尝试了。
让我们走得更远些,可能在那个序列节点上面还有一个选择器,因为这个序列节点的失败决定使用另一套动作?
![](http://img.manew.com/data/attachment/forum/201603/16/105136zevv2pepeabw7wo8.png.thumb.jpg)
这里我们扩展了这个行为树,在最上层增加了一个选择器。左边(最希望的)我们从门进入,如果失败了,就尝试从窗户进入。实际上的实现会和这个不太一样,和我们在Zomboid项目中相比还是很简单,但是足够表达意思了,后面我们将会得到更通用和更实用的实现。
总之,我们得到了一个可靠的“进入建筑”的行为,或者进入建筑,或者通知父节点不能进入。可能根本连窗户都没有呢?这样最顶层的选择器就失败了,可能这时一个父节点会让AI去另一个建筑?
对于我以前的尝试来说,大大简化行为树开发的一个重要因素就是,失败并不意味着就要停止我正在做的事情(例如,寻路失败了,怎么办?),而是很自然地在行为树中做出自然而合适的决定。
你可以将容错机制和适应所有可能情况的可选行为组合放进去。一个Zomboid项目中例子就是EnsureItemInInventorybahviour。
这个行为接收一个物品类型,然后使用一个选择器来从几种不同动作中决定一个,来确定这个物品是否在NPC的物品栏里,包括使用不同的参数对这个行为进行递归调用。
首先,它会检测这个物品是否已经存在于这个角色的主物品栏中,这是最理想的情况,什么都不必再做。如果是的话,选择器成功,整个行为成功。EnsureItemInInventory就成功了,可以使用这个物品。
如果不在角色的物品栏中,那么它会检测角色的袋子或者背包中的内容。如果找到了,它会把物品传送到主物品栏中。这会返回一个成功,然后整个行为成功。
如果上面失败了,那选择器的第三个分支会做的的确定它是否在角色居住的建筑中。如果是,角色会走到有这个物品的容器的位置,将它拿出来。依然行为是成功的。
如果上面还失败了,就要考验NPC的手艺了。它会遍历合成菜单,找到想要的物品,还会遍历找到合成需要的原料,再递归地调用EnsureItemInventorybehaviour找到每一个原料。那些动作都成功了,我们就知道NPC拥有合成那个物品的所有原料了。角色会使用这些原料制作出物品,我们已经知道拥有这个物品了,然后返回成功。
如果上面还是失败了,那EnsureItemInInventorybehaviour行为就失败了,没有再多回溯,NPC会将这个物品列入愿望清单,在没有这个物品的情况继续生存,并在完成任务过程中寻找它。
事实是,只要拥有原料,NPC就能立即制作出来,即使没有原料也可以从建筑中取到。
因为行为可以递归的特性,如果他自己没有原料,他会尝试用更底层的原料来制作原料,如有必要还会搜索建筑,将各个阶段的物品制作出来,以制作最终想要的物品。
这样我马上就拥有了一个很复杂而且很好看的AI行为,实现方式也只是几层节点。EnsureItemInInventory行为可以在其它行为树中任意使用,适用于所有我们需要确定NPC是否拥有某种物品的情况。
我觉得有些情况下,在开发过程中我们会做得更多,会有另一个回溯,假如他急切需要这个物品,就允许NPC出去寻找它,选择一个掠夺的目标,很有可能会得到那个物品。
另一个相对优先级比较高的容错机制是,考虑别的具有相同功用的物品。如果我们实现了对临时工具的支持,当需要钉钉子时,比起穿越整个街区去一个被僵尸感染的五金店找锤子,还不如寻找不太有效的替代工具比如石头。
因为开发过程中扩展行为树很方便,可以先创建一个简单的行为“做某事”,然后使用选择器通过加入容错机制和回溯机制来减少失败的可能性。制造的回溯被加在很后面,而且也仅仅是找到装备更多的NPC,他们具有帮助别人制造物品的行为。
除些之外,如果优先级分配很合理,这些回溯操作除了要高效的实现代码,还要处理智能问题和自然决策。
Random Selectors / Sequences
随机选择器/序列节点
我不再细细探究这个了,因为它们的行为之前已经说过了。随机选择器/序列节点的工作方式就像它们的名字,除了子节点的实际操作顺序是随机的。这适用于角色对于每一种动作组合没有偏向性,给予它更多的不可预测的因素。