驱动游戏世界运转的“心跳”:游戏循环及实时模拟

腾讯游戏学院 2021-08-20 3.8k
以下文章来源于腾讯游戏学院 ,作者腾讯游戏学院

“和学霸一起学”栏目推送游戏相关的专业课程内容,通过相对专业、体系的知识内容,帮助大家提升对游戏的认识水平和理解力。本篇内容源于由清华大学美术学院与腾讯游戏学院联合制作“游戏程序设计”系列课程,课程名称为《游戏循环及实时模拟》(讲师:兰翔)。

本文通过游戏循环概述、游戏计时机制与游戏循环驱动的主要子系统的介绍,并结合demo游戏《无尽之路》,具体讲解各子系统与子系统开发的方法。为了方便理解,《无尽之路》按照一种比较大的游戏方式做了架构,这个demo游戏可以在http://github.com/dreamanlan/GameDemo上查看。


《无尽之路》基本上没有什么美术资源,大都是Unity自带的模型,所以看起来比较简陋。它设计了一个地形,上面摆了很多小球,而玩家在地面上移动,而几个AI跟在玩家身后,在这个场景中大概有两千多个小球。游戏规则也比较简单:玩家通过鼠标操作移动游戏角色,游戏角色走到哪个地方,上面的球就会掉落,玩家被砸到就会掉血。

#01 游戏循环概述

各类程序及其特点


常见的程序,第一类是面向任务的被动处理软件,没有任务就可以休眠。一般包括:命令行程序、GUI(图形用户界面)程序、操作系统服务、WEB服务、MIS(管理信息)系统、ERP(企业资源规划)系统以及进销存系统和财务软件等财务系统。这类软件的主要特点,是被动处理。只有去请求一个功能时,才需要进行处理。如果不做请求,就不需要做什么,自动休眠。


第二类,是自动控制软件。在给定设定值后,这类软件就会通过调节器,进行自动调节。这类软件的特点,是主动处理,不能停止运行。特别是在工业控制中,如果软件停下来,现场可能就会失控。


而游戏程序,相对于上面两类软件,有自己的独特之处。游戏最典型的特点是,游戏世界是对真实世界的模拟。即使是小型游戏,比如棋牌类游戏,也是对现实世界棋牌的模拟。大型游戏,比如最典型的RPG(角色扮演类游戏),则是完完全全在模拟真实世界。

此外,游戏世界和真实世界类似的地方在于,即使玩家不做任何操作,游戏世界还是会正常运转。正是由于这个特点,游戏需要主动运转,而主动运转整个世界的驱动,是在游戏循环中进行的。

游戏循环的作用及层次

在游戏行业,游戏的循环被称为“心跳”。游戏循环主要有三个作用:

①驱动游戏世界的运转。游戏在每一次“心跳”时,都会进行相应处理,来驱动整个游戏世界的运转。

②驱动游戏中NPC的行为。NPC是非玩家角色,一般由AI控制。NPC的行为,也是靠循环驱动的。

③实现游戏世界与玩家的交互。

而本文所讲的游戏循环,主要指前端的程序循环。一般分成两个层次:游戏引擎循环和游戏逻辑循环,游戏循环一般是由底层的引擎循环驱动的。

游戏引擎循环


3D引擎的循环,其实是在模拟真实世界的摄像。相当于在游戏世界中,有一个摄影师拿着相机在观察这个世界。所以,在3D引擎中,摄像机是最主要的概念。

此外,3D引擎的概念,还包括可移动物件、场景和UI。可移动物件,指在游戏中可以动的物件。场景,通常是游戏中的游戏世界,游戏里所有的动作和事件,都在这个场景里发生。UI,通常在玩家和游戏交互的过程中会涉及。

在游戏中,引擎往往服务于相机、UI系统、场景管理、游戏物件的管理和支持。


引擎的主循环,主要包括上图的五个步骤。

首先是游戏逻辑的循环。引擎里会有一部分去驱动游戏逻辑的循环。NPC、可移动物件位置的变动、属性的变化等都存在于游戏循环中。

