GAMES104实录 | 如何构建游戏世界(上)

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

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

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

01「面向对象的游戏世界」

上节课我们学习了游戏引擎的基础结构,知道现代游戏引擎基本上分为五层:平台层、核心层、资源层、功能层和工具层。

有了这些知识,我们只是知道游戏引擎这个大厦长的什么样,但并不知道这个大厦里面的建筑、砖石、水电是怎么在一起工作的。今天这节课,将会是一节含金量非常高的课,将告诉大家该怎么去构建真正的游戏世界。

如何去构建一个游戏世界?首先要明白,这个世界到底由哪些东西构成?这些东西是如何被描述的?它们彼此之间又是如何被组织、如何被调动起来的?

继续以小明同学为例,比如小明玩了《战地2042》这个游戏,觉得很好玩,也觉得有“拉胯”的地方。假设他设定了这样一个目标:决定自己做一个游戏引擎,能做出一个比2042更好的游戏。

那么首先,他应该学会对这个游戏世界进行拆解,并能知道游戏世界应该有哪些东西。拆解后,首先能想到的是一大堆的可交互的动态物,这些东西可以根据你的行为去交互,在现代游戏中的动态物,是最容易关注到的东西。比如坦克、无人机、端着枪往前冲的NPC小兵,当然还有火炮、导弹。

第二类东西是默默的静态物,比如瞭望塔、飞机的机棚、仓库、房屋、街道等。它们虽然不可以交互,但是构成了游戏中各种各样关键性玩法的元素。

其次就是大家最不容易注意到但又无处不在的——地形系统。它会无限绵延下去,是动态物、静态物支撑的托盘。(在游戏引擎中,地形系统一般是个单独的系统,我们后面的课程会讲到)

还有大家注意不到的天空系统,想象一下,如果在一个真实的游戏世界里面,要表现日夜的变化、不同的天气变化,要实现天空中变化莫测的云,其实这些视觉/动效并不简单,有一个专门的说法叫Time of the Day(TOD,日夜变换系统)

还有大家熟悉的植被,植被属于动静结合态系统,因为它是固定在一个地方,不能动,但它又会随着风雨等做各种交互,做出各种各样的变化。植被的体量特别大,是构成整个环境的核心元素。

小明分解后得到了动态物、静态物、环境,基本上就可以画出所能看见的这个世界了,但对游戏来讲其实还远远不够,因为游戏还存在大量其他的物体,比如一个个检测体。举个例子,游戏中最令人讨厌的空气墙,当你玩战地的时候,开着一架飞机玩得正嗨,突然告诉你已经飞出了舒适区,五秒钟之后就把你击落了。那检测体是如何被启动的呢?它放了一些你看不见的东西,包括游戏玩法的规则,其实也可以抽象成游戏中的对象。

所以在现代游戏中,我们会把所有的动靜态物体统称为游戏对象Game Object (GO)。(注意:大地和天空系统是独立的)小明想要构建游戏世界,只要管理好这一系列的游戏对象就可以了。

有了游戏对象(game object),那如何去操控这些游戏对象去做好一个游戏呢?举个例子,小明想做个可以自动巡逻的无人机,让它在战场上飞来飞去监控整个战场的环境。

需要先去分析它的行为,比如无人机的行为一般分两类,第一类叫属性,如几何外形、各种动画、空间位置,还有血量、油量、电池量等。另外一类叫行为,比如无人机可以移动、可以飞来飞去、可以由AI算法控制沿着某个固定航线去巡逻或者追踪。

所有在游戏世界里描述的物体,都可以把它归类成两类,一类叫做属性,另一类叫做行为。无论大家是写Java,C++还是C#,只要是面向对象的现代语言,会很自然的先定义一个类。比如说我们先给无人机取名叫John,位置定义成三维向量(x, y, z),血量定义成标量,范围在(0~100),油量也是标量,范围在(0~1),用函数来自定义行为(比如可以移动、巡逻等),那么这样一个类就很完美的把无人机的属性和行为封装好了。

现在我们看这个可能有些过时,但最早期的游戏还真的就是这么架构的,非常的简单。

更近一步,我们希望无人机不仅可以巡逻,看到目标后还可以攻击,那这个时候就需要再给无人机增加一个攻击行为。说到攻击,还得再加一个Ammo属性表示它的弹药量,因为子弹不是无限的。

所以如果我用派生和继承的关系,定义无人机的类,那么我再派生一个类,叫侦查攻击化无人机,然后这个时候我只需要多加一个属性表示弹药量,一个行为表攻击,那么这个无人机它就有了侦查和攻击一体的能力。

