核心游戏系统架构设计

过段时间会在公司做一个讲座课程,这个文章就是在准备过程中,用来整理思路的,里面的一些内容,在以前的博客上也有提及,有兴趣的可以去翻翻,这次系统整理一下,自己的思路也更清晰了,予人玫瑰,手留余香。

首先先来定义一下什么是我这里说的核心游戏系统,一般来说,游戏可以大致分为两个部分,一个部分是我这里指的核心游戏部分,比如FPS里的射击战斗部分,或者如LOL里的战斗对抗部分,又或者是体育类游戏里的比赛部分等等。

fps lol fifa

这些是游戏里的主要玩的点,核心游戏部分可以很重,占到玩家80%以上的游戏时间,也可以很轻,甚至没有,像现在很火的列王的纷争(COK),几乎就是没有什么核心游戏部分。另一部分就是外围的辅助系统,比如装备,任务,社交等等,这部分也有玩点和设计用意,这两个部分相辅相成组成了大部分游戏的主体框架。而今天要聊的就是第一部分的核心游戏系统。

从开始做游戏到现在,我大部分的工作是专注在引擎以及核心游戏系统部分,所以今天就想来聊聊如何来设计核心游戏系统。当然这个设计不一定适用于所有的游戏,仅仅是我个人的经验之谈,希望能给大家一些参考的价值。

大多数情况下,核心游戏系统都比较复杂,牵涉到很多系统之间的协作,也和策划的需求有相当紧密的联系,国外公司一般称这类程序员为Gameplay programmer,国内公司这种职位相对较少,一般就以泛指的客户端程序员代替了,但和做外围系统的程序员不同,真正的Gameplay programmer需要对于AI系统,动画系统,物理系统都有一定的了解,因为这是核心游戏部分都会涉及的领域。正因为核心游戏系统的复杂性,所以必须要有一个适合的,灵活的架构来支持,抛开一些基本的优点,诸如可扩展性,低耦合等等要求不说,我最直观的感受,就是以下两点好处:

  • 多人合作:一个好的架构可以将系统进行合理的拆分,这样的话就便于多人协作,对于核心游戏系统来说,一般是不可能一个人单枪匹马的去完成的,所以如何去拆分任务让更多的人参与,对于项目而言是相当有利的。对于现在的AAA的游戏来说,单单一个主角,可能就会有将近10个程序员在一起制作,包括AI,行为,动画等等,所以好的架构可以保证工作效率随着人数的增加而得到提升
  • 留有“挥霍”的空间:架构是很难完美的,因为在开始设计的时候,所有的需求并不明确,特别是核心游戏系统,可能会推倒重来,重构很多次。而当游戏方向定下来了之后,一些策划的改动或者扩展,也会使得以前的架构在某些情况下变得不是很适用,这个时候就会需要一些对于特殊情况的处理,也就是所谓的“hack”,好的架构会让我们在开发后期,在不重构的情况下,有余地进行适当的“hack”,而不是在一开始就“hack”到底,导致bug满天飞。

好,接下来开始说说设计思路。

解构一个复杂的系统,有一个很好(不是唯一)的办法,就是“分层结构”(Layered structure),也就是把一个复杂系统,分成一层一层的结构,每一层都做每一层自己的事情,并且每一层都是单向依赖。这样可以把一个网状的,如同乱线团一样的复杂系统,梳理的非常的清楚。这样的例子其实很多,比如学计算机的人都很熟悉的OSI网络七层架构,这就是一个非常好的,把复杂问题层次化的典型例子,它使得每一层都可以独立设计,而且可以有明确的设计目标,层与层之间的接口也变得非常清晰。

osi

还有一个游戏的例子,就是游戏的架构,游戏其实也是遵照这层次化的设计思路来设计的,虽然不像OSI那样有一个标准化的结构,但是大部分游戏可以分为核心层(Core),引擎层(Engine),游戏类型层(Game Genre),游戏层(Game),像现在一般的商用的游戏引擎,基本就做到核心层和引擎层,再往上就是使用引擎的人自己设计和实现了,像一些大公司可能会有一些积累,就会根据不同的游戏类型在引擎层的之上抽象出游戏类型层,比如体育类游戏,射击类游戏等等,然后再开始开发实际的游戏产品。这种分层的架构设计就可以帮助我们把复杂的系统进行解构,从而实现每个子系统或者模块的功能单一化。