其次,游戏逻辑控制可移动物件的位置、旋转、缩放等变化,进行Transform更新。当游戏场景比较复杂时,可能会采用空间数据结构进行管理,涉及二叉树或者八叉树,以及相应数据结构的更新。

然后,是整个场景的渲染。场景渲染结束后是渲染UI,因为UI一般是覆盖在场景表面上的。

最后,是双缓冲的切换。一般在显示一帧游戏画面时,另一帧在后台就会提前写好,这样画面的显示就会比较连贯。


上图左是搭建的一个场景,包含相机视角和各种物件的展示,图右是屏幕中的呈现效果,它实际上是所有3D物件在相机近裁剪面上的投影。

游戏逻辑循环


游戏逻辑的循环是由引擎来驱动的,一般在引擎循环每一帧的开始或结束进行游戏逻辑的处理,不会嵌在中间。因为引擎的整个渲染需要加锁,如果在中间进行修改,可能会影响整个引擎的管线。

游戏逻辑的循环主要是用来驱动游戏的各个子系统,包括网络、场景、资源、游戏对象、AI、战斗、剧情和UI等。其中,游戏对象指玩家和NPC,AI主要用来控制NPC,战斗包括技能和BUFF等信息。

游戏逻辑循环一般有两类风格:

一种是事件或消息驱动,类似windows的消息循环机制,更有设计特点;

另一种是静态循环,依次调用各个子系统的Tick(即“心跳”),这种方式比较直白,容易理解。一般来说,游戏逻辑循环更常采用静态循环的方式。因为游戏开发是一个多人协作的过程,而且游戏开发的时间一般都比较长,可能会有人员的流动,所以保持简单和可理解,对开发人员来说是比较重要的事情,而且,简单对于产品质量的影响,比采用更好的技术可能会更大一些。而事件或消息驱动的方式,编写和调试比较复杂,代码不易理解,所以不太常用。

游戏循环分类

游戏循环主要可以分为三种类型。


第一类,是Windows消息主循环。以前在Windows上,都是GUI系统(Graphic User Interface,图形用户界面),Windows是消息驱动的,所以游戏的循环是在Windows消息的主循环里。相当于每一次Windows程序处理完消息队列里的消息,就会做一次游戏的Tick。


第二类,是游戏帧的回调。图中的是比较老的windows系统上的开源引擎——OGRE。它在每一帧的开始和结束有两个回调,中间是游戏引擎的循环,渲染当前的场景。两个回调,实际是执行游戏的逻辑,修改游戏世界里逻辑层的一些东西,中间通过引擎的方式表现出来。


第三类,是Unity3d。它把游戏开发做的很简单,允许开发者不用再去了解很底层的东西。它使用的是一个C#语言的脚本,可以理解成一种事件驱动。但实际上Unity3d是在MonoBehaviour上,在C++里直接调用了Awake、Start、Update和FixedUpdate这四个方法,而不是用C#的event机制。

上文提到过,游戏循环是在驱动各个子系统。每一个子系统基本上都有一个“心跳”,循环就是依次对各个子系统“心跳”的调用。循环如果是静态方式的话,可以很直观地看到各个子系统的调用。如果是事件驱动的方式,那么看到的是一个已经注册好的列表循环Tick,这个循环会随各子系统工作与否产生变化。比如,某些系统可能不需要工作,那它就可以从列表里移除,Tick的时候就会少Tick一些,所以性能上可能会稍微好一点。但对于一般游戏的系统,特别是对RPG游戏,大多数子系统都是要一直工作的,所以两种方式的差别很小。

游戏里的时间


游戏里的时间,一般有两类:绝对时间和相对时间。关于相对时间,可以用Unity的Time scale属性来解释。这个属性默认是1,当调成2、3或4时,游戏进程就会变快,这种时间就是相对时间。游戏都是通过心跳来驱动的,心跳本质上是对游戏世界的模拟,这种模拟是基于时间的。相对时间代表了一个按照某种速度流逝的时间,时间流逝后就会产生对应的影响,所以如果把这个时间的速度加快,整个演算的速度就会变快。

