记得以前我在博客中,提到过一种层次化的AI架构,这种架构的核心就是定义了“请求层”的概念,用来分隔决策和行为,并通过行为请求来清晰的定义了决策和行为之间的输入输出关系,不过,当我们仔细审视这个结构的时候,发现其中貌似缺失了对于某种情况的处理,这就是我今天要谈到,如何处理“被动式的行为请求”
一般来说,我们通常所认为的决策是一种“主动式(Active)的行为请求”,比如,我按了个键,玩家所控制的角色就是做出某些行为,或者AI通过对于当前情况的判断,做出了下一步的行为决策,所以说,主动式的行为请求,就是表达了一种“我想要去做什么”的语义。在游戏中,大部分的行为都是输入主动式的行为,那FPS(第一人称射击)类游戏来说,它可能包含的主动式行为有,移动,跳跃,射击,与游戏场景中物体的交互(拾取物品,开门)等等,当玩家和NPC在行动的时候,就是在这些已经定义的主动式行为中,做出决策。在我们以前提到的那种层次化的AI架构中,可以很好的满足这些需求。
但在游戏中,还是一些行为并不在主动式的行为中,比如死亡,当角色生命值等于0的时候,游戏系统就会触发这个行为请求,它不是由玩家或者NPC所发起的,而是有游戏系统判断出某些条件满足的时候,自动触发并赋予角色的行为请求。这一类的请求,我们就可以称之为“被动式(Passive)的行为请求”,在谈如何处理前,我们可以先看看这种请求有什么样的特点。
首先它可能会覆盖原本的主动式的行为请求。比如玩家想要快跑向前走,那他就会发出一个“快速移动”的行为请求,但就在这个时候,角色正好被横在路中间的一根树干绊了一下,当游戏系统检测到这个事件的时候,就会触发一个“绊倒”的行为请求,期望做一个摔倒的行为。显然,“绊倒”这个行为不是玩家主动做出的,是属于被动式的请求,而且这个请求应该被立刻响应(要不就不真实了),所以,当比较这两个请求的时候,我们就会用“绊倒”覆盖掉原先的“快速移动”,将其作为当前请求传给行为层。那我这里为什么说是“可能会覆盖”呢?因为并不是所有的被动请求都需要被响应的,这取决于当前产生的主动和被动,这两个请求的优先级设定,这种优先级设定一般来说会来自与游戏设计的需要。假设,我们在游戏中有一个行为是“超级移动”,这种行为很强大,效果也很炫,是游戏的一个很大的卖点和可玩点,所以游戏设计者就希望尽量能让玩家触发出这种行为,以提到游戏的游戏性和画面效果,所以当玩家触发“超级移动”的时候,我们可能就会忽略掉同时触发的“绊倒”这个行为请求。
其次,它是触发式的,所以意味着同一时刻它可能会有多个。试想一下,对于主动式请求来说,因为角色不可能分身嘛(除非是超人游戏),所以一般来说,同一时刻主动请求只会有一个,也就说,你不可能既想做这个,又想做那个。而被动式请求,就完全不同,在同一时刻,可能会有多个条件被满足,然后同时触发了多个被动行为请求,比如在被绊倒的那一刻,你中了一枪,然后生命值正好减到0(真倒霉。。),所以同时就产生了“绊倒”,“死亡”两个被动请求。那我们就面对,多个请求需要权衡的情况,在上述的例子里,显然,“死亡”的优先级应该是最高的,所以它会成为最后的请求被送到行为层。
还有一个特点就是关于被动请求产生源,在角色与角色交互的时候会非常容易产生被动式的行为请求。举个FIFA的例子(对此我研究了很多:)),在足球游戏中,我们可以看到很多球员间交互的例子,比如碰撞后的摔倒,争头球的挤位,带球过程中的拉扯,躲避铲球的跳跃,等等,这些都是被动式的行为请求,是玩家不可操控的,而且这些请求,不像我前面举到的例子,是单个人身上的,而是属于一种“相互作用(Interaction)”的行为请求。它的触发和处理,都会牵涉到两个,甚至多个人的判断。
谈完了特点,接下去来思考下如何来处理这些请求。当然,处理的方案多种多样,我先抛砖引玉的谈谈我想到的两种结构。还是基于原有的层次化的AI架构模型,做一些小的修改。第一种方案采用的是比较直观的集中化的处理。先定义一个结构表示当前时刻所有产生的行为请求。
1: struct CandidateRequests
2: {
3: Request m_ActiveRequest; //主动请求,假设同一时刻只有一个
4: Array<Request> m_PassiveRequests; //被动请求,同一时刻可以有多个
5: }
然后,我们定义一个模块来负责处理这个结构,并产生一个最终的行为请求,值得注意的是,我们把当前正在处理的请求也作为考虑的因素之一,传入这个函数中。
1: class RequestFinalizedModule
2: {
3: public:
4: static Request GetFinalRequest(
5: const Entity& entity,
6: const Request& curReq,
7: const CandidateRequests& nextReq)
8: {
9: //添加选择的规则,根据当前请求和下一时刻的候选请求,抉择出最终应该做的行为
10: }
11: }
集中式的处理方式将所以处理的可能性放到一个函数中,这样的好处是便于排查可能导致的选择不正确的问题,而且对于原有的结构没有什么更改,仅仅是在决策和请求层之间加了一个处理模块。缺点就是这块的代码,随着优先级关系的复杂,会显的相对比较乱,不过将脏代码堆在一个地方,也是一种不错的设计:),下面是修改后的架构图
另一种方案是让游戏系统不直接触发被动式请求,而是触发一些标记,然后AI在决策的时候将这些标记作为决策参考信息的一部分,最终做出一个合理的行为决策,这种方案延伸了决策部分的定义,使其即能产生主动式请求,也能产生被动式请求。如果用行为树的话,可以非常好的表示出这种优先级的关系,如下图:
这种方式的优点是可以用到行为树本身对于优先级处理的优势,而不需要额外的添加模块,只需修改原本行为树的设计即可,作为游戏信息的一部分,也可以沿用原有的收集游戏信息的相关模块,做适当的扩展就可以了。缺点是,由于我们将原有的触发式的模式,变成了轮询式的,所以,可能会降低行为树的些许效率,而且对于行为树的设计也提出了更高的要求。不过从架构上来看,是比较清晰,并且能很好的融入原有的设计。修改的架构图如下:
好,就聊到这里了,不知道大家有什么想法,欢迎一起留言讨论。
————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————
博主是假定角色只能同时执行一种行为么?我觉得有些行为是可以同时执行的,比如喝水和移动,我可能需要移动的同时喝水。如果有这种需求,我是不是就只能做一个边喝水边移动的行为了?
没有,一个请求可以有多个并行行为,我这里强调的是,如何处理多个请求的情况,并且如何来接收外部事件(被动请求)