行为树(Behavior Tree)详细介绍

源自: https://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php

行为树是控制AI实体决策流程的分层节点树。

在树的范围内,叶子是控制AI实体的实际命令,而形成分支的则是各种类型的效用节点,它们控制AI沿着树走,以达到最适合这种情况的命令序列。

这些树可能非常深,节点调用执行特定功能的子树,允许开发人员创建行为库,这些行为库可以链接在一起以提供非常令人信服的AI行为。开发是高度可迭代的,您可以从形成基本行为开始,然后创建新分支以处理实现目标的替代方法,并根据其需要对分支进行排序,从而在特定行为失败时允许AI具有后备策略。

行为树的核心方面在于与代码库方法不同,树中的的特定节点或分支可能需要很长时间才能完成。在行为树的基本实现中,系统将每帧从树的根向下遍历,测试树下的每个节点以查看哪个节点处于活动状态,并在此过程中重新检查所有节点,直到到达当前活动节点为止并再次选中它。

这不是一种非常有效的处理方式,尤其是当行为树随着其在开发过程中的发展和扩展而变得更深入时。

流动
行为树由几种类型的节点组成,但是某些核心功能对于行为树中的任何类型的节点都是通用的。这是因为他们可以返回三种状态之一。三种常见状态如下:

成功(Success) 失败(Failure) 运行(Running)

顾名思义,前两个通知其父节点其操作是成功还是失败。第三种意味着尚未确定成功或失败,并且该节点仍在运行。下次树被选择时,该节点将再次被选择,此时它将再次有机会成功,失败或继续运行。

此功能是行为树功能的关键,因为它允许节点的处理在游戏的许多滴答声中持续存在。例如,“走动”节点会在尝试计算路径的过程中以及角色走到指定位置所花费的时间中提供“运行”状态。如果寻路由于某种原因而失败,或者在行走过程中出现了一些其他复杂事件以使角色停止到达目标位置,则该节点会将失败返回给父级。如果角色的当前位置在任何时候都等于目标位置,那么它将返回成功,表明“行走”命令已成功执行。

这意味着该节点单独具有为成功和失败定义的固定内容,并且可以确保使用此节点的任何树都可以确保它从该节点接收到的结果。然后,这些状态传播并定义树的流,提供事件序列和不同的执行路径,以确保AI的行为符合预期。

使用此共享功能,共有三种主要的行为树节点原型:

复合节点(Composite) 装饰器节点(Decorator) 叶节点(Leaf)

复合节点
复合节点是可以具有一个或多个子节点的节点。他们将根据所讨论的特定复合节点以先后顺序或随机顺序处理这些子项中的一个或多个,并且在某个阶段将认为其处理已完成并将子节点的成功或失败传递给其父项。在处理子项期间,他们将继续将“运行”返回给父项。

最常用的组合节点是顺序(Sequence),它仅按顺序运行每个子节点,在任何一个子节点失败的点返回失败,如果每个子节点返回成功状态,则返回成功。

装饰器节点
装饰器节点可以具有子节点。与复合节点不同,它们特定地只能有一个子节点。它们的功能是根据装饰节点的类型,从子节点的状态转换接收到的结果,终止子节点,或重复对子节点的处理。

装饰器的一个常用示例是取反(Inverter),它将简单地反转子级的结果。子代失败,它将成功返回给其父级,或者子代成功,它将失败返回给父级。

叶节点(Leaf)
叶节点是最低级别的节点类型,不能有任何子级。叶节点是最强大的节点类型,因为它用于行为的定义和实现,以进行特定的功能测试或者使行为树实际起到有用动作的作用。

由于可以定义自己的叶子节点(通常只需很少的代码),因此当它们位于复合和装饰器之上时,它们可以表现出很高的表现力,并允许您创建功能强大的行为树,从而能够进行相当复杂的分层和智能地区分优先级的行为。

将组合和装饰器视为函数,if语句和while循环以及用于定义代码流的其他语言构造,并将叶子节点视为特定于游戏的函数调用,这些调用实际上用于表达状态或者情况。可以使用参数定义这些节点。例如,“走动”叶子节点可能具有角色要走到的坐标。

Leaf节点的另一种整体类型是调用另一行为树的一种,将现有树的数据上下文传递给被调用树。这些是关键,因为它们使您可以对树进行大量模块化,以创建可以在无数地方重复使用的行为树,也许可以使用上下文中的特定变量名进行操作。

几种常见的复合节点

顺序(Sequence)
一个序列将按顺序访问每个孩子,从第一个子集开始,当访问成功时将调用第二个孩子,依此类推。如果任何子代失败,它将立即将失败返回给父代。如果序列中的最后一个子级成功,则该序列将成功返回给其父级。序列最明显的用法是定义必须全部完成的任务序列,而如果其中一项失败,则意味着对该任务序列的进一步处理变得多余。例如:
在这里插入图片描述

