如何从零开始构建游戏AI系统?

跟怪物和角色AI打交道也有段日子了,受之前底层架构限制。没办法自由的做一些修改和扩展,现在结合这些经验总结一下,在从零构建的时候需要考虑的点和问题。

 AI应该做成什么样? 

我认为,我们不断地追求更聪明的AI,主要为了在满足玩家期望的同时,尽可能的节省我们的工作量。一个能够满足玩家期望的AI应该具备以下2个点:提供符合常识的反馈,和不符合常识的反馈。符合常识的反馈,使其增强代入感,而异于常识的反馈,则是玩家想要体验的东西。

01. 让玩家看到他觉得正确的

正确的,即是符合常识的,是符合玩家认知内应当发生的一些因果。人被砍会受伤,人被杀就会死,疲劳了动作会迟缓,看见敌人就要警惕,敌人靠近就要攻击,打的过我要追,打不过我就跑。这些都是符合我们现实生活的常识性行为。

那么让玩家在干涉环境时,看到他觉得对的反馈,就是AI的其中一个重要功能。越普适的地方,玩家接触越多的反馈,就越要如此。对于怪物AI的部分,怪物的触发与睡眠,巡逻和追击,在进入战斗时不同条件下的行为模式,以及这些行为模式之间的切换,都尽可能要符合我们的常识,除非……

02. 超出玩家的期待

我看见了一个敌人,悄咪咪地走了过去。瞄准,开了一枪,卧槽没打中,赶紧藏箱子里!卧槽他好像看见我了?……哦还好还好还好没看……他干嘛呢,无线电?……停一下停一下怎么就请求支援了,还来了一个排的人,都冲我来的吗!?救命啊!

这种超出期待的,所谓异于常识的反馈,是出于一个大的常识框架下的。敌人发现了我,所以对我进行攻击。但通常的游戏,敌人发现我后的行为,不是立刻攻击,就是立刻停止,所以呼叫援兵是异于玩家常识的。在大框架下,其选择的行为方式越超出玩家想象,就越能给玩家留下深刻的印象。这种新鲜的感觉使得他愿意去尝试,体验更多的玩法,以换取更多刺激的体验。

无论符合玩家常识,或是超出玩家期待,本质上都是设计师对玩家行为和心理的预测与应对。在这一点上,了解人的情感是很重要的。而AI系统的重要性在于,它使得设计师可以根据玩家行为变化,动态地给出不同的应对措施,同时这个设计是可重复利用的。

03. 让策划省事

对的,最终目的还是让策划省事,你们知道我曾经做boss挑战的时候干过什么吗?一个动作游戏做了个小范围内的时间轴boss,硬生生靠猜玩家行为做出来了。然后这套只是完全随机打几个组合连续技的boss花了我接近一周的时间去构思和调试,一旦玩家摸清套路,之前看上去各种预读的技能就变成了毫无卵用的杂耍,而赶完版本重新用AI来做,这套东西一个下午就配出来了,还顺带做了优化。AI相比之下,是应用面最广、量产成本最低的一个实现方式。

所以说AI相关系统的需求包含了:尽可能多的能够对不同条件的情况作出判断和回应,并且能够尽量节省策划的配置量和思考量。

 怎么设计AI系统? 

对于不同项目来说需要考虑的因素有很多,最终导向的AI相关的功能也千差万别,我就是想要一个非常拟真的生态系统,和我只想要一个一旦发现就几乎会追你一辈子的怪物AI实现方式是完全不同的。都是精细的AI判断,开放世界的AI和动作游戏的AI生效的部分也完全不一样,系统的重点在于2个部分:怎么将我们想要自动化的行为抽象和切分,还有用什么形式来编辑和储存这些AI。

01. 对大的行为模式切割

对于怪物来说,比较常见的一些AI行为模式如下:

  • 闲置:不执行任何有意义反馈的状态,像小范围的遛弯、随便蹦蹦跳跳之类的行为其实是可以包含在内的。
  • 战斗:准备从技能池中选取合适的技能释放的状态。
  • 移动:未触发战斗时,从一个位置移动到另一位置的模式,比如巡逻和脱战返回。
  • 追击:当触发了战斗后,从一个位置移动到另一位置的模式。
  • 互动:与玩家进行非战斗交互的部分,比较个性化,就先合并到一起来说。