02「组件化」

最早期的引擎就是按这样一个简单的面向对象的逻辑去构建这个世界,这个方法虽然非常的简单易懂,但它有一个缺陷:随着游戏世界越来越复杂,有些东西它并没有那么清晰的父子关系。

比如我把坦克和船合到一起就会造出水陆两栖坦克,而坦克本身是派生于车辆,而巡逻艇是派生于船的,那水陆两栖坦克,那么它的父亲是车辆还是船呢?这个是一个非常经典的面向对象的问题,不仅在游戏引擎的架构上,其实在现代的编程语言中,我们也会发现这样的问题,这个问题有一个经典的解法,叫做组件化。我们把对象的行为拆分成无数的组件,就像一辆玩具挖土机,你看起来是个挖土机,但是它的铲子换成各种各样的部件,可以变成一个压路机也可以把它变成推土机、举重机、挖路机。

同一个模块可以变成各种各样的东西,那么大家在现代游戏中最喜欢玩的东西是什么,自定义化武器对不对?比如说我们玩一个现代射击游戏,我们会把各种枪的组件模块自定义化,加上一个三倍狙,这就是一个狙击枪了,再加上消音器,就是一个微冲了。

所以,游戏设计里面的武器,有大量的组件化的思想。而这个思想,其实在十几年前甚至更早就在现代游戏引擎设计中就已经开始使用了。

回到刚才小明做无人机的案例,重新理解这个无人机的行为,我们会发现一个叫model的组件,能够把它的外形表达出来。叫motor的组件可以表示各种运动属性,比如说飞行速度,加速度以及它的整个移动惯性。还有一个叫health的组件,告诉我这个无人机还有多少血,包括它的AI行为,物理行为,动画行为等等都是一个个的小组件。当我把小组件模块拼到一起的时候,我就有一个自己的无人机了。

其实在真实的物理世界里,你们现在买的很多现代玩具,它都不是包装好的,而是提供了各种选项让你自己可以去选配,对不对?在游戏世界里面,我们很早就按这个逻辑去构建游戏的整个物体逻辑。

我们用C语言为例,大家只要定义成员的基类,让它统一好每一个组件的基础行为接口,这里面大家注意一个细节,每个组件都会派生一个Tick函数。我们的各种位移,模型,动画等全部派生于成员的基类,这样的话,我们就可以在插件里让各种各样的小组件在一起协同工作了。

按照这个思想,无论是侦察飞机,还是侦察攻击一体化机都是一样的移动,模型,动画,物理等。如果我想让AI模块换一下,从而可以攻击的更猛;或者再加一个战斗系统,能够帮助我瞄准搜索,此时一架无人机就变成了另外一架具有新特性的无人机,所以非常的简单,对不对?

其实这就是现代游戏引擎架构的一个核心理念:它的整个基础结构,第一个要让我们的开发者好维护,好理解。同时,交付给大量的艺术家和设计师去使用,各种交互能够符合艺术家对大自然的理解和直觉。那么游戏引擎的架构本质是什么呢?它不是为了炫耀我们技术多厉害,它是一个方便大家去理解生产力的工具。

比如像Unity,他们都会去提供组件的概念,如果大家打开Unity点开任何一个物体,都会发现下面出现各个组件,而且这个组件你可以增加,定义,甚至自定义化它的小脚本。

在UE里面其实也是类似的,如果大家看UE的代码就会发现,它所有的东西都派生自一个Uobject,它可以管理任何对象的生命周期,创建完后内存释放,就是我们讲的垃圾回收。

在现代引擎里面,组件的能力是非常综合的,举个例子,我们在游戏里看到各种各样的光源,无论是点光源,面光源,还是方向光,在UE里面它都会被定义成一个组件。在Unity里,各种行为,动画,物体模型也都是组件。所以我们用组件作为积木去构建出整个现代游戏引擎的基础结构。

基本上听到这里,我觉得很多开发能力比较强的小伙伴,已经想亲自去写代码了。其实我在一开始学这些技术的时候也是这样的,就是我听着听着就很想去实践它。

同学们只需要记住两件事就可以了。第一,游戏世界里面几乎把所有的东西抽象成了游戏对象Game Object (GO)这样一个东西。第二,每个GO用各种各样多功能的组件把它组合起来,所以各种组件又是游戏对象的原子,如果这两件事情明白了,你就明白了现在游戏的组合的一个基础逻辑。

03「如何让游戏世界动起来?」

