— AI分享站

Archive
Tag "架构"

记得以前我在博客中,提到过一种层次化的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: }

集中式的处理方式将所以处理的可能性放到一个函数中,这样的好处是便于排查可能导致的选择不正确的问题,而且对于原有的结构没有什么更改,仅仅是在决策和请求层之间加了一个处理模块。缺点就是这块的代码,随着优先级关系的复杂,会显的相对比较乱,不过将脏代码堆在一个地方,也是一种不错的设计 :) ,下面是修改后的架构图

layered-ai-architecture_modified

另一种方案是让游戏系统不直接触发被动式请求,而是触发一些标记,然后AI在决策的时候将这些标记作为决策参考信息的一部分,最终做出一个合理的行为决策,这种方案延伸了决策部分的定义,使其即能产生主动式请求,也能产生被动式请求。如果用行为树的话,可以非常好的表示出这种优先级的关系,如下图:

bt-flag-passive-action

这种方式的优点是可以用到行为树本身对于优先级处理的优势,而不需要额外的添加模块,只需修改原本行为树的设计即可,作为游戏信息的一部分,也可以沿用原有的收集游戏信息的相关模块,做适当的扩展就可以了。缺点是,由于我们将原有的触发式的模式,变成了轮询式的,所以,可能会降低行为树的些许效率,而且对于行为树的设计也提出了更高的要求。不过从架构上来看,是比较清晰,并且能很好的融入原有的设计。修改的架构图如下:

layered-ai-architecture_modified_2

好,就聊到这里了,不知道大家有什么想法,欢迎一起留言讨论。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

最近在做项目前期的一些调研的工作,研究并参考了几个引擎和框架的设计,包括内部引擎,商业引擎,和开源引擎,通过比较和学习后,觉得对于游戏中的实体实现,用“组合”的设计模式会比用“继承”的更为便利,想到我早些时候的一个项目里用到的一个引擎,也是实现了组合式的实体,而且对于AI程序员来说也是和“游戏实体”打交道最多的,一个好的设计可以大大的提高代码的质量和可维护性。所以,我觉得很有必要在这里记录一下,也和大家一起分享一下我的一些心得。

在学习面向对象编程的时候,一个很重要的概念就是“万物皆对象”,我们可以把现实世界的物体抽象成一个个的Object,并且通过继承的方式实现多样化的对象集。这个是面向对象编程的一些很重要的概念。我想学过的同学都应该对此非常的熟悉。

游戏中也是一样的,一般游戏内都会抽象出一个称之为“游戏实体”(Entity)的类,当然也有直接叫Object的,不管名字怎么样,概念上是相通的,总之都是对于游戏中可能的任何物体一种提炼。在引擎中用继承的方式扩展Entity是一种比较常见和直观的方式,可以充分利用到面向对象编程中的多态的优势,虽然可能大家都比较熟悉这种方式,但我想还是用一个比较简单的例子来帮助理解,假设我们仅仅做三个类层次的话,可以从Entity这个基类中先继承出,静态实体(Static Entity)和动态实体(Dynamic Entity),静态实体表示没有运动信息的实体,比如一些静态的建筑,地图的网格等等,动态实体表示带运动信息的实体,比如人物,机械,怪物等等,这个作为第二个层次,最下面的一个层次可以是具体的物件,就像我上面所具的那些例子。

继承的好处是直观,而且分工合作也不错,大家的协作可以互不干扰,但“继承式”的实体结构有一个问题,我想用过此类模式的同学应该也有所体会,就是随着开发的进行,基类的“体积”会日益庞大,因为当我们发现子类间有一些共通的地方时,我们可以选择的办法之一,就是把这个共通的地方写到他们共有的基类里面去,我们用上面的例子举例的话,当“人物”和“怪物”有一些属性是共通的时候,我们就不得不把这些代码移到“动态实体”这个类中,当然这还不是最糟,当“人物”和“建筑”有共通的东西时,我们就只能把代码写到最高层的“Entity”中了,而且这些共通的代码可能“怪物”这个类并不需要,但因为它也继承自Entity,所以也“被迫”的包含了这些代码。所以基类代码的可维护性就变的很差,虽然,好的继承关系的设计可以一定程度上缓解这样的问题,但并不能从根本上解决。