相对时间涉及两个概念,一个是加速播放,也称为重演。比如,在王者中,如果玩家中间掉线了,重新连接上的时候就会看到,各种动作都会加快,这就相当于重演。

另一个是和重演相对应的反演,也叫时间倒流。反演最终可以无缝做到像录像一样,就是把每一帧都记下来,这样就可以随意跳到任何一帧。但反演的实现比较难,因为可能不是每一帧都能被记下,而是定期记录一个快照与基于最近快照演算相结合。

假设某一帧里有一个NPC被杀死,那它就消失了。如果没有做任何记录,然后根据玩家操作往回反演的话,到这一帧时,只知道有一个NPC消失了,但是恢复回来后发现这个NPC很多的信息其实都没有。这是因为游戏演算的时候,每一帧都是对之前整个历史信息的积累,除非真的把NPC信息记下来,否则,反演的难度是很大的。所以,拥有时间倒流功能的游戏,一般来说技术上是比较先进的。


其次,是帧时间和实时时间。帧时间就是每帧的时间,循环里是各种各样的Tick,一般来说,取一帧的时间跟上一帧计算,这样就能知道时间流逝了多久。游戏里的逻辑,很多时候是以每一帧时间作为步长去演算的。所以,很多时候在游戏里使用帧时间就够了。

实时时间,就是真实时间,任何时候去取,取到的都是真实流逝到这个点的时间。游戏逻辑的Tick里会做很多运算,假设每秒钟需要30帧,那么每一帧最多只能有30ms的时间,但如果运算一帧需要50ms,并且在这一帧里把运算全部做完,那么帧率会下降一半,所以需要把逻辑上的一些处理,进行分帧处理。把一个操作分到若干帧中去完成,就需要用到实时时间。在运算过程中,实时关注已经算了多久,按照预想的时间片,如果时间到了,就把运算先暂停一下,到后面帧再继续处理。


最后,是高精度时间,它是和硬件相关的。高精度时间可以做到微秒级,主要用于性能分析。比如,游戏运行很卡时,就要分析原因,需要去度量每一个函数的开销,这时就需要用到微秒级时间。

此外,还有垂直同步这一概念。垂直同步在早期CRT的显示器会用到,现在的硬件其实是不需要的。如上图所示,CRT显示器是靠电子枪在屏幕上一行一行快速扫过,让这些点在屏幕上依次显示。如果在电子枪扫的过程中,更新了帧缓存,就会看到不同的颜色。比如在上图中,假设缓存更新,下面三条是蓝色的,那看到的上一帧是上面五行紫色,下一帧就是下面三行蓝色,就会出现帧的撕裂现象。为了解决这个问题,以前会等每一屏电子枪扫描完,然后让电子枪回到屏幕左上角,这个过程就称为垂直同步。为了做到每次更新都是在垂直同步的时候,游戏帧率要跟显示器帧率保持倍数关系,这样的画面显示就不会有问题。

虽然现在一般没有这个概念和需求,但在引擎里依然保留了这种方式,比如Unity也提供了三十帧和六十帧的标准帧率,而现在移动游戏的帧率一般是三十帧每秒。

游戏循环的并行


我们前面都默认游戏循环是由渲染循环来驱动的,但除了单线程之外,还有一种多线程的运行方式。只要是在渲染循环里做一次驱动,比如启动游戏逻辑后,游戏的循环就可以自己进行下去。

这种方式最典型的好处就是,可以将逻辑与引擎的线程进行分离。比如上文提到的分帧处理,如果游戏循环跟渲染循环分开,那么游戏循环不会影响玩家看到的帧率,做一个帧率更稳定的游戏就会比较容易。此时,游戏循环和引擎能够以不同的帧率去工作。比如,游戏需要六十帧每秒的视觉效果,但游戏逻辑只需要十帧每秒,这样就可以通过并行的方式去做分离。

