谈一谈游戏AI - 行为树

本文主要探讨了游戏AI中行为树的应用,解释了为何需要行为树以解决状态机的局限性,并介绍了行为树的定义、构建方法、实现形式及其特点。文章还讨论了解决行为树无记忆和效率问题的策略,如上下文保存、黑板系统和效率优化措施。
不要用过去的成绩看未来,而是要用未来的眼睛看现在。

郑重说明:本文适合对游戏开发感兴趣的小白初学者,本人力图将事物用简单的语言表达清楚,但水平有限,能力一般,文章如有错漏之处,还望批评指正。

在本系列的前几篇中,我们谈到了谈一谈游戏AI - 综述 以及 谈一谈游戏AI - 状态机。今天我们来继续聊一下另一种高级决策模型:行为树。

一、为什么需要行为树

有一种很重要的认识事物的思维模型就是 why-what-how 模型,因此首先我们就要了解一下 why 的问题,看看为什么需要行为树这种高级决策模型。

在前面介绍状态机时,分层有限状态机(HFSM) 让我们能够以相对直观的方式,构建相对复杂的行为集。

但是这个设计中存在一个小问题

以状态转换规则构成的决策系统与当前状态强绑定在一起,这是一些游戏所需要的。

仔细使用状态层次结构可以减少重复转换的数量,但有时候,我们的需求不一样,可能无论处于哪个状态或几乎在所有状态,你的规则都要适用。打个比方,如果 AI对象(agent)的生命值降低到25%时,则无论其当前处于战斗状态,空闲状态、谈话状态或者其它任何状态,你可能都希望他逃跑。如果用状态机,你需要在每个状态里都做这样的判断逻辑。将来新增每个状态时,都一定把这个条件添加进去。

针对这种情况,一个理想的系统应该将决策过程和状态本身分离,这样你只需要在一个位置进行更改,仍然能够正确转换状态,这就是行为树的应用场景

二、决策的行为树表示

行为树定义

在之前的讲述游戏AI决策综述的文章中,我们提到过一个球拍击球的小游戏AI,用代码 hardcode 的伪代码如下:


every frame/update while the game is running:
 
if the ball is to the left of the paddle:
 
    move the paddle left
 
else if the ball is to the right of the paddle:
 
    move the paddle right

其实这用一种称为决策树的图表示起来更为直观:

在每次决策循环执行时,只需要从最上面的 Start 开始遍历整棵树,遇到菱形的节点,判断其条件,然后选择对应的分支,直到叶子节点。

而行为树是节点更丰富功能更强大的决策树:

  • 黄色的装饰器节点,也称为条件节点,对应 hardcode 伪代码中的 if 条件

  • 蓝色的组合节点,可以是 Sequence 顺序执行节点和 Selector 选择节点,这也类似于程序逻辑中的与、或关系

  • 绿色的行为节点,是AI逻辑中需要具体实现的比较独立的业务逻辑,如寻求帮助、巡逻、逃跑、攻击等

行为树的构建

由于行为树本身比较复杂,也由于组成树的节点类型比较固定,往往利用图形化的行为树编辑器来构建一整棵行为树,例如UE中的编辑器:

行为树编辑器在使用时,导出形式可以有多种,可以是生成对应的程序代码;也可以采取数据驱动的方式,将关键信息数据化,然后放入行为树执行引擎来执行。

三、行为树的实现形式

实现行为树有几种不同的方式,但是本质上是相同的,而且与前面提到的决策树非常相似:从“根节点”开始运行,树的节点用来表示决策或行动。但还有一些关键区别:

  • 节点的返回值有三种:成功(工作已完成),失败(无法运行)或正在运行(仍在运行并且尚未完全成功或失败);

  • 采取行动的节点将返回正在运行,表示行动正在进行;

之所以有正在运行这种返回结果,是因为很多逻辑在一次决策循环中是无法执行完成的,可能跨越很多个决策循环周期。因此在遍历这棵树时,继续上次的节点逻辑。

还有一种行为树的形式是前面一种形式的变形。笔者之前所在的项目使用过以下编辑形式:

可以看到这种方式没有所谓的选择节点,其条件逻辑和行为逻辑是并行的关系,在考察每个节点的时候,会考虑其对应的条件组是否满足,条件组本身又可以包括与、或的嵌套逻辑组合。

四、行为树的特点

通过前面的介绍,我们可以总结出行为树的几个优点:

  • 常用于比较复杂的逻辑

  • 比较直观,逻辑很清晰

  • 无状态,每次都从root开始,一帧运行不需要知道上一帧是什么状态

  • 树的分支之间互不关心、互不影响,只有树形的父子层级关系,子节点之间互相没有联系。因此很灵活,新增、扩展、删除分支都不影响其他的,很方便。而对比来看,状态机互相之间是需要知道彼此的存在的,还有转换,经常耦合在一起

  • 程序可以和策划一起配合使用。

它也有很明显的缺点:

  • 每次执行都要遍历整棵树,效率是个大问题,执行时间比状态机要长很多

  • 无状态,相当于没有记忆

关于没有记忆带来的问题,举个例子来说明:

一个农民(NPC AI)要收割作物,敌人出现了,农民逃跑,逃出了距离敌人的一定范围之后,又回去收割作物,走到敌人的范围又逃出,这样来回往复,是一个bug,可以根据情况来写代码避免,但解决方式不够优雅。

五、解决行为树的问题

上下文保存

很显然,状态机需要保存上下文来解决 AI 行为树没有记忆的问题

自定义状态

很多游戏有自己自定义的状态保存结构,一个例子如:

struct AI_RUNTIME_DATA
{
    // 定时 timer

    // 通用信息,如动态思考间隔、自定义tag标记
    
    // 移动相关信息,如坐标、速度、移动技能等

    // 追击目标,射击信息等
}

黑板

更通用一些的做法是一种叫做 “黑板” 的做法,即不关心状态数据的组织形式,统一以key-value 的形式来保存数据,这有点类似于数据存储领域 NoSQL 的做法:以Key区分不同的上下文数据,value为用户自定义的二进制数据,这样使得数据的保存通用化。

黑板还可能分为本地黑板或者全局黑板。

  • 本地黑板:作用域限定为特定的状态机/行为树层级内部

  • 全局黑板:作用域为整个游戏逻辑内

UE4中就内置了黑板的功能,方便行为树使用时候的状态保存。

效率优化

另一个需要解决的问题就是行为树的执行效率问题。

降低更新频率

对于大量AI运行的情况,例如服务器中存在的大量NPC,如果需要每帧都去做一次 Sense/Think/Act 决策行为,对应到行为树上就是从 Root 节点开始扫描整棵行为树,这势必带来大量的性能消耗。

如果不是每帧都去遍历,而是用定时器,定时思考是一种可以考虑的优化方式,其本质是降低行为树的更新决策频率。

定时思考的问题是不够实时,因此也要同时添加对外部事件的响应,否则AI就会显得很傻。

例如 FF15 中添加事件响应:

行为树中使用Active Selector节点,当指定的事件响应(OnMessage)时,打断当前行为并且重置状态

行为树与状态机组合使用

行为树本身遍历是非常耗时的,而状态机则效率较高,因此我们可以将行为树结合状态机一起使用

只有最复杂的部分使用行为树,其他情况下使用状态机保持住当前状态,而不必用行为树频繁选择分支。

例如 FF15 中的一个示例:

上图只有最复杂的战斗逻辑使用了行为树,其他情况下可以嵌套状态机来使用。

代码实现优化

树这种数据结构在实现时,往往使用指针来表示父子节点之间的关系,这对于 CPU 在执行时是不够友好的。

可以考虑将缓存不友好的树遍历,改造成缓存友好的数组遍历,以空间换时间来提高执行效率。

小结

在本文,我们谈了行为树的多个方面,包括:

  • 引入行为树的原因

  • 行为树的定义、构建以及具体的实现形式

  • 行为树的特点以及解决行为树问题的几个思路

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

零壹协奏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值