为了解决这样的问题,所以,现在很多的引擎架构里,都提出了“组合式”实体的概念,“组合式”的概念就类似与小孩搭积木,通过用不同部件来“组合”出不同的物体。

比如,对于“静态的建筑”这个实体来说,它可以由“空间属性模块”,“显示模块”组成,“空间属性模块”包括了位置,朝向等信息,“显示模块”包括了这个建筑的模型,贴图等和渲染相关的信息。那对于“人物”这个实体来说,他的模块就更多了,除了“空间属性模块”,“显示模块”,还会包括,“人物AI模块”,“动画模块”,“物理模块”等等。对于“怪物”来说,它可能包含了和“人物”差不多的信息,但我们会给它一个“怪物AI的模块,而不是“人物AI模块”。但这时,也许游戏设计师需要一个“高级的人形怪物”,它更聪明,需要有像人一样的AI,如果按照原来“继承式”的设计方式,我们可能需要从“怪物”这个类里继承,然后再从“人物”这个类里拷贝AI部分的代码,或者将这部分代码移到“动态实体”类中,以便于在两个类中共用,但在“组合式”的设计中,我们要做的,仅仅是将那些模块重新组合,用“人物的AI模块”来替代原来的“怪物AI模块”,这样我们就得到了一个全新的实体。

这就是“组合式”实体概念,这些用来“组合”的元件,称之为“组件(Component)”,或者“轨道(Track)”,这些组件是可继承的,所以从本质上来说,它将基于实体的继承,转移到了“组件”的继承上,将实体和属性分开,达到了简化实体类的作用。明白了它的概念,那从实现上来说就更简单了,我们可以在实体类中,包含一个存有组件基类指针的数组,然后在构建实体的时候,将相关的组件添加进来就可以了。然后在更新实体的时候,将所有在这个实体上的组件一并更新即可,代码我就不在这里写了。

“组件式”实体的概念和实现并不是很复杂,但效果却是惊人的,它用一个简单的设计模式,使得代码更易维护,而且,不同的程序员可以更合理的分工实现各自的组件部分。像AI程序员,就可以专注与对于AI组件的开发了,比如,可以做一系列的AI组件,对应不同的难度的AI,只要将这些组件和实体相连,就可以实现虽然怪物的外观相同(因为“显示组件”是一样的),但AI截然不同的效果了。对于3D程序员也是这样,比如,我可以先做一个真实效果的渲染组件,但可能我还想试试卡通渲染效果,那只要再实现一个这样的组件,将原来那个替换掉就可以了。

当然,“组合式”实体的开发中,也有些问题值得大家思考,最重要的一个问题就是,“如何在组件间传递数据?",最实际的一个例子就是,“空间组件”中存有实体的位置信息,那其他的组件开发者肯定是需要得到这些信息的,所以在组件间传递数据的功能是必不可少的,我不推荐直接开放诸如GetComponent这种直接得到组件指针的接口,那如何安全而有效的传递数据呢,这个仁者见仁,智者见智问题就留给大家讨论了。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

第一部分

上次我们说到,游戏中的运动系统一般有两种方式,“动画配合运动”以及“运动配合动画”。对于第一种方式,由于是采用运动函数或者经验数据表,所以可以很简单的将“未来时间”带入其中,来预测未来某一时刻的运动结果。但对于第二种情况,因为所有的运动结果都是从动画中取得的,如果不知道动画信息,就无法知道相应的运动结果,所以,简单的将时间带入是不能作出预测的,对于这样的情况,如果我们要做预测的话,就不得不将动画的因素考虑在里面。