这种运行方式的缺点是比较复杂,游戏的逻辑实际上是给引擎提供数据,对于并行这种方式来说,逻辑和引擎是在两个线程里,必然会涉及数据在两个线程之间的同步问题,就会比较麻烦。


异步编程的复杂性,最主要的一点就在于锁。假设整个Tick加一个锁,会导致不必要的等待,性能就会比较低,如果两个线程的每一个Tick,都是一个锁的话,就相当于被串行化,没有多线程了。而锁的粒度比较小,只锁真正需要同步的那些线程时,逻辑上就会非常复杂,容易引起死锁或活锁。

不同软件对于锁的使用有些差别,在游戏软件里,很少使用锁的方式,因为游戏的逻辑会比较复杂。传统软件一般会与某个行业相关,会有对应的业务模型。这个业务模型是趋于稳定的,长远来看,业务中的很多东西,除了直接面向用户的层面之外,到后期的变化就比较少。所以可以使用比较复杂的技术,因为这个技术不会被应用到所有的地方。

但对于游戏软件来说,游戏所有的设计都是由策划需求驱动的,策划做游戏有自己的追求,希望和别人做的不一样,这与软件工程的求稳是完全冲突的。而且,游戏逻辑真的非常复杂,随时都有可能变化,所以一般不会使用加锁的方式对游戏逻辑进行处理。


此外,在游戏里,如果涉及多线程,那么会比较多地采用消息队列的方式。假设有四个线程和一个主线程,尽量保证每个线程运行过程中没有其它的依赖,当有依赖时,就在线程之间加一些消息队列。消息队列本身是加锁的,但因为这个锁只需要锁整个队列,所以比较简单,不需要在逻辑上加锁。逻辑上,比如线程A需要和线程B通信,就会发一个消息,转到消息队列里进行加锁,线程B就会按照自己的步调,从消息队列里解锁,读出线程A发出的消息,这样的通信方式和前文的加锁是类似的。这种方式是比较标准的,Windows的消息队列也是以这种方式工作的。在游戏里,这种模式在后端会比较常见。

这样的异步操作,会有一个比较难处理的问题,当执行操作的数据由多个回调提供时,代码会相对复杂难写。在上图中,假设有线程ABCD,此时,ABCD 各有一个操作,都在并行地执行,而主线程上有另外一个操作,这个操作必须要拿到ABCD全部的结果才可以执行。ABCD的操作结果都会发到消息队列,再由主线程从各个消息队列中取出,但取出的时间点肯定是不一样的,有可能在第一、二、三、四帧分别取到A、B、C和D的结果。那么,当主线程拿到A、B、C的结果时,是无法执行操作的,这就意味着前面三个操作的结果,都必须做缓存,这是异步编程会遇到的比较典型的问题。解决这种问题的方法,就是一旦涉及到操作的依赖,就必须将前面的操作结果进行缓存,最后再去处理。这也是常用的命令式程序在异步处理时的一个特点,对于这个问题,暂时没有好的解决方案。

异步编程的另一个复杂性,在于调试和排错困难。主要是因为,多线程程序的调试需要设很多的断点,因为在线程A里单步调试,是永远不可能走到线程B里去的。断点的设置,就需要开发者自己去分析整个流程如何,而不像单步调试那样可以一步一步往下走,清晰简单地了解整个数据流和控制流是怎样的。

#02《无尽之路》的实现

《无尽之路》的玩法与规则


《无尽之路》这个游戏demo比较简单,用的都是Unity自带的模型。《无尽之路》是在场景里提前摆了两千多个浮空的小球,玩家在场景中移动,一旦靠近小球,就会导致小球自由落体到地面产生爆炸,爆炸会对一定范围内的玩家和NPC造成掉血伤害。玩家每触发一个小球掉落,就会获得一定的分数,但同时也会掉血。玩家没血代表闯关失败,玩家抵达中心的大球下方时,就会获得最后的胜利。

《无尽之路》的架构


游戏里没有特别明显的架构层次,下文要介绍的是比较常用的一种方式。这里的游戏架构主要是针对前端来讲的,因为前端基本没有数据,主要做的是游戏逻辑。

