本期为GAMES104《现代游戏引擎:从入门到实践》视频公开课文字实录第5期。本课程由GAMES(图形学与混合现实研讨会)发起,游戏引擎技术专家王希携手游戏引擎一线开发者共同研发。

课程共计22个课时,将介绍现代游戏引擎所涉及的系统架构,技术点,引擎系统相关的知识。为配合学习实践,课程组在 GitHub 上开源了小引擎Piccolo,上线1个月即获得了2900+star, 累计下载量已超过20000+。

以下内容为公开课视频转文字版本,为阅读通顺,有删减

引言

上节我们讲了游戏引擎的五层架构,这节课会真正给大家讲清楚现代游戏引擎的五层架构到底是什么,具体是怎么做的。

GAMES104实录 | 引擎架构分层(上)

学一个东西最好的方式是什么?是练习它。我们给小明同学出了一道难题:学了上节课,你觉得已经懂了游戏引擎怎么做,那就做个小小的挑战吧:在游戏中做一个可以动起来的角色,然后从下往上把这些代码写到游戏里面去。

小明想,既然知道了引擎的五层架构,我就要去想每一个功能模块到底怎么写。这其实是一个非常有意思的脑力劳动,同学们可以先停一下,回顾一下我们讲的游戏引擎的五层架构:平台层、核心层、资源层、功能层和工具层。

如果小明想做一个可以动起来的角色,在游戏引擎中的代码该怎么去写呢?这件事情比大家想象的稍微复杂一点。

在这一讲中,我会跟大家讲每一层是什么以及每一层的要点是什么。

我们就从小明的挑战“做一个动起来的角色”开始,逐步带领大家进入引擎的深水区航行。

01「资源层」

从资源(resource)到资产

首先,小明遇到的第一个挑战:好不容易求做美术的好朋友用3DMax做了一个很可爱的小角色,还做了很多的贴图和动画。那这些东西叫什么?叫resource,资源。

每一个数据的格式都是不一样的,有的是Max文件,有的是PSD文件,这些数据肯定不可能在引擎中一一打开。大家想想看,如果写个小软件,要加载个声音,我要去问你声音是什么格式?MP3还是WMV?但在游戏引擎那么复杂的环境里面,不可能去读这么多复杂的数据格式。

比如像PSD、Max、Maya这些数据格式,它们工具做得非常复杂,这些数据如果直接在游戏引擎中加载,效率会非常低。所以会做一步转换,会把这些数据全部转换成引擎的高效数据,这个转换发生之后,我们就把它叫做Asset(资产)。

从资源(resource)到资产,有一个很大的不同是什么呢?

举一个简单的例子,如果是一张贴图,无论是JPG还是PNG,它里面有很多的压缩算法,如果在GPU中以它的数据格式存储的话,绘制起来效率是很低的,但在这一步处理(转换)的时候,会把所有的贴图,不管你是什么格式,都把它转成DPS格式。我们在游戏中看到的所有漂亮花材的颜色,都是用这种格式直接放到显存中。

这一步导入帮我们做了很多工作,包括在Max里面编辑一个漂亮的mesh。如果发现mesh文件特别大,一旦把它导成引擎的asset,你会发现很多编辑的数据全部扔掉了,这种感觉像什么?

就像这篇文章,我用word去写,那word保存下来的文件会比较大。但是,如果只取文章里的内容,把它变成txt,我们就会发现这个txt文件非常小,导入做的工作就是这个。所以小明同学第一步就要把这些数据引擎化,变成资产。

GUID,游戏资产的身份识别号

大家要关注的第二个问题是什么呢?

比如要表达这个可爱的小机器人(上图),首先这个小机器人有形体、贴图以及一大堆的动画资源,这些资源实际上是关联在一起的,但在引擎代码中没有一行代码说明这些资源之间是什么关系,这时候我们要定义另外一个资产。

这个资产就叫做composed asset。