举个简单的例子,一个人做一个跑动中转身停下的行为,假设他当前时刻T1,处于A1位置,速度是V1,朝向是F1(假设和速度方向一致),他的目标状态是速度是0(没有速度),朝向是F2,位置不指定。这时当我们给出未来时间T2,应该怎样来做这个运动预测呢?

PredictableLocomotionEx

和第一种运动系统不同的是,这里我们需要一个额外的模块,称之为“动画选择模块”。在我们实际要去完成这个运动目标的时候,我们会用“动画选择模块”去选出每一步的动画,比如最直观就是“直线跑”,然后会产生一个“急停”使速度从V1到0,最后是“原地转身”使朝向从F1到F2,当这些动画做完后,我们就可以得到这个人最终的“目标位置”。由于是运动配合动画的,所以对于他的最终位置,我们完全是靠这些动画实际的运行结果而得到的。

PredictableLocomotionAnimseq

当我们要做预测的时候,显然我们也是需要用“动画选择模块”做预先的动画预测的,所以,在设计可预测的运动系统的时候,就需要将“动画选择模块”独立出来。如下图所示:

PredictableLocomotionSystem

动画选择模块的输入是“当前状态”和“目标状态”,输出是一个动画的序列,这就有点像以前我们讨论过的“计划器”(Planner),相当于为我们的运动做了一个计划(对于这样的计划器的实现,就不在这次的讨论中了)。有了这样的计划后,当我们将时间T2带入,就可以通过读取动画信息的方式来获得T2时刻的运动信息了,见下图,AT指动画的时间(Animation Duration)

PredictableLocomotionTimeSeq

对于给定的“当前状态”和“目标状态”,都会有一组动画序列与之对应,所以如果是基于目前的运动状态所作的预测,那我们可以重用我们已有的动画序列来提高效率。如果是对于假想的运动状态的预测,那我们就需要用“动画选择模块”重新做一个新的动画序列,然后再得出未来时间的运动状态,这也是为什么我们需要独立的“动画选择模块”的原因,但如果频繁做较长步骤的假想预测,可能会产生一些性能上的问题,这是需要注意的地方,当然,这也取决于“动画选择模块”实现的复杂度和动画的丰富程度,比如在上个例子中,如果我们有一个“急停转身”的动画,那我们就可以减少动画序列的个数,也就是减少了计划的步长了。

另外,在实践中,我们会采用两种运动方式混用的情况,比如对于“直线跑”,我们会用运动函数来实现,而对于“转身”,“急停”这样的行为,我们会采用第二种方式来实现,对于这样的运动系统,也可以用到这样的“计划器”,只是这个计划中的某一步换用函数方式罢了,对于上面例子,我们可以参考下图:

PredictableLocomotionTimeSeq2

除了预测未来时间的运动状态外,可能我们还会预测到达某一个状态所要用到的时间,当我们有了上面的系统后,这也会非常容易做到,比如上面的例子,如果我们要预测他到“目标状态”需要多少时间,那我们只需要把动画序列中每一个动画的时间求和就可以了,T = T1 + AT(Run) + AT(Scram) + AT(Turn)。

可预测的运动系统对于某些游戏可能是一个非常重要的系统,希望上面的讨论对大家有所帮助。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

前端时间举家出游了一次,加上国庆期间一直跑东跑西,博客的更新就一直没跟上,距上一篇文章也是好久了呢,有时感觉一个人维护也有点小累,所以如果大家有好的想法,好的分享,也可以投稿给我,我想能有这样一个分享的平台,让志同道合的朋友一起讨论学习,也算是为中国游戏技术的发展贡献点绵薄之力,虽然我是付不起稿费的,但看着文章的浏览数和回复也是挺有成就感的事情,对吧 :)

好,言归正传,这次想大家来讨论讨论如何来做一个“可预测的运动系统”。首先来说说什么是游戏里的运动系统。