有了这些理解后,小明就很自信了,他觉得可以去构建一个游戏世界了,但是真得想构建一个这样可以真正玩的虚拟世界好像还少了点什么。坦克要能启动、飞机要能上天,我怎么能让这个虚拟世界真得动起来呢?

记得上节课讲过,在游戏引擎里面核心的一个函数叫做Tick,就是每隔1/30秒让这个世界往前走一步,就如同我们真实世界里面一个普朗克时间一样。我们生活在一个虚拟世界里面,会认为每个普朗克时间就是上帝给我们设置的Tick,那么在游戏引擎里面有一个Tick,它是怎么向游戏世界里的这些对象传导的呢?

其实非常简单,就是把每一个游戏物体的每一个组件依次去Tick一遍,比如我的一个坦克,或者一架飞机,那首先Tick一下我的发动机,直到它往前走了一步。

如果知道速度是30cm/s,那现在往前移动30cm,发动机就会告诉我要去更新一下履带。如果战斗系统告诉我还要开一下火,还得在这1/30秒里往外发射子弹。我把坦克里面的每个组件依次Tick一下,把整个游戏世界里面所有的物体都Tick一遍,这个世界是不是就动起来了?

所以Tick其实非常简单,所以刚才我们在讲成员组件的基类的时候提到过,无论你写的是Java, c #还是C++,在写成员组件的基类的时候一定要加上Tick函数。真实的游戏引擎比这更复杂,我们以后带着同学们再深度的理解。但现在大家只需要知道我们有一个Tick函数就可以了。

但是在现在游戏引擎中,我们一般不是按照每个对象进行 Tick 的,我们会把一个个系统里面的全部组件进行Tick ,比如说我们先把坦克系统里各个组件全部Tick一遍,再去天空的飞机或者拿枪的老兵。为什么会这样呢?

其实有一个最简单的类比,大家想象一下假设一个汉堡店有五个工人,大家直觉的做法是什么?

每个人先去分别烤面包,烤牛肉,洗蔬菜,然后放上黄油在一起,最后五个各式各样的汉堡就做出来了,但这样的生产效率显然是不高的。现代工业核心的概念叫流水线,最高效的做法是有人专门去烤面包,有人专门去烤牛肉,有人专门去洗菜,大家配合好,然后到了时间,一起合成一个汉堡,这样的效率是最高的。

现在计算机很大的一个好处是什么呢?就是所有的成员数据,我们都会把它尽可能的集中在一起处理,到时候整个一次批量处理,大家还记得我在上节课讲的那个图灵机的故事吗?就是那个无限长的纸袋,在图灵机上效率最高的处理方式是什么?我们也是把同样的数据尽可能放在一起,无论读写一次性把它处理完。这样的架构下处理效率是最高的。

我们的高级课程,会讲到实体-组件-系统(ECS)架构。

但这节课,我只是简单的提到这个架构和Tick系统。

现代游戏引擎,为了追求效率,逐渐的会转向按照每个系统或者是每一种组件进行Tick,就是我要造个流水线进行批处理,这样我的效率会特别高。所以这样的架构,坦克也能发动了,飞机能飞了,但是这个世界,还缺了一点什么呢?也就是说坦克与飞机还有老兵是各顾各的表演,他们之间没有任何关系。

下节课我们讲给大家分享如何让这个游戏世界是能产生交互的,教大家构建起一个完整的游戏世界。

本文编辑:Piccolo 社区编委会 张嘉瑶

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

piccolo-gameengine@boomingtech.com

关于我们

Piccolo游戏引擎社区

Piccolo社区是中国开源游戏引擎分享、学习的非营利性平台,由游戏引擎行业大佬、共创官、学习者共同建立。你可以在我们的社区里交流技术、互助问答、参加活动,你也可以参与Piccolo的共建,如撰写贡献代码、撰写技术文章、参与技术挑战等。

Piccolo游戏引擎

由中国游戏引擎社区Piccolo开源的一款Mini游戏引擎。采用世界-关卡-游戏对象-组件的简洁架构,便于理解游戏引擎架构思想,它不仅能有效的帮助开发者学习游戏引擎架构知识,也能帮助一线开发者实验引擎算法与第三方库、辅助个人项目快速启动。截止目前,Github点赞已突破3600+,累计下载量已超过20000+

Piccolo GitHub地址:https://github.com/BoomingTech/Piccolo/discussions

关注我们

GAMES104课程官网

GAMES104课程视频

© 版权声明
THE END
喜欢就支持一下吧
点赞167 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容