上图所示的架构,可以理解为两层。一层,是用一种语言去写比较基础的东西,包括通用组件和容器与子系统,另一层是用脚本语言写的脚本。这种架构层次的处理,主要是为了适应变化。为了尽可能适应变化,开发者使用脚本语言,然后其它部分写得尽量可重用。

最下层,是容器和子系统。其中,容器是为了更贴合现在比较流行的业务系统的概念,一般更强调的是子系统,即上文提及的游戏循环驱动的几个系统。游戏的业务就是体现在子系统中的,从程序角度来说,子系统主要提供了游戏的机制。比如,子系统中的任务系统,提供了可以做任务的游戏机制,之后在这个子系统上做一些配置,就可以运转起来,例如《魔兽世界》的任务就非常多。而每个游戏或多或少都会有一些子系统,每个子系统都固化了游戏的某一个方面,比如战斗、任务、剧情、UI、场景等。

中间层,是通用组件,它不是特别复杂的层次。这一层不涉及设计模式,而且和游戏的关系不是很大。通用组件是在引擎的支持下,提供游戏逻辑无关的功能。一般对引擎直接提供的功能的封装,或使用子系统的扩展机制来实现的功能属于通用组件层。仅仅有子系统和脚本,是不足以表达整个游戏的,所以需要通用组件来辅助。

最上层,是胶水层,即脚本。这个游戏demo用的不是一个标准的脚本,是我自己开发的一个DSL脚本,我们项目用来做剧情脚本,是一个基于命令队列的执行模型,它在语法上还是属于C语言风格,这块大家有兴趣可以看源码了解,不关心细节可以当作文本描述或配置来看。

《无尽之路》的系统


《无尽之路》这游戏个demo比较简单,只涉及五个子系统:场景与资源、玩家与NPC、AI、玩法逻辑和UI,剧情其实是没有涉及的,只是用脚本来实现了玩法。游戏开始是初始化,之后进入到游戏循环,整个游戏的逻辑,都是由游戏循环来驱动的。

《无尽之路》的开发方式


根据游戏架构的三个层次,游戏的开发步骤可以分为三步:①确定子系统;②确定通用功能;③使用DSL脚本实现游戏逻辑。

首先,对于子系统,要确定游戏会涉及哪些方面的系统。对于《无尽之路》,它只需要有场景、玩家、NPC和很简单的AI,以及一个脚本来实现玩法就可以了。

其次,是通用功能,它跟游戏逻辑的关系不是直接密切相关的。通用功能,一般会涉及相机、UI、小球的空间管理、重力、特效、音效与背景音乐。相机一般不被当做一个系统,它只是简单地实现游戏里以第三者视角跟随的功能。UI一般是有系统的,但这个游戏里所涉及的UI比较简单,所以将UI作为一个通用功能,为了在脚本里操作UI,会提供UI上的简单封装。

关于小球的空间管理,一般采用KD树的方式。当玩家走到一个地方时,游戏系统就需要知道,玩家周围大概有多少小球被触发。如果要完全遍历,那么玩家每走一步,系统就要计算两千多个小球和玩家的2D距离,计算量非常大,这时就会采用Kd树来进行空间管理。

Demo中的重力、特效和音效,是直接使用Unity引擎的,在demo中只是做了一个脚本提供接口,供DSL脚本调用。

#03《无尽之路》的功能支撑

UI


《无尽之路》的UI设计,使用的是Unity的UGUI。在UGUI之前,Unity比较常用的是NGUI,在NGUI的作者设计出UGUI后,UGUI更广泛地使用。Unity3d底层所提供的主要是画布和画布的渲染,这两者的应用,使得UGUI比NGUI快一些。

3D游戏里,所有的UI都是用两个三角形拼一个矩形,然后在矩形上贴图,构成玩家看到的UI系统。Mesh指的是,由一系列三角形组成的多边形网格。3D游戏都要考虑渲染的批次问题,即从CPU提交到GPU的次数,这个次数越多,游戏性能就越低。类似于上文提到的多线程加锁,加锁相当于并行变成串行。同样的,渲染批次过多,就会导致CPU和GPU之间串行的时间越长,游戏性能就会很低。为了减少渲染批次、提高性能,UI系统会把小三角形,按照某种方式拼接,构造成一个个的Mesh。