此顺序将使给定角色走过一扇门,将其关闭在他们身后。实际上,这些节点可能会更抽象,并在生产环境中使用参数。因此,处理顺序为:顺序->走到门(成功)->顺序(正在运行)->开门(成功)->顺序(正在运行)->穿过门(成功)->顺序(正在运行)->关门(成功)- >序列(成功)->此时,序列会将成功返回给它自己的父代。
如果角色未能走到门上(也许是因为道路被挡住了),则尝试打开门或穿过门不再重要。遍历失败时,序列返回失败,然后序列的父级可以正常处理失败。

序列自然会适合于角色动作序列,并且由于AI行为树倾向于表明这是它们的唯一用途,因此,除了使角色按顺序列出角色外,尚不清楚有几种不同的方式来利用序列’事件’。考虑一下:
在上面的示例中,我们没有动作列表,而是测试列表。子节点检查角色是否饿了,是否有人在吃东西,是否在安全的地方,并且只有当所有这些都成功返回序列父级时,角色才会吃东西。使用这样的序列,您可以在执行操作之前测试一个或多个条件。类似于代码中的if语句,以及类似于电路中的AND门。由于所有子级都需要成功,并且这些子级可以是复合节点,装饰节点或叶子节点的任意组合,因此可以在AI大脑中进行非常强大的条件检查。

Inverter装饰器
在这里插入图片描述

Inverter的使用可以大大减少所需的节点数量。

选择器(Selector)
如果顺序是AND,要求所有子代都必须成功返回成功,则选择器将在其子代中的任何一个成功且不处理任何其他子代的情况下返回成功。它将处理第一个子级,如果失败将处理第二个子级,如果失败将处理第三个子级,直到达到成功为止,这时它将立即返回成功。如果所有子级都失败了,它将失败。这意味着选择器与“或”门相似,并且作为条件语句可用于检查多个条件以查看其中是否为真。
首先,它将处理“开门”节点。最可取的解决方法是简单地打开门。如果成功,则选择器成功,知道它做得很好。不再需要探索该选择器的任何其他子节点。
在这里插入图片描述

但是,如果由于某些因素已将门锁定而无法打开门,则打开的门节点将失败,并将失败传递给父选择器。此时,选择器将尝试第二个节点,或者第二个可取的行动原因,即尝试解锁门。

在这里,我们创建了另一个顺序(必须完整完成才能将成功传递给选择器),在此我们首先解锁门,然后尝试将其打开。

如果解锁门的任何步骤失败(可能是AI没有钥匙,或者没有所需的开锁技巧,或者也许他们设法撬开了门锁,但发现试图打开门时门被钉上了钉子?),那么会将故障返回给选择器,然后选择器将尝试第三种操作,将门从铰链上砸碎!

如果角色不够强壮,那么这可能会失败。在这种情况下,没有更多的动作了,选择器将失败,这又将导致选择器的父序列失败,从而放弃尝试走进门的尝试。

要更进一步,可能有一个选择器上方的选择器,该选择器随后将根据此序列的失败来选择其他操作方案?
在这里插入图片描述

在这里,我们使用最顶部的选择器扩展了树。在左侧(最可取的一侧),我们通过门进入,如果失败,我们改为尝试通过窗户进入。

装饰器节点
取反器(inverter)
取反器将反转或否定其子节点的结果。成功变成失败,失败变成成功。它们最常用于条件测试中。

成功器(Succeeder)
无论子节点实际返回了什么,后继者都将始终返回成功。在您要处理预计会发生故障或预期会发生故障的树的分支但不想放弃处理该分支所在的序列的情况下,这些选项很有用。不需要这种类型的节点的对立面,因为如果父节点需要发生故障,则逆变器会将后继节点转变为“故障节点”。

重复器(Repeater)
每当子节点返回一个结果时,重复器将重新处理它的子节点。这些通常用于树的最底部,使树能够连续运行。重复器可以选择在返回到它们的父节点之前运行它们的子节点一定次数。

重复直到失败(Repeat Until Fail)
就像重复器一样,这些装饰器将继续对其子级进行重新处理。直到子级最终返回失败,然后重复器将成功返回给其父节点。

定义叶节点
其细节取决于行为树的实际实现。为了向叶节点提供功能,以允许将特定功能添加到行为树中,大多数系统具有需要实现的两个功能。

Init-在其父级执行期间节点被其父级首次访问时调用。例如,一个顺序将在其节点被处理时调用此方法。在父级完成处理并将结果返回给父级之后,直到下次触发父级节点时,才会再次调用它。此功能用于初始化节点并启动该节点代表的操作。使用我们的walk示例,它将检索参数并可能启动寻路作业。

Process-当节点正在处理时,这被称为行为树的每一个tick。如果该函数返回成功或失败,则其处理将结束,并将结果传递给其父函数。如果它返回正在运行,它将在接下来的标记中被重新处理,然后一次又一次地重复,直到它返回成功或失败。在Walk示例中,它将返回运行,直到寻径成功或失败。

节点可以具有与之关联的属性,这些属性可以是显式传递的文字参数,也可以是对受控AI实体的数据上下文中的变量的引用。