它相当于一个关系脚本,我简单地做了个案例,定义了一个资产叫做Robot,它的mesh是什么?纹理是什么?该用什么动画?当引擎读取这一段composed asset的时候,它就知道要加载这些资源,所以它本身也是资产之一。

这里面要讲个概念,在游戏引擎中,特别在现代游戏中,其实核心的功能是数据之间的关联,或者叫做reference。大家如果打开任何一个现代游戏的安装目录,会看到很多很多文件,现在游戏发布的时候,一般都会把它打包成几个大的package,那里面有大概几十万甚至上百万个文件,而这些文件之间,有的是贴图,有的是声音,有的是动作,它们都像无数的网关联在一起。这里面大家会遇到一个非常有意思的概念,叫做GUID(唯一识别号)

在现代游戏引擎中,我们希望给每个资产设置一个唯一识别号。打个最简单的比方,比如这个小机器人,贴图、mesh这些资产,现在是通过路径找到的,但这就像你只记得朋友的住址,那朋友搬家了怎么办?

很简单,每个人有一个身份证号,这样你的朋友搬到哪里都没有关系,只要这个身份证号在,你永远可以找到他。这个GUID的全称叫做全局唯一编号,实际上是游戏资产的身份识别号。

runtime asset manager

把游戏原始散乱的文件变成资产,进入到资源系统的时候,我们需要一个实时的资产管理器,这个资产管理器其实非常简单。

读了这么多文件,在引擎中就会管理,如同刚才讲的那个Reference关系一样,这些资产在资产管理器中,一般叫runtime asset manager(这里runtime指运行时或者实时)。

游戏跑起来也是一个一个文件,放在这掐头去尾就只是内容,但它们会互相指向对方。在游戏引擎设计中有一个很重要的系统,叫做handle系统。handle系统简单解释就像邮箱一样,你可以搬来搬去,我也不知道你在不在,但是我始终有你邮箱的钥匙,我知道你是105号邮箱,邮箱也知道你是105号邮箱的主人,这样,邮箱的主人在不在,我只要问这个邮箱就知道了。(这个地方比较深,我们以后会细讲)

简单来讲,游戏中最核心的是管理所有这些资产的生命周期,所以资源层是游戏引擎非常核心的一个层次。

为什么生命周期管理会这么重要?大家可能没有意识到,现代游戏中,比如我一路闯关杀降,从关卡A到关卡B再到关卡C,每一次闯关卡的时候,其实很多资产就要失效了,要加载很多新的资产,这些资产再根据当前玩家进度不断地加载和卸载,这个关系是非常复杂的。

前面讲的GUID系统和handle系统都在解决这个问题。大家如果有一定的编程基础,如果你学过C#, Java,知道有个概念叫做GC——垃圾回收。

在现代游戏里面,一般来讲,GC做不好就会让整个系统的效率变得非常低。如果去观察现代游戏引擎的架构,特别是很多游戏产品,你会发现它突然就会变得很慢,很多时候其实就是GC没有做好。一个关卡打完了要下去,另一个关卡要起来,大量的资源一下子加载进来,如果没有做好GC,机器就卡在那一帧了。这里面有很多很难的策略。

另外一个叫做延迟加载,延迟加载指的是希望人走到哪,这些资源再加载,因为内存很小、硬盘很大,希望根据我玩到哪,再不断地去加载。大家如果玩虚幻引擎做的游戏的时候经常有个细节(不知道最新版本有没有修改):一个角色出现在你的面前,一开始看到那个贴图很模糊,然后一点点变得更清晰,这个东西就是延迟加载导致的。

资源层是游戏非常核心的一个管理层,它管理能让整个游戏引擎跑起来的生态资源池的分配,它管理每个资产实时的生命周期,所以如果要做个游戏,我会建议大家先去看一下资源怎么去分配。

02「功能层」

Tick的魔力—— Tick Logic 和 Tick Render

有了这些资源,小明同学就可以开始做他游戏真正的玩法了,开始的那个挑战还记得吗?我们要做一个会动的角色。那怎么让角色动起来呢?