此外,UI系统通常会有输入系统,一般涉及事件分发与冒泡。事件分发是指,当玩家在UI上点击时,系统需要找到玩家触发的按钮。当玩家点击按钮,但对应的事件并没有响应时,就需要通过冒泡来逐级上报,直到容器一级的控件得到通知。由此可以看出,事件处理不一定要安装在最底层的按钮上,也可以放在更上一层,通过冒泡功能的应用,来同时进行多个按钮的处理。


UI系统也使用了一个开源库Tween,将这个脚本挂在Unity里的GameObject上,为游戏提供一些功能,包括位置动画、颜色动画和alpha动画。Tween在这个demo里,主要用来做伤害数字,即飘字。

通用功能


在通用功能中相机的实现里,屏幕输入的脚本是用C#写的。这个脚本,主要实现的是摇杆和键盘的移动,以及相机的一些调整。上图所示的相机,只是3d游戏设计中的一种传统方式。Unity加了timeline之后的相机插件,有很多虚拟相机,与图中的方式就不太相同。传统相机的表达,会有特定的目标,相机会看着或跟随这个目标。相机的参数调整,主要包括相机的yaw、相机到目标的距离和相机的高度。相机的yaw,指的是绕这个目标旋转的方向。


空间管理的实现,是依靠ObjectDetector脚本的。它使用KdTree查询玩家周围的小球,并发送剧情消息,即DSL脚本。这个过程就是根据KdTree的数据来进行处理,与游戏就没有关系了。


StoryObject主要用于处理每个角色头顶的飘字。此外,它还会处理游戏中的碰撞回调。当小球掉落到地面时,会引起爆炸,这时就需要通过C#使用引擎对应的功能,做一层转接,发消息到剧情脚本里,进行检测。在做游戏时,像这种将特定处理利用通用机制,转发到脚本进行处理的方式,是很常见的。

StoryCamera是在实现上文提到的三个参数的调整之后,具体实现相机功能的脚本。

#04《无尽之路》的机制与系统支撑

时间


时间是由TimeUtility类实现的,包括帧时间和高精度时间。

场景子系统


《无尽之路》的场景文件,直接采用了Unity3d提供的格式。场景子系统主要负责场景的加载、展示和切换。首先,场景要进行资源的加载,并实现场景的实例化,最后显示场景上的各种对象。场景的切换,一般包括清理旧的场景,并加载新的场景,最后初始化新的场景。

资源子系统


游戏服务器相对于之前已经发展了许多,而且计算机的综合能力也越来越强。

资源子系统相对来说复杂一些,主要有资源的组织、打包、加载和资源池及对象池的处理。资源的组织,是对3d资源进行分类,主要会涉及引擎资源和原始资源。引擎资源是引擎可以识别的,存放在Assets目录下或它的普通子目录(指名称不是unity约定的特殊目录)。原始资源,是开发者要用的,放在Assets/StreamingAssets目录下。

其次,是资源打包。资源打包在demo中没有涉及,但实际游戏的制作中都会用到。比如,美术做的贴图、特效模型,都是一个一个的资源文件,到游戏发布时,所有的资源会被打包为一个压缩包。Unity提供了AssetBundle的机制,用于资源的打包。

然后,是资源的加载,它对前端来说是比较关键的。资源加载的方式,有同步和异步两种。同步加载时,如果资源在读取时,当前的游戏画面类似于卡住了,那么加载的时间,会直接影响游戏的帧率。如果时间很长,玩家就会感到明显的卡顿。为了解决同步加载的卡顿问题,需要考虑两个策略:第一,是资源的预加载。即在场景切换时,提前把需要的资源加载好,这样游戏加载的时间,就不会体现在游戏过程中。第二,就是资源加载的另一种方式——异步加载。举例来讲,假设现在有一个NPC进入玩家的视野,那么就需要创建它。此时,先去发起一个加载资源的请求,等到资源加载完成,进行回调,再实际去创建NPC的形象。资源的加载在Unity中使用的机制,根据资源是否打包,是有差别的。

