不要用过去的成绩看未来,而是要用未来的眼睛看现在。
郑重说明:本文适合对游戏开发感兴趣的小白初学者,本人力图将事物用简单的语言表达清楚,但水平有限,能力一般,文章如有错漏之处,还望批评指正。
在本系列的前几篇中,我们谈到了谈一谈游戏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 在执行时是不够友好的。
可以考虑将缓存不友好的树遍历,改造成缓存友好的数组遍历,以空间换时间来提高执行效率。
小结
在本文,我们谈了行为树的多个方面,包括:
引入行为树的原因
行为树的定义、构建以及具体的实现形式
行为树的特点以及解决行为树问题的几个思路
本文主要探讨了游戏AI中行为树的应用,解释了为何需要行为树以解决状态机的局限性,并介绍了行为树的定义、构建方法、实现形式及其特点。文章还讨论了解决行为树无记忆和效率问题的策略,如上下文保存、黑板系统和效率优化措施。
3214

被折叠的 条评论
为什么被折叠?