对于游戏里某一个智能体(就是带AI的物体),当AI决策结束,并且告诉行为层要干什么,行为层就会让它“动”起来,动的过程中可能是没有动画的,比如一辆车会前进,会后退,也可能是有动画的,比如一个人会走,会跳。显然一辆车的运动方式和一个人的运动方式是不同,那具体控制它如何运动,就是有专门的“运动系统”来负责了。

严格意义上来说,“运动系统”的实现并不在AI的范畴内,而是属于“游戏物理引擎”部分,但在某些时候,作为AI程序员,会对运动系统提出一些特殊的要求,比如今天我们会说到的“可预测性”,这些要求会用来来辅助AI系统的决策。在我现在的项目中,我们对预测的要求相当高,希望在决策的时候能做一个精确的运动预测,但比较遗憾的是,现有的引擎由于这样那样的原因,并不能提供这样一个精准预测,预测结果和运行结果存在较大的差距,所以我在抱怨的同时(因为我用的最多嘛),也在思考如何做这样一个系统,把想到的一些东西也记录在这里。

一个比较简单的“可预测的运动系统”就是匀速直线运动,我们可以根据匀速直线运动的公式,来预测将来任意时刻的位置,速度等运动信息。当然,游戏里的运动系统要复杂的多。一般来说,游戏里的运动可以大致分为两种:

  • 动画配合运动
  • 运动配合动画

第一种就是运动和动画是分离的,我可以用任何的运动函数来移动物体,动画只是配上去的效果而已,这种方式有动画和没有动画,对于它的运动效果来说是不变的,动画只是一种锦上添花的表现,因为运动方式都是自己定义的,所以我们可以很容易控制它运动效果,但缺点是,可能会产生“滑步”的现象,就是人感觉在太空漫步一样,可以想象我们用跑步的速度配一个走路动画的效果。

第二种是物体的运动效果是跟着动画的,如果动画中移动了1米,那我在播放这个动画的时候,这个物体也就移动了1米,这种方式就不存在一个所谓的“运动函数”的概念,因为都是动画数据驱动的,所以运动系统基本上就是从动画中取得当前物体的位置,这样的好处就是避免了“滑步”的问题,但是对于程序员来说,它不可控,完全取决于数据。

所以,当我们想要做一个“可预测的运动系统”的时候,就不得不考虑上面的两种情况,因为在不同的游戏中,运动系统的实现是不同的,有的用了第一种,有的用了第二种,有的是在第一和第二中之间切换的。

为了更好的表述“可预测的运动系统”,我需要定义一个运动状态的结构来描述当前物体的运动状态:

 1: struct PhyXState
 2: {
 3:     Vec3 m_v3Position;
 4:     Vec3 m_v3Velocity;
 5:     Vec2 m_v2Facing;
 6: };

这个结构里包括位置(3维向量),速度(3维向量)和朝向(2维向量),这是描述物体运动状态的三个基本量,对于通用的“可预测的运动系统”来说,它的输入输出就可以这样来描述:

PredictableLocomotionSystem1

输入是当前状态(Current PhyXState),目标状态(Target PhyXState),当前时间(Current Time)和想要预测的未来时间(Future Time),输出就是在未来时间的状态(PhyXState at Future Time)。特别要指出的是,如果预测的运动状态是基于当前的运动行为,那我们可以不传入“目标状态”,“当前状态”和“当前时间”,仅仅传入“未来时间”即可,因为对于当前运动行为而言,其内部已经保存了“目标状态”,“当前状态”和“当前时间”了。但作为通用描述,还是将这三项列在其中。

我们仔细考虑的话,会发现,其实运动系统天生是带有些许预测功能的,当每一帧在更新的时候,就是向运动系统传入了这些参数,然后得出了当前运动物体应该处于的运动状态,在这些参数中唯一值得注意的是“未来时间”,在正常的游戏循环中,“未来时间”是被指定为:

未来时间 = 当前时间 + 步长(Time Step)

也许你会觉得问题似乎是解决了,因为这样的话,不是就可以精确预测任意时候的运动状态了吗?

但回想一下我上面所说的游戏里用的两种运动方式,那问题就并不是这么简单了,对于第一种运动系统,确实不怎么修改就可以实现精确的预测,因为这种运动系统一般是基于数学函数,或者经验数据表(就是预先算出不同输入下的运动数据值,然后通过查表的方式来返回结果),要做的仅仅是封装一个接口,但对于第二种,或者对于混用的运动系统,我们就需要考虑更多的问题了,……。

(待续)

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

前段时间,谈到了一种层次化的AI架构,通过“请求”来隔离出“决策层”和“行为层”,这种架构的核心是通过“请求”来起到承上启下的作用,并由此得出当前AI所应当有的行为,所以可以称之为“请求导向(Request-Oriented)”的结构。这种结构比较适用于需要频繁做AI决策的游戏场合,比如体育类游戏等,一旦变换了请求之后,行为层就可以迅速的找到相应的行为来完成请求的内容。

这次我们换一种思路,来讨论一种“目标导向(Goal-Oriented)”的AI结构,这种结构对于一些持续性相对较长的AI策略来说,是非常适用的。还是先举个例子吧,我平时很喜欢玩星际(没玩过的同学,可以看我下面的描述),那假设我用单个农民(SCV)造一个人族的重工厂(Factory),造重工厂的前提是需要200水晶(Mineral)资源,100气体(Gas)资源,而且必须要先造好兵营(Barrack),兵营是150水晶资源,还有在采集气体资源前,需要造一个气矿(Refinery),气矿也需要100水晶资源,具体流程图如下:

GoalOrientedExample

在这个流程里,我们为这个农民设定的“目标”就是“造一个重工厂”,由于在游戏中,要完成这个目标前,需要有其他一系列的预备行为,如果我们为这个农民写AI的话,它就需要根据上面的流程一步步的来完成我们要求它的“造一个重工厂”的目标。由于所有的AI行为决策都是由指定的目标引起的,所以我们把它称之为“目标导向(Goal-Oriented)”的AI架构。

由上面的例子我们可以看到,一个目标之后,必然会有一系列的行为与之对应,而这些行为就是为了最终完成这个目标,那这些行为是如何串联的呢?或者说,我们如何来做这样的行为计划呢?在“目标导向(Goal-Oriented)”的AI架构中,一般都会存在一个称之为“计划器(Planner)”的模块,由这个模块负责生成这样的行为序列,计划器有两种工作方式,一种是静态的,一种是动态的。

静态的计划器是在游戏设计时,对于所有已定义的可用目标,预先设计好计划流程,就像我们上面这个例子,我们就对“造一个重工厂”这个目标预先设计了一套计划流程,当游戏中的农民收到这个目标后,就会按照我们定义的这个流程来做。

动态的计划器是在游戏运行时,实时的根据目标来计划行为,因为我们看到,计划中每一个单步的行为都存在“前提(Precondition)”和“效果(Effect)”两个部分,“前提”就是做这个行为需要满足的条件,“效果”就是这个行为对于游戏世界的影响。还是用上面这个例子,像“造兵营”这个行为,它的前提就是“需要有150水晶资源”,它的效果就是“当前存在一个兵营”,再如“造重工厂”,它的前提就是“需要200水晶资源”,“需要100气体资源”,“当前存在一个兵营”,它的效果就是“当前存在一个重工厂”,可以看到,前一个行为的效果会成为了下一个行为的前提,所以有了这样的定义,我们就可以通过一些算法,来把可用的行为串联起来,形成一个计划,这样最后一个行为的效果,就是我们所定义的目标。