engine

核心游戏系统架构也可以用这样的思路来设计,这样每一层都可以由不同的人来负责,如果实现的话,在一层当中也可以进行任务分工,那下面我就根据执行顺序,自上而下一层一层来描述

总共的架构分为五层

main


第一层:更新/收集世界信息

这部分主要是要设计两个部分,一个是知识池(Knowledge Pool),另一个就是感知器(Sensor)。听上去很高大上,其实概念上很简单,知识池可以理解为就是世界信息的数据存储,比如某一个智能体需要一个这样的数据,“谁是离我最近的人”,这个数据就可以存下来,方便获取,写成代码的话,就类似于这样:

KnowledgePool().Get(WHO_IS_NEAREST_TO_ME, meEntity)

这种数据存储的数据结构,可以根据不同的情况去选择,用key-value的黑板格式,或者自定义的数据类型都可以,也可以分成多个知识池来管理不同类型的数据,设计的关键就是要有清晰的世界信息获取方式。

感知器的话,就可以理解为具体的获取数据的方法,可以定义一个感知器的接口类:

interface ISensor {
    Update()
}

这样就可以把这个感知器注册到一个感知器的管理器中,当收集所有世界信息的时候,只要遍历一遍这些感知器,就可以完成对于世界信息的收集工作:

foreach(s in sensers){
    s.Update()
}

感知器也可以分为两种,一种是全局的感知器,这种可以看成是对于游戏整个世界,或者关卡的抽象,比如势力图

im_pic_4

还有一种是个体感知器,比如听力,视野等等

sensor

当然,这只是一种思路,也可以不定义接口,而是写成不同的数据更新方法。这就是第一层,主要的功能就是为下层预备数据。

第二层往下,都是针对单个智能体的更新,也就是说需要对每一个智能体执行更新操作。关于智能体的行为,很多时候容易写的一团糟,又要决策,又要运动,又有动画,还要处理物理,有些系统呢,需要每帧更新,比如位置,有些呢,又不需要更新的这么勤快,比如决策,所以在设计上我把它分成几个层次,决策层,请求层,行为层,运动层。

第二层:更新决策层(做什么)- What to do

决策层就是负责来决策此时该智能体应该要做什么,比如我要走到某个位置,我要攻击,放技能等等,可以说,这就是传统所说的人工智能AI部分,这部分只根据当前所有的世界信息,产生“做什么”的决策,决策的内容会封装在一个“请求”(Request)的结构中,继续向下传递:

这部分的结构可以有多种选择,状态机,行为树,甚至神经网络都可以,但是有两个要点

  • 决策的时机:也就是什么时候进行决策,这里就可以用来控制决策的频率,比如离玩家很远的人可以降低决策频度,离玩家近的人,可以提高决策频度等等,类似与这种的控制都应该在这一层中得到支持和实现。
  • 决策请求的类型:这一层的输出可以看成是所有该智能体可以做的决策的总和,所以千万不要把一些下层的行为放在这里,比如寻路,接下来会说到,寻路并不是决策层的决策行为,它只是来处理“移动”这个决策的一个方法。还有比如选择动画,也不应该是决策层所要关心的内容。

还有一种特殊的模块是属于这一层,那就是玩家输入,玩家的输入说起来,其实也是一种决策,只是这个决策是通过玩家来做出的。行为树就很容易处理这个情况,将玩家输入和AI决策可以融合成一体让所有的智能体共用:

btstrategy

第三层:更新请求层

上面说到,决策层产生的输出是“请求”,请求是一种自定义的数据结构,包含所以该决策所需要传递的决策信息。那为什么要更新请求层呢?直接把当前请求传递给下一层不就好了吗?这一层的功能抽象,也是我在实践中的经验,总结一句话,“请求层”就类似于一个“防火墙”,由它来“过滤”,那些请求会被继续往下传递到行为层。