Walk 节点(可以是character或 destination)
-成功(success):到达目的地
-失败(failure):无法到达目的地
-运行中(running):途中

在本例中,Walk有两个参数,character和destination。虽然总是假设运行Al行为的character是节点的主题,因此不需要显式地作为参数传递,这似乎很自然,但最好不要做这种假设,尽管Walk是一个相当安全的赌注。很多时候,特别是在条件节点上,我发现自己不得不重新编码节点,以测试其他字符的状态或以某种方式与它们交互。即使你非常确定只有运行该行为的AI才需要它,你也应该多做一些工作,将命令所适用的character传递给它。

堆栈(Stacks)
当我们第一次研究行为树时,很自然地要限制用于角色动作的节点的范围。或关于字符或其环境的条件测试。在这种限制下,有时很难看出行为树有多强大。当我想到将堆栈操作作为节点来实现时,我才真正明白了它们的用处。所以我在游戏中添加了以下节点实现:
PushToStack(item, stackVar)
PopFromStack(stack, itemVar)
IsEmpty(stack)

PushToStack如果不存在则创建一个新堆栈,并将其存储在传递的变量名称中,然后将“ item”对象压入其中。
PopFromStack类似地,pop从堆栈中弹出一个项目,并将其存储在itemVar变量中,如果堆栈已经为空,则失败,
IsEmpty检查传递的堆栈是否为空,如果为空则返回成功。否则就是失败。
有了这些节点,我们现在可以遍历像这样的对象堆栈:
在这里插入图片描述

使用直到失败的装饰器,我们可以重复地从堆栈中弹出一个项目并对其进行操作,直到堆栈为空为止,此时PopFromStack将返回失败并退出直到失败的装饰器。

其他几个重要的实用程序节点:
SetVariable(varName, object)
IsNull(object)

这允许我们在整个行为树中设置任意变量,而在这种情况下,复合和装饰器不允许我们获得足够的粒度来获得我们需要的树中的信息。

现在假设我们添加了一个名为GetDoorStackFrom Building的节点,在这里您传递了一个Building对象,它将检索该建筑中的外部门对象列表,用对象更新和填充堆栈,并设置目标变量。
在这里插入图片描述

简而言之,这是一种行为,它将检索然后尝试将每个单扇门进入建筑物,如果角色成功进入任何一扇门,则返回成功,如果没有成功则返回失败。

首先,它抓住一个堆栈,其中包含进入建筑物的每个门口。然后,它将调用“直到失败”重复器节点,该节点将继续重新处理其子节点,直到其子节点返回故障为止。

该子级(一个序列)将首先从堆栈中弹出一扇门,并将其存储在door变量中。

如果由于没有门而导致堆栈为空,则此节点将失败并成功退出直到失败重复器(直到失败始终成功),以继续执行父序列,在此我们对’usedDoor进行了反向的IsNull检查’。如果usedDoor为null(由于没有机会设置该变量),它将失败,这将导致整个行为失败。

如果堆栈确实设法抓住一扇门,则它将调用另一个序列(使用取反器),该序列将尝试走向门,打开门并穿过它。

如果NPC无法通过可用的任何方式进入门(门已锁好,并且NPC太弱而无法将其分解),那么选择器将失败,并将失败返回给父级,即取反器,它将失败转化为成功,这意味着它不会逃脱直到失败重复器,后者会重复并重新调用其子序列以从堆栈中弹出下一扇门,而NPC将尝试下一扇门。

如果NPC成功通过一扇门,那么它将在“ usedDoor”变量中设置该门,此时顺序节点将返回成功。成功将转化为失败,因此我们可以逃脱直到失败的转发器。

在这种情况下,我们然后在usedDoor上进行IsNull检查失败,因为它不是null。这被转换为成功,这将导致整个行为成功。父级知道NPC成功找到了一扇门,并将其穿过了大楼。

如果失败,则可以使用GetWindowStackFromBuilding节点重复相同的过程,以对Windows重复该过程。或通过对更多节点进行少量堆栈操作,也许您可以在彼此之后立即调用GetDoorStackFromBuilding和GetWindowStackFromBuilding,并将窗口附加到门堆栈的末尾,并在相同条件下处理所有窗口,直到Fail,假定Open,在门和窗的通用基础上进行解锁,粉碎,关闭操作,或在运行时检查其操作的对象。

最后,您可能会注意到我添加了一个Succeeder装饰器作为关闭门节点的父项。这是因为我想到,如果NPC砸碎了门,他们无疑将无法关闭。

如果没有后续器,这将导致序列在usedDoor变量被设置并移动到下一个门之前失败。另一种解决方案是,即使门被撞碎,也能成功地关闭门。然而,我们希望保留测试关闭门是否成功的能力(例如,在“安全的安全屋”行为中使用节点将被视为关闭门的失败,因为它不再与情况相关!)因此,如果需要这种行为,一个Succeeder可以确保忽略失败。

  • 36
    点赞
  • 159
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值