除了核心的计划器模块,我们还需要目标选择(Goal Selector)模块,来选择我们当前要完成的目标(相当于更高层的决策层),还有一个就是计划实施(Plan Stepper)模块(相当于行为层),用来按部就班的完成计划中各个行为,这两个模块比较好理解,就不多说了,有了上面几个部分的定义,我就可以画出这样的“目标导向(Goal-Oriented)”的AI架构图:

GoalOrientedArchitecture

由上图可以看到这种架构也是相当清晰的,核心的计划器部分在具体实现的时候,还有各种各样值得讨论的地方,本文作为抛砖引玉的作用就不多提了,以后可以继续讨论。现在的游戏AI中,这种Goal-Planner架构的也占了相当大的比例,在动态的计划器的驱动下,可以呈现出多样和逼真的AI行为,希望这篇文章对大家了解这种架构起到一些帮助。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

这篇文章针对于已了解行为树和黑板的读者,如果不是很了解,请参考此处(123)。

黑板(Blackboard)是一种数据集中式的设计模式,一般用于多模块间的数据共享,我在做行为树的过程中,发现黑板非常适合作为行为树的辅助模块来使用,这次就来谈谈如何在行为树中使用黑板。

行为树的决策一般要依赖于外部的输入,如下图所示。

输入内容的来源取决于行为树用在整个AI架构的哪一层,可以是游戏世界的信息,或者是上层模块的输出。输入的形式,可以是分散的(Decentralized),也可以是集中的(Centralized)。举个例子来说,如果我们做一个战士是移动,还是攻击的决策,这是决策层的行为,所以输入内容就是游戏世界的信息,它可能包括战士自身状态(在模块A中),敌人状态(在模块B中),装备物品情况(在模块C),地图场景情况(在模块D中)等等,所以,当我们搜索和执行行为树时,我们需要从4个模块中获取信息来帮助决策,这样的方式就是我上面说的分散的方式,它的好处是调用非常直接(可能是用多个Singleton提供的接口),没有数据冗余,缺点是使得行为树对于数据的依赖度太分散。

集中的方式的话,就是我们可以定义一个数据结构专门用于行为树的输入,将上面提到的需要用到的数据,在进行行为树决策前,先从各个模块中收集到这个数据结构里,然后再递交给行为树使用。集中式的输入减少了输入和行为树之间的接口数量(只和预定义的数据结构通信),但缺点是,存在数据冗余。不过,我们可以看到集中式的数据输入使得行为树的表现更像一个黑盒了(可以伪造数据来测试行为树),这也是我们一直以来想要的。可以参看下面对于两种方式的示意图:

BehaviorTreeInputModule_1

基于上面的原因,黑板(Blackboard)这样一个概念正好符合我们的需要,所以我们就可以用黑板从各个模块中来收集行为树决策和执行的过程中需要用到的数据,然后提交给行为树使用。值得注意的是,这块黑板对于行为树来说是只读(Readonly)的,行为树不允许修改和添加任何信息到这块黑板上面。因为很难从程序上去限制(就算用const,有时为了方便还能强转成非const),所以限制只能是一种规则,或者说约定。

说完了外部世界的黑板,我们再说说另一块可能会被用到的黑板。这也可以看成是对上面这块只读黑板的补偿吧, :)

在行为树的使用过程中,发现有时候节点和节点间,行为树和行为树之间确实需要有数据共享,比如对于序列(Sequence)节点来说,它的执行行为是依次执行每一个子节点,直白一点说的话,就是执行完一个再执行下一个。一般用到序列的行为,其子节点间总会有一些联系,这里就可能存在节点间通信的问题。再比如,在一些团队AI的决策过程中,当前AI的行为树决策可能需要参考其他AI的决策结果,所以这样就存在了行为树之间需要通信的情况。

所以,在实践过程中,我们还会定义另一块黑板来负责行为树间和节点间的通信需求,示意图如下

BehaviorTreeInputModule_2