我们可以先来看一下请求层的设计,请求层的设计,借鉴了渲染中的“双缓冲”结构,把请求分为,前端请求(Foreground Request),后端请求(Background Request),前端请求就是当前智能体正在执行的请求,因为一个决策请求可能需要多帧才能完成(想象一下,移动到某一个点这个请求,就需要一段时间才能完成),后端请求就是准备执行决策请求,当一定条件满足后,就可以做了一个“Flip”的操作(前后互换),把后端请求变成前端请求,这样的话,后一层就会执行这个请求,从而改变行为了。

layered-ai-architecture-1

细心的同学会发现,在上面的描述中,有一个地方值得回味,就是Flip操作是,“当一定条件满足后”,而这个条件的监测,就是这里更新请求层的时候需要做的事情了,其实每一个决策是存在潜在的优先级的,这个优先级和策划的设计有关,比如我当前正在执行一个攻击的请求,这个时候,新的请求是一个释放技能,此时策划要求,技能释放能够打断当前的攻击行为,那这个判断逻辑就可以写在这一层中,使得当这个条件满足时,可以立即切换请求。这里的设计一般采用配优先级表,和基于规则的(Rule-based)的实现方式。

有了这一层的逻辑抽象,就可以保证它的上层和下层都不需要关心决策能否被执行,而只要关心自身的决策/行为逻辑就可以了,大大降低了上下层的实现复杂度。由于这部分逻辑相对比较繁琐,所以把这些繁琐的逻辑集中在一起,也是一种理想的设计思路。

第四层:更新行为层(怎么做)- How to do

就像补充里所描述的,行为层的职责就是怎么做,也就是如何去完成上层经过决策,经过规则的逻辑判定,最终“胜出”的那个前端请求。像前面提到的寻路,或者选择需要播放的动画,都是在这一层所完成的工作,这层的实现同样可以用行为树,或者状态机,不过我还是推荐用行为树,因为行为树可以扩展和处理更复杂的行为逻辑,比如随机,序列,并行等等。某些请求可能不是用单个行为可以完成的,需要多个行为的配合,比如完成一个技能释放,需要先集气,然后再释放,类似这样的行为,就可以用行为树来实现了,更棒的是,集气这个行为还能被共用。

skillinbev

在这一层中,会产生一系列的输出,有特效,有动画,可能还有声音等等,有一个很重要,也是必不可少的,那就是运动信息(Kinematic Information),这也是智能体最终呈现的样子,这部分内容包括空间信息(位移,旋转,缩放)和动画信息,某些情况下,智能体的空间信息可以通过物理计算在这一层直接更新,动画信息直接调用引擎的播放接口即可,但有时候这种处理还不够,那就需要第五层,运动层的参与。

第五层:更新运动层

游戏中物体的移动有两种方式,一种是动画跟随物理,比如为了解决移动中的滑步问题,我们可以做多个移动的动画,然后根据速度做融合,这样可以调出一个滑步不明显的表现,还有一种就是root motion,也就是物理跟随动画,就是物体的移动和旋转完全跟随动画中的效果,这样可以解决一些物理没有办法模拟的复杂运动。

有些时候,我们需要混合使用这两种方式,并且在位移过程中需要加上一些修正(比如为了解决同步问题),这个时候,就需要用运动层来实现。这一层的输入,就是行为层产生的运动信息,输出自然就是智能体最终的空间信息和动画了。

在实现上,建议对这两种方式进行封装,这样可以对于上层来说,接口就相对统一了。

有了这五层的设计,整个核心游戏系统的更新循环就完成了,并且每一层的功能职责和输入输出都有了明确的定义,如下图:

5th

每一层具体的设计可以仁者见仁,智者见智,并且也和具体的游戏有关,但是整体的架构基本就可以参考这样的思路,至少以我的实践来看,不会导致结构混乱,也可以更好的进行分工和合作。


最后再聊一个关于核心游戏部分网络同步和回放系统的问题(同步和回放是差不多的东西,回放只是把同步的东西存下来而已)。