这些行为模式其实覆盖了一个怪物可能与玩家交互的绝大部分行为,在这个基础上再进行细分,去做各个行为模块的逻辑,我认为是比较好的。

一个AI行为应当包含的是:在某个检测对象满足某种触发条件下,某个行为对象执行了某个特殊行为组合,也就可以分解成如下的几个部分。

02. 触发条件和检测对象

触发条件:攻击类、受击类、击杀类、死亡、tick/周期检测。

触发条件需要支持计数,因为有瞬间性行为,所以对条件要有一个计数有效时间。作为if(*)内的条件语句,自然也要有且或非。

检测对象:检测对象与我的关系、检测对象的相关条件(血量、距离、特殊标记、属性等)。

我将常态化的条件部分切分进了检测对象中,这样将检测对象的属性封装成了一个整体,方便调用。

03. 行为与行为对象

行为:寻路、释放技能、固有属性修改、特殊标记与取消等。

行为集:这算是我上个项目的遗恨了,多个行为的序列与并发,或者干脆将他们按照状态机的方式封装成一个行为模式,这样可以定义一套很完整的行为。

行为对象:行为对象有时未必只针对自己,比如说一个怪物头头发出指令,让其他怪物跟他一起巡逻,这相当于一个技能修改了几个怪物的行为模式,所以行为里需要对行为对象进行指定。但本着AI挂谁身上谁办事儿的原则,执行行为的个体一定是自己,不需要额外进行区分。

04. 一些特殊部分

这部分我暂时不清楚要怎么划分,因为实际上AI在游戏中的应用有两部分控制需求相对集中:交互状态的改变,和具体技能释放的选择。其中后者主要应用在一些特定战斗下,较多的技能分配上。有些团队会选择直接通过关卡逻辑来调整,有些是完全随机选择,有些是固定时间轴,更多是以上几种的混合控制。

这类AI有时候甚至会单分出一个功能来实现,比如单独控制怪物的lua脚本。而我今天说的更偏向于利用一个完整的功能将这些囊括进去。根据团队开发习惯和项目侧重不同,实现时很多方式的评估结果是不一样的。

 工具相关的问题 

关于AI方面,我之前经历过两个不同的项目。一个是走状态机,使用表格做特殊行为的检测和执行。一个是UE4,选择了原生的AIController来控制。我曾经一度非常吹捧ue4的行为树与其编辑器,但是后来也慢慢有一些想法在改变。

01. 学习成本

我本身对于程序是有一定了解的,也说不上很好吧,自己在unity写过状态机,平时能看看项目代码,瞅瞅策划失传的无主字段干什么用。学行为树的时候看了看大致思路:“哦,一个大型的if-else嵌套集合”,基本上就知道怎么做了,细节的部分程序规范一下,跟蓝图一样设计。这方面表格看起来反而有些吃力,打开一张表密密麻麻的都是数,一个一个看批注烦都烦死了。

长时间下来,我发现有时候需要程序指点的零碎地方,比预想的要多。随着经历更多的事情,我意识到自己终归和程序存在着本质差别,那么并没有自学过程序,本身没有程序功底的人呢?即便程序也是会随着经验的不断加深而重构代码,一个庞大而复杂的AI自然也是。更多时候,策划并不需要那么多数据随时可以访问,我们应该主动封闭一部分不必要的数据,来减少策划的设计负担。那么数据具体暴露到什么度,是需要程序和策划之间,根据双方的能力和默契程度不断协调的,这部分更多是需要靠经验决定的。

从我的认知角度讲,直接按照一个完整事件来封装一条AI,是最直观和容易认知的。想到一条写一条,之后再决定检测次序和优先级,组合成一个大的AI链。实际上串联排好后会发现,这种结构和行为树还蛮像的。另外,整体功能参数越复杂,思考难度就越大,功能参数越简洁,要实现同一个复杂功能可能需要的步骤就越多,这两种情况最终都是会提高学习成本的,需要根据需求平衡和规避。

02. 量产与个性化

