游戏AI之行为树(下)

游戏AI之行为树(下)


Decorator Nodes
修饰节点

Inverter
反相器

我们之前已经说过了。将它们放在节点之上可以将其结果反相,成功变失败,失败变成功。它最常用在条件的测试上。

Succeeder
成功节点

成功节点不管子节点返回什么,它都返回成功。这适用于,有一个你希望或者可预料到会返回失败的节点,但是你不想让它阻止它所在的序列的运行。如果是相反的情况,你会需要一个failer节点。

Repeater
重复节点

重复节点每当它的子节点返回一个结果时,会重复处理这个节点。这适用于行为树中非常基础的部分,需要它持续运行。重复节点可以是重复执行指定次数就返回。

Repeat Until Fail
重复直到失败

像重复节点一样,这个节点也会持续重复处理子节点。但是直到子节点返回了失败,它就会向父节点返回成功。

Data Context
数据空间

它的具体实现取决于行为树的具体实现、使用的编程语言和其它因素,所以我们只在抽象和概念层面讨论它。

当AI物体的行为树被调用时,也会创建一个数据空间,作为一个存储机制来存储数据,这些数据在节点中解释和修改(使用C#中的字典、Java中的HashMap、可能用C++的string/void* STL map创建<string, object>序列,已经很久不用C++了,应该有更好的方式)。

节点可以读写这些变量,用以后续节点的处理,这样行为树就成为一个有机的整体。一旦你开始着重使用这块内容,行为树的复杂度和适用范围就非常可观了,你指尖的力量将是巨大的。一会儿当我们再次回到我们的“门和窗”行为时将会用到这个。

Defining Leaf Nodes
定义叶子节点

同样的,它的具体内容取决于具体实现。为了赋予叶子节点功能,让具体的游戏逻辑能够添加到行为树中,大多系统都有两个需要实现的方法。

Init – 当节点第一次被父节点访问时调用。例如,当序列节点要处理它的子节点时会调用这个方法,它完成了这次处理返回了之后,下一次再执行时,就不会调用init方法了。这个方法用于初始化节点,开始节点的动作。拿我们的例子来说,它会接收参数,可能初始化寻路工作。

Process – 当这个节点激活时这个方法会每帧调用。如果在这个方法里返回成功或失败,它的执行将会终止,结果返回给父节点。如果他返回Running,它会在下一帧被重复执行,直到它返回成功或失败。在我们的例子当中,在寻路返回成功或失败之前,它会一直返回Running。

节点可以拥有一些字段,可能是明确指定传入的参数,也可以是数据空间的变量的引用。

我不会讨论具体实现,因为它不仅依赖于语言还依赖于行为树的实现,但是参数和数据存储的概念是通用的。

例如,我们可能会这样定义Walk节点:
Walk (character, destination)
- success:  Reached destination
- failure:    Failed to reach destination
- running: En route

这种情况下,Walk有两个参数,角色和目标地点。我们会很自然地想到运行这个AI行为的角色是确定的,因而我们没必要明确将他作为参数传递,但是最好不要这样想,尽管对于Walk是一个很靠谱的假设。有太多次的经验,尤其是在条件节点,我在测试不同的角色状态时或者交互时总是需要修改代码,所以最好是多废点力气将角色当参数传入,即使你坚信只有那个AI会需要它。

目标地点这个参数,就像我之前说的,可以手动填入X,Y,Z坐标。但是很有可能它会保存在数据空间,被另一个节点引用,可能包含了另一个游戏物体、建筑的位置,或者可能根据NPC的所在位置计算出来的安全地点。

Stacks

第一次思考行为树时,很自然地把节点的使用范围与角色动作、条件判断或者角色环境联系起来,这将会限制你发挥出行为树的强大力量。

当我用节点实现栈操作时,我发现了这一点。所以我在游戏中加入了以下的实现:
PushToStack(item, stackVar)
PopFromStack(stack, itemVar)
IsEmpty(stack)

就是这样,就这么三个节点。它们需要的也是init/process方法,用了很少的代码实现了创建和修改标准库的栈的操作,而且,衍生出了更多可能性。

例如,PushToStack会存储传入变量名,压入栈中,如果栈不存在则创建一个。

相似地,pop方法将元素弹出栈,将值存储在itemVar变量中,如果栈是空的,则会失败,所以有IsEmpty节点来检查栈是不是空的,如果是空就返回成功。

有了上面的节点,我们可以这样来遍历整个栈:



使用一个“直到失败”的重复节点,我们可以重复从栈中弹出元素,并执行一些操作,直到栈为空,PopFromStack会返回失败,然后退出“直到失败”重复节点。

接下来是几个其它我常用的很重要的工具

SetVariable(varName, object)
IsNull(object)

这允许我们通过行为树设置任意的变量。合成节点和修饰节点未提供足够的支持来让我们获取到行为树的信息,这时它们就非常有用了。随后我们会创造这么个情景,尽管我觉得还是有方法来解决的,它不是必需的。

现在假设我们添加一个节点叫GetDoorStackFromBuilding,会传入一个建筑物体,它会从中取出门物体的一个列表,用这些物体新建并且填充一个栈,然后设置目标。我怎么用上面提到的工具来完成呢?



哎呀,搞得略复杂,一眼看过去很难知道到底在干嘛,但和任何语言一样,到最后还是很容易理解的,而且你牺牲可读性换取了复杂度。

但是它到底做了什么?一开始你可能有点头疼,但是只要你熟悉了节点的工作方法,以及失败和成功的状态是怎么传递的,就很容易理解了。如有必要我可能扩展这一部分到行为树的Walk,假如我的描述不够充分的话。

简而言之,这是一个会获取建筑所有门并进入,并且如果角色进入任意一个门就会返回成功的行为,如果未能进入,则返回失败。

首先它获取一个包含了进入建筑的所有门的栈,然后调用一个“直到失败”的重复节点,它会重复执行直到子节点返回失败。

那个子节点是一个序列节点,先从栈中弹出一个门,存储在door变量中。

如果栈是空的,那说明根本没有门,这个节点就会失败,直到跳出重复节点,重复节点返回一个成功(“直到失败”节点总是返回成功),继续处理这个序列,我们加入了一个反相的IsNull检测usedDoor。如果usedDoor是空(因为从来没有设置过这个值),这会导致整个行为失败。

如果栈确实弹出了一个门物体,就会调用另一个序列(加了反相),它会尝试走向门,打开然后穿过。

假如NPC用尽各种方法也没穿过门去(门锁了,NPC也不够强壮将其打开),选择器就失败,返回失败给它的父节点,是一个反相器,将失败反相为成功,意味着它无法跳出重复节点,然后回来再次调用它的子序列,将下一个门弹出,然后NPC会尝试这个门。

如果NPC成功进入了一个门,那么它将会将usedDoor设置为door的值,这时序列节点返回一个成功,这个成功被反相为失败,之后跳出重复节点的循环。

这种情况下,我们在IsNull节点的节点返回失败,因为usedDoor不是空。它被反相为成功,导致整个行为成功。更高一层的父节点知道NPC成功找到一个门,进入了建筑。

如果行为是失败的,那么会用一个GetWindwoStackFromBuilding节点来重复执行,来重复之前的操作从窗户进入,需要少量的节点执行栈操作。或许你可以先后调用GetDoorStackFromBuilding和GetWindowStackFromBuilding,将窗户压入门的栈顶,然后在同一个循环里处理所有,对门和窗执行相同的Open,Unlock,Close操作,还有变量的检测。

最后,你可以注意到我到close door节点之上加了一个成功节点,这是因为如果NPC是破坏掉门进去的,它关门的动作会失败。

如果没有那个成功节点,会导致这个序列返回失败,没有给useDoor变量赋值,并尝试下一个门。一个可选方案是让CloseDoor节点总是返回成功,即使门被破坏了。但是,我们想要检测关门是否成功(例如,在“保护安全屋”行为中,会将关不上门视为失败,因为门已经不在门框上,也就是不安全了!),所以成功节点可以让那个失败被忽略,如果需要那种行为。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值