我们在上一节课立过一个Flag:希望全部课程上完之后,同学们看到一个引擎,知道从哪下手,这个部分要讲的就是真正的游戏引擎的功能,首先要讲一个有趣的概念:Tick。

我蛮喜欢tick这个词的(一直没有找到一个很好的翻译词),非常像一台永动机,每隔一个固定间隔,就会把这个世界再往前推一小格,我是个狂热的民间物理科学爱好者,我认为上帝也是在Tick我们。上帝的Tick是多少呢?是一个普朗克时间。(大家如果有时间可以去研究一下普朗克,如普朗克速度、普朗克长度、普朗克能量等)

普朗克时间,非常非常小的一个时间片段,我们认为任何一个物理过程可能不能小于普朗克时间。在游戏中,每一个Tick,就是我们构建的这个世界里的普朗克时间,当一个Tick之后,系统就会依次把所有该做的事情做完。

比如我先去看看输入输出,动一下相机,动一下人物和角色,播一下动画,还可以绘制一帧的画面,还可以做一些刚才讲的memory GC……

在游戏的世界里面,是如何让这个世界动起来的?

其实就是利用现代计算机非常高的计算速度,在每隔1/30秒把整个世界的逻辑和绘制全部跑了一遍,这就是Tick的魔力。

十年前我在做引擎的时候,第一个半年并没有像大家想象的,写了很多的graph feature,写了很漂亮的rendering、shader。我们首先定义的是数据、数据格式以及这些数据在引擎、编辑器、文件中应该怎么呈现,这些其实是引擎开发真正的核心。

大家看现代游戏引擎,我会非常推荐找到这个tick函数,但它在源码里面不一定叫tick,一般在main函数里面会看到一个Tick。在Tick里面,就会出现两大神兽,一个叫Tick Logic,一个叫Tick Render。在第一讲里提到过游戏引擎不只是渲染,或者更确切地说渲染只是引擎的其中一部分,不是很大的一部分。也就是说一般会先Tick Logic,再Tick Render,它们的关系是什么呢?

继续回到我的这个民间物理科学家的思维方式,上帝如果把整个世界虚拟出来,首先会把整个世界的物理规则全部计算一遍。比如说现在有个姿态,无论绘不绘制它,这个姿态已经存在了,如果这个时候有惯性,那在下个Tick来的时候,是不是要再往前走一步?这也是自动符合物理学规律的。

但这个时候如果小明出现了,他有两双眼睛,他会选择一个视角去观察这个世界。我们以小明的眼睛生成一个二维的画面,这就是他所看见的东西,这个过程叫什么?就叫渲染。

所以说我们是以对这个世界的模拟为先——Tick Logic把这个世界模拟出来,然后再去渲染它,这就是现代游戏两大神兽的操作流程。

大家去看游戏,基本上就是先是逻辑后是渲染。逻辑把各个物体的输入输出读一遍,把整个世界的物理算一遍(比如把相机调一调、把角色动一动、把碰撞检测一下)

比如张三有没有打中李四这件事是逻辑的事还是渲染的事?上完104课程的同学就会知道,那是逻辑的事。因为大家知道,无论你看见没看见,张三就是打中了李四,而且扣了李四的5点血,这就是逻辑。(以后讲到网络的时候,这个过程会更复杂)

逻辑和绘制一定要严格细分,一个没有受过系统训练的游戏引擎开发者,很容易把两边的代码写混在一起,但是,如果把绘制和逻辑分开,基本上无论怎么写,都不会重合在一起。

渲染就更专业了,比如各种裁剪、光照、阴影、预计算等等(后面几节课会给大家介绍,这里不作为重点展开)

所以每隔30秒tick,很多在屏幕上显示不出来的东西就会被裁掉,你只能看到你看得见的东西,你看到的人物的动作、树叶的飘落,都是一帧一帧地在变化,所以motion graph整个动画的基础理论就是依靠人的视觉残留感,从而产生一个连续的世界。这一点会在现代游戏中被充分地利用起来。

今天只是讲了一个功能层,最简单的东西就是从Logic到Render。