开头我要以状态机的形式定义一些行为模式,一方面是因为这种切分形式符合人的朴素思维,另一方面是因为对其中一部分行为模式继续切分,对于设计师来说本身就是无意义的。为什么程序会存在方法这个概念?为什么会有封装、继承和多态?——因为省事=清晰=方便复用,我为什么要每次都重新在行为树里执行一遍判断是否有敌人,有敌人是否在攻击范围内,在的话选择技能对其释放,而不让他直接走“攻击模式”这个行为呢?

这部分其实是和上面联动的,就算使用行为树,这些基础的部分也会被封装成一个行为集合,这也是我为什么在前面对行为集怨念的原因,不过这部分有很多东西会需要评估如何进行封装,我目前还是持中性态度,以程序的意见为最先考量。

03. 出现bug时

上一个项目另外一个怨念的部分就是不能打断点,可憋死我了!(我不是我没有

客户端高级程序员熊宝宝给我讲:“就算把客户端代码给你,逻辑不是你写的,我不告诉你哪个类,你要断去哪断?我们自己看以前代码都需要理解和查bug的时间,你能保证断得出来,其他策划能吗?”

是这样的,而且我也不能,断点程序这玩意儿确实如他所说,并不是策划应当触碰的领域。就算程序互相之间也很难查bug,更何况策划,但我并不是想要断点的功能,而是想知道策划配置能够正确行进至哪一部分,方便定位问题。

最起码对于自己朝夕相处摆弄的功能,绝大部分人知晓一个最简单的工作原理,在这个基础上迅速定位问题,很多配置错误其实不需要再经过程序也能快速查找出来,能够有效提高双方的开发效率。这方面编辑器,尤其是内嵌在引擎可以同时保证调整和监控的编辑器,是有得天独厚优势的。

04. 后续扩展与持续维护

扩展包含两个维度:程序方面的扩展和策划表/编辑器结构的扩展。这两部分分别会引发不同的维护问题,最终都会反馈到策划这边:前者会使得功能扩展本身难以推进,后者则会让配置量越来越大,容错性和bug排除难度等,都会越来越恶化。

另一方面,由于我在这里吃过大亏,所以这件事一定要记住:一旦产品上线,所有已有功能的修改,哪怕是纯配置都要经过程序,保证在其知晓的情况下进行修改。

因为一旦上线,玩家在某个功能获得了数据(很多情况下是付费了的)并进入了数据库储存,你的配置改动也许会使得链接的数据源头改变,那么很有可能会产生脏数据。这个脏数据可以理解为无主数据,但无论如何玩家是不可能接受【清库】这种处理方式的,直接对服务器中的脏数据进行遍历处理又会极大的增加服务器负荷,最后很可能需要服务器特别为这些脏数据添加处理代码。这样有可能最终会形成策划数据和程序代码的一些废弃数据,影响整洁度,进一步阻碍下一次的功能扩展。

关于如何保证一个系统的可扩展性,我的倾向和经验是:在设计之初想的可以非常大,然后和盘托出讲给程序,之后共同阉割无关功能,到目前需求的部分。这种工作方式需要双方互相的一定程度的信任,但程序知道你未来打的什么算盘,架构上一开始就会留好那个方向的余地。当然如果自己没想好或者计划赶不上变化的时候,总会有做出难以扩展的系统的情况,那个时候我的建议也只有尽早跟上级沟通确认不能实现的效果是否是刚需,一旦有这种情况,也只能请程序老哥重新帮忙修改一下了不是?毕竟危楼之上垒得再高,还是危楼。

05. 合并分支

这块我自己不同项目的经验也不多,只能先抛出问题:编辑器在上述几个问题中大部分是优于表格的,但唯独这块有着很大劣势。在合并分支版本时,单独的某些部分的值很难被合并,ue4的map、blueprint,unity的scene和prefab都是如此,他们很难达到像xml一样快速合并的灵活性,只能作为一个整体。尤其是blueprint这类本身就是可视化编程的模块,合并某个版本甚至会需要你单独为其摆放一部分蓝图和创造新的变量。这会增加很多无谓的分支bug。编辑器输出xml化是一个解决方案,但多数策划看不懂这件事还是很容易出现,难道只能以大功能模块为切分去做更新了吗?

本文来自腾讯游戏学院,本文观点不代表GameLook立场,转载请联系原作者。

关注微信