可以看到这块黑板是又可以读又可以写的,为了防止黑板混乱的问题(可以参看我以前对于共享数据的文章),我们必须在使用时规定一些限制,可以称之为黑板数据的“作用域”,我们知道很多编程语言里,变量都是存在作用域的概念的,有全局,有局部等等,借鉴于此,我也在这块黑板上规定了作用域,由上面的分析我们可以将黑板上的数据分成如下几种作用域

  • 全局域(G):此数据可以给其他行为树访问
  • 行为树域(T):此数据可以给行为树内的任意节点访问
  • 指定节点域(N):此数据可以给指定的行为树内的某节点(可以是多个)访问

这样的话,黑板的混乱程度就会好很多了,我们可以提供相关的接口来帮助操作黑板上的这些变量。

好了,基本上黑板在行为树中的应用就聊这么多了,希望对大家在使用行为树的过程中有所帮助。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More

记得在以前的一篇文章中谈到了一种类似于双缓冲的AI结构,最近在整理一些东西的时候,发现这样的AI结构具有一定的通用性,而且层与层之间耦合度相对较低,作为一种层次化的AI架构,非常值得一谈。

在我的脑海中,AI一般分为两个部分,一个是决策(Decision)部分,一个是行为(Behavior)部分,决策部分负责做什么,行为部分负责怎么做。在一些国外的公司里,AI程序员也大致分为这两种(不过,一些国内的企业可能就分的没有这么细,一般都是统称为AI程序员,或者有的分的更粗,将一些游戏中的其他游戏逻辑部分一起涵盖,统称为游戏性(Gameplay,GPP)程序员)。正因为这样,所以我们一般希望,在AI架构上,这两个部分的耦合度是相对较低的,这样也便于任务的分工。所谓层次化的AI架构(Layered AI Architeture)也就基于了这样的理念。看下面这个图:

layered-ai-architecture-1

在这样一个层次化的AI框图中,我们定义了“请求(Request)”这样一个概念,请求可以看作是AI决策的结果,或者称之为一个命令,比如,在射击游戏中,请求可能就定义为,射击,移动,逃跑等等,在动作游戏中,请求就会定义成攻击,格挡,跳跃等等。当行为层收到上层的请求后,就会设法去处理该请求的内容,还是以射击游戏为例,当行为层收到射击的指令,就会从射击的动画列表中选择某个射击动画,然后转向目标,播放动画等等工作来处理射击的请求。所以请求相当于就成了决策层和行为层之间的接口。这样,对于决策层和行为层的输入和输出就很明确了:

  • 决策层:输入(游戏世界信息),输出(请求)
  • 行为层:输入(请求),输出(修改游戏世界的相关信息)

layered-ai-architecture-2

由于有请求层作为中间接口层,所以决策和行为部分就很自然的分开了,而且有清晰的输入和输出,AI团队中的人员的工作职责也就很明确了,做为决策层的AI程序员,就只需要关心如何产生请求,而行为层的AI程序员,只需要关心如何处理请求,一旦定义好完备的请求内容,不管在代码还是在工作上都不会产生很大的粘连度了。

另外值得注意的是,这边的请求层用到了类似双缓冲的结构,分成后端和前端,换个词的话,可以说成当前在处理的请求(前端),和下一个要处理的请求(后端),具体的分析可以参考我以前的文章在AI结构中用双缓冲》,这里就不多做介绍了。

这样的层次化结构在AI中有很强的通用性,因为这是用最高的层面来总览AI的架构,而像其他诸如行为树(Behavior Tree),分层状态机(HFSM)等都可以看成是在决策或者行为层中的具体实现方式。所以不管AI代码是如何实现,大部分都可以归到这种层次化的结构中,因此,我想,我们在设计AI结构之初,就可以用这样的方式来思考和架构整个框架,分割决策和行为,定义请求,然后再针对每一层来选择具体的实现方法。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————

Read More