我想给大家讲一个概念,其实功能层是非常庞大的。我继续引用上古神书《游戏引擎架构》,这本书中关于功能层这一块的内容是最多的,真的是无所不有。

功能层有些模块是很清晰的属于游戏引擎的,比如绘制、渲染和管线,整个资产的管理等等,这些一定是属于引擎的。所以功能层在未来也会作为我们介绍的重点。

但功能层在很多引擎的架构中,它经常和具体的游戏关联在一起,让你无法区分。

比如相机控制,你觉得是游戏引擎提供的功能,还是说只要提供基础的相机绘制能力,相机的控制就交给某一个特定的游戏?比如说第三人称的射击游戏,希望这个相机有手持摄像机的摇晃感,那这个时候相机的摇晃、镜头的模糊、拉远拉伸等等,它其实是跟这个游戏有密切的关系的。这个模块是不是应该作为游戏的代码,而不是作为游戏引擎的代码呢?所以很多时候哪些功能属于游戏,哪些功能属于引擎,基本上就会在功能层上打架。

多核时代的游戏引擎架构

希望同学们能注意到一件事情:现在计算机的架构,已经在十几年前逐渐从单核走向多核。未来的多核时代,也会是游戏引擎架构的一个核心方向。

大家知道最早的游戏引擎是单线程的,就是一个线程跑道,后来有多核了。

最简单的做法是什么呢?比如说刚才讲了一个叫tick logic,一个叫tick render,我把logic和render分到两个线程里面去,当然还有额外的线程,比如做加载的。这是一个比较基础的多线程的算法。

现在的商业引擎,比如Unity或者Unreal,他们会再往前走一步,会把一些特别容易并行化的计算(比如物理、一些其他的animation等)单独地fork出来,分散到很多很多引擎。现在很多主流引擎都是这样的一个架构。

我们认为未来的引擎架构,能不能把所有的任务变成一个原子的,我们叫做job。你有4个核、8个核、16个核,没关系,我就一直扔,把你每个核吃得满满的。从上面的图来看,是不是觉得好像第三种方案看上去更漂亮点?

引擎到底是按照哪一种架构,其实有一个很简单的做法:你打开一个游戏,打开Windows的CPU profiling工具,去看每个CPU代表的那个小窗口是不是都吃得满满的?如果出现了有的吃得很满、有的吃不掉的情况,那它的多线程大概率是没有采取高级的job系统去做的。

这件事情可能比大家想象的要复杂得多。当真的去写游戏功能层的时候,你会发现很多计算中间是有一个依赖关系的,学术术语叫做dependency。

比如我现在拿手一挥,手上想打出个特效,那首先这个动画系统要算完,直到我的手到达特定位置,这时候才能把位置传导给粒子系统,然后把一个个小的光的粒子发出来。粒子系统的计算和动画系统的计算,对于角色来讲必须要有个先后关系。这一套非常高效率的多核并行架构,它难就难在dependency的管理上面,能够让它不出乱子。

我希望大家建立一个概念:未来的引擎架构一定是个多核架构。大家开始进行底层架构的时候,非常强烈推荐从多核开始去设计和思考整个底层代码。

我们的引擎之旅,到了功能层之后,小明同学已经非常厉害了。他已经知道把自己动画系统的角色挂到主的tick上了,他知道去做动作的时候,要在tick logic里面加一个tick animation,这样每次从资源里面把一个个的动画帧读进来,再把它骨骼驱动起来,那这个人就可以动起来了,然后在render里面每一帧去绘制,那个角色就绘制出来了。

小明肯定不止于此,因为我们是要做完整的游戏引擎的。因此下节课将和大家分享引擎架构分层的其他内容,如核心层、平台层等。

我们且听下回分解。

本文编辑:GAMES104编委会 张嘉瑶

如对本节课有任何问题,欢迎加入我们的社群或给我们发送邮件:

piccolo-gameengine@boomingtech.com

关注我们

GAMES104课程官网

GAMES104课程视频