其实如果有了上面的架构设计,理解网络同步就很简单了。

  • 如果同步放在第二层,那就是采用的“同步输入”(Input synchronization)的方式

inputsync

  • 如果在第三层,那就是“同步命令”(Command synchronization)的方式

cmdsync

  • 如果放在第四层/第五层,那就是“同步状态”(State synchronization)的方式

statesync

 

这三种方式是越往下传输的数据越多,但是“失同步”(Out of synchronization)的风险就越小。当然不同的方式在具体实现上,还是有很多值得讨论的地方,这里就不多说了。如果客户端和游戏服务器采用相同的语言,那就可以很方便的在单机游戏和网络游戏间切换,在单机模式下,只是本地和本地通信罢了,FPS游戏很多都是这样去实现的,其实在单机模式下,内部也是一个CS的架构,而如果需要一个服务器的版本,只是加一个宏去编译而已。

好了,就说这么多,欢迎讨论,也可以下载那个僵尸的demo,里面也采用了类似的层次结构,可以参考一下。

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

(已被阅读23,381次)

25 评论

  1. 博主你好 谢谢你的分享。我有个疑问,就是下层的状态变化怎么通知到上层?比如角色被冻住了,上层怎么知道被冻住了,自行查询?

    1. 我个人角色,可以通过一个中间件去缓存当前角色的状态,然后上层去轮询

  2. 真的是实践中 体会出来的东西 很实用,最近在优化公司mmorpg的ai代码,当前使用分层状态机去做的,可是会遇到博主说的,被动请求的问题,以及其他意想不到的问题,导致代码很凌乱,但是看了这篇文章,顿时茅塞顿开,增加一个请求层,这个真的是很有意思。
    真的不只是编码问题和经验这总简单的问题,实践出真知。 学习了。

  3. 博主,您好,对于“留有‘挥霍’的空间”这部分不是很明白。
    我的理解是:在架构的设计时预留一个特殊空间来处理在架构设计时没有考虑周到且后期无法整合到架构中的内容,也就是用于实现一些架构不支持的需求。这样可以保证在不重构架构且不破坏当前整体架构的前提下,满足策划的需求。那是否可以认为如果可能的情况下,在重构架构时最好将“挥霍”空间中的特殊需求考虑进新的架构中,以保证满足当前的所有需求? (“挥霍”空间的需求做为之后优化架构的参考)
    如果理解不正确希望博主指出,如果理解完全不着边也希望博主提点~~~~ 谢谢!

    1. 因为实践中经常发生的是,你没有办法预计后面游戏会做成什么样子,在一开始的时候,你得到的需求是不明确的,所以我们只能凭经验去设想以后可能会有什么样的需求,把能考虑到的,尽量做到架构设计中,所以一开始的设计很重要,还有一个原因是,游戏不是一个人开发的,别人在理解你的框架的时候会有偏差,经常也会把框架写乱,设计得好一点的框架,可以把这种情况控制住,让每一个人可以在使用中慢慢理解,而且即使写乱了,不至于太乱:)

  4. 博主您好,我看了你的很多文章,很多地方让我茅塞顿开,非常感谢。有一个问题我想请教一下。
    就是更新/收集世界信息我没有理解透彻。
    逻辑是不是这样的 知识池是一个库,里面包含各种感知器感知后的结果,当感知器感知到的元素发生变化时,通知知识库变更,知识库对对应数据进行更新,并通知所有监听这个知识的智能体,是这样的么

  5. 博主写的很赞,我也是一名游戏AI开发者,很多博主说的东西,在我的游戏中都有具体的使用,特别是共享型行为树,我对Planner这种AI比较感兴趣,如果博主有空可以聊聊这种AI如何在游戏中应用

  6. 最近在接触游戏AI,国内也没有什么太好的书籍,偶然间搜到了博主的网站,细细读来真是受益匪浅啊!
    希望博主继续写出更优秀的游戏AI技术文章!大力支持!

发表评论

邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

Copyright © 2011-2020 AI分享站    登录