最后,是资源池和对象池,二者都是为预加载服务的。资源池用于保存预加载的资源,对象池用于缓存预加载中所需要的游戏对象。

游戏对象子系统


游戏对象,包括玩家和NPC等。在传统的游戏里,游戏对象的设计较多使用继承的方式,但这种方式目前不被推荐,而是更推崇组合的方式。

Unity使用的是ECS模式,它最早是在07年被提出的。游戏设计早期,面向对象是比较主流的方式,开发者很自然地按照面向对象的思维去考虑,对游戏对象进行分类,但这会产生一个问题:对游戏对象进行了分类,同一功能在不同游戏对象上的实现,可能要分别编写代码,非常麻烦。比如,游戏里的NPC和陷阱,都属于游戏对象。若按照传统的方式去分类,往往会将游戏对象分成静态游戏对象和动态游戏对象,所以陷阱是静态的,NPC是动态的。静态对象下,又可以分成各种各样的类型;动态对象下,可能会分NPC、Boss、Monster等。而当游戏的后续需求提出需要AI时,将AI与动态游戏对象进行关联是比较自然的,但若又想要静态游戏对象,比如陷阱也加上AI的效果,因为AI只在动态游戏对象上才有,这时就需要在静态游戏对象这边新拷贝一份代码。这也是在游戏开发里比较普遍的问题。

在出现这样的问题后,一些开发者发现游戏对象并不是一个面向对象的概念,不太适合按照分类的方式去管理,它更像游戏对象的数据库。游戏对象数据库的概念被提出后,开始出现了ECS模式和面向数据的设计。

所以,游戏对象设计的发展顺序是:

首先,是非继承方式的ECS模式,当时主要考虑的是游戏对象的管理,即它是OO(Object Oriented,面向对象)风格的分类方式还是数据的组织方式。

其次,在大家逐渐认可数据的组织方式之后,出现了面向数据的设计。而且,在游戏引擎层面,面向数据的设计可以很方便地做到Cache友好。因为OO的方式是按照Class把数据拆开了,在内存中,数据散落在各个不同的区域。而Cache是关联存储,只加载自身附近的数据,如果连续访问游戏对象,会不停出现Cache数据的替换,Cache基本是无法命中的。所以,现在游戏对象逐渐倾向于面向数据的设计。

剧情子系统


剧情子系统是基于DSL语法的,主要是消息和消息处理。传统的脚本是基于栈的,这个demo用的脚本,也称为剧情,它是基于队列的。基于队列的执行模型,使得剧情可以方便地实现类似协程的效果。

AI子系统


《无尽之路》用的AI子系统是比较简单的状态机的方式,这在游戏里是比较常见的。AI状态,包括休闲状态、移动状态、追击状态、战斗状态和脱战状态等。在游戏中,当一个怪物视野范围内没有玩家时,它就处于休闲状态,做出指定动作。当它的视野内出现玩家,它就会进入追击状态,不断地接近玩家,直到玩家进入它的技能范围,即攻击范围内。当玩家进入到攻击范围,它就会切换到战斗状态,选择各种技能进行战斗。如果玩家离开,它可能还会再次回到追击状态。

每一个怪物是从属于一个特定区域的,如果它追击玩家的距离过远,将要脱离所属的区域,它就会进入脱战状态,回到所属区域。这种状态机机制,在MMO(Massive Multiplayer Online,大型多人在线)游戏中比较常见。

#05 小结


关于游戏循环的知识,本文讲解的还不够全面,未涉及的部分,有网络与消息处理、逻辑数据管理、用户输入与操作、战斗系统和热更新等。

来源:腾讯游戏学院

相关推荐

最新评论
暂无评论
参与评论

Facebook游戏出海峰会
推广
商务合作 查看更多

编辑推荐 查看更多