现代游戏引擎 - 引擎工具链基础(十三)

工具链

工具链(tool chain)是沟通游戏引擎用户以及更底层run time(渲染系统、物理引擎、网络通信等)之间的桥梁。
对于商业级游戏引擎来说,工具链的工程量往往要比run time大得多。
用户与引擎运行时之间的图层

另一方面,工具链也是连接游戏引擎以及第三方DCC工具的核心。在现代游戏工业中需要使用到大量的第三方工具如MAYA、3DS MAX等,
在进行游戏开发时需要通过工具链将这些第三方DCC的资源加载到游戏引擎中。
DCC工具和游戏引擎之间的桥梁

从更高级的视角来看,工具链的本质是调和不同背景和思维方式用户的一套平台。对于开发者,工具链需要方便开发者管理游戏中大量资源和对象;
对于设计师,工具链需要帮助他们快速实现不同的游戏逻辑;而对于艺术家,工具链则需要帮助他们表达不同的创意。工具链需要服务这些拥有不同知识背景的用户以便更好地完成游戏开发的过程。
让不同背景和思维方式的用户一起工作

复杂的工具

图像用户接口(GUI)

GUI是工具链与用户直接进行交互的接口,在现代软件工程中GUI是人机交互的必要模块。
图像用户接口

目前GUI主要有两大类实现方式,其一是immediate mode。在immediate mode中用户的操作会直接调用GUI模块进行绘制,让用户立刻看到操作后的效果。
这种模式的特点是它非常直观而且易于实现,但它的效率和可拓展性往往不尽如人意。
即时模式
即时模式2

在现代游戏引擎中更常用的GUI实现方式是retained mode。在retained mode中用户的操作不会直接进行绘制,而是会把用户提交的指令先存储到一个buffer中,然后在引擎的绘制系统中再进行绘制。
这样做的好处是可以极大地提高系统的运行效率和可拓展性,当然代价是这种方式的实现要更加复杂。
保留模式
保留模式2

设计模式(Design Pattern)

在设计GUI系统时需要设计模式(design pattern)相关的知识,这里简要介绍一些在GUI设计中常用的设计模式。

MVC

MVC是经典的人机交互设计模式。MVC的思想是把用户(user)视图(view)模型(model)进行分离,
当用户想要修改视图时只能通过控制器(controller)进行操作并由控制器转发给模型,从而避免用户直接操作数据产生各种冲突。
MVC模式

MVP

MVP可以看做是对MVC的演变。MVP模式对视图和模型进行了更彻底的分离,视图只负责对数据进行展示而模型只负责对数据进行处理,
它们之间的通信则通过展示者(presenter)来实现。当用户想要修改数据时,用户的请求会通过视图提交给展示者,然后再由它转发给模型进行处理。
MVP模式

MVVM

MVVM是目前游戏引擎中大量使用的UI设计模式,在MVVM中视图和模型的中间层称为ViewModel。在MVVM模式中,
视图只包含简单的UI状态数据,这些数据通过ViewModel解析成合适的数据结构再提交给模型进行处理
MVVM模式
MVVM模式2
MVVM模式3

加载和保存(Load and Save)

加载和保存各种不同类型的数据是工具链的核心功能。在保存数据时需要使用序列化(serialization)的技术来将各种不同的数据结构或是GO转换成二进制格式,
而当需要加载数据时则需要通过反序列化(deserialization)从二进制格式恢复原始的数据。
序列化和反序列化

最简单的序列化方法是把数据打包成text文件。text文件虽然简单,但实际上目前很多系统仍然是使用text文件进行信息的传输。目前常用的text文件格式包括json、yaml、xml等。
Text文件

text文件可以方便开发人员理解存储数据的内容,但计算机对于文本的读取和处理往往是比较低效的。当需要序列化的数据不断增长时就需要使用更加高效的存储格式,
通常情况下我们会使用二进制格式来对数据进行存储。
Binary文件

和text文件相比,二进制文件往往只占用非常小的存储空间,而且对数据进行读取也要高效得多。因此在现代游戏引擎中一般都会使用二进制文件来进行数据的保存和加载。
Binary和Text对比

资源引用(Asset Reference)

在很多情况下游戏的资产是重复的,此时为每一个实例单独进行保存就会浪费系统的资源。
资产数据重复

因此,在现代游戏引擎中会使用资产引用(asset reference)的方式来管理各种重复的资产。实际上资产的引用和去重是游戏引擎工具链最重要的底层逻辑之一。
资产引用

在游戏开发过程中工具链往往还需要提供对GO进行修改,从而实现不同的艺术效果。
场景中的对象实例
对象实例差异

在调整和修改数据时直接进行复制很可能会破坏GO之间的关联而且容易造成数据的冗余,因此在现代游戏引擎中对于数据引入了继承(inheritance)的概念。
数据之间的继承可以很方便地派生出更多更复杂的游戏对象,从而方便设计师和艺术家实现不同的效果。
通过复制生成差异
通过数据继承生成方差

资产加载

解析(Parsing)

在上一节我们主要是考虑如何对数据进行保存,而游戏引擎中工具链的一大难点在于如何加载不同的资产,即反序列化的过程。
反序列化的过程可以理解为对文件进行解析(parsing),文件中的不同字段往往有着不同的关键字以及域。我们需要对整个文件进行扫描来获得整个文件的结构。
解析资产文件

对文件完成解析后可以得到一棵由< key-value >对组成的树来表达不同类型的数据。
构建键值对树

实际上这样的过程与对文本文件的解析过程是非常类似的。
Binary VS Text

字节序(Endianness)

对二进制文件进行反序列化和解析时需要额外注意endianness的问题。在不同的硬件和操作系统上同样的二进制文件可能会被解析为不同的数据,这对于跨平台的应用需要额外注意。
字节序

版本兼容性(Version Compatibility)

在工具链的反序列化过程中还需要考虑资产的兼容性问题。游戏的开发周期往往是比较长的,在这一过程中可能会不可避免地出现引擎以及各种工具的升级。
而我们希望新版本可以对旧版本中设计好的资源进行兼容,从而避免重复的劳动。
资源的版本兼容性

在版本更迭中最常见的情况是数据的域发生了修改,新版本的数据定义可能会添加或删去老版本定义的域。
添加或删除字段

为了处理这种问题可以手动为数据添加版本号,在加载数据时根据版本号来控制加载过程。
通过版本硬代码来解决兼容性问题

更好的处理方法是使用guid来进行管理。如Google就提出了使用protocol来为每一个域赋予一个uid,在进行反序列化时只需要对域的uid进行比较即可。
通过字段UID解决兼容性问题

如何制作高鲁棒性的工具

在游戏引擎中工具链对于鲁棒性有非常高的要求,一旦游戏引擎的工具链出现问题会对整个游戏开发流程产生巨大的影响。
鲁棒性最基本的要求是允许程序从崩溃中进行恢复,从而还原初始的开发状态。为了实现这样的功能我们需要将用户所有的行为抽象为原子化的命令(command),通过命令的序列来表示整个开发的过程。
命令

对command类进行抽象时需要为每一个command实例赋予单调的UID从而保证顺序的正确性,
同时每一个command定义都需要实现Invoke()Revoke()方法表示执行命令以及恢复到执行命令前的状态。
除此之外还需要实现Serialize()Deserialize()方法来控制生成的数据序列化以及反序列化过程。
命令定义
命令UID
命令序列号和反序列化

整个command系统可以划分为三种不同类型的指令,包括添加数据、删除数据以及更新数据。实际上几乎所有的command都可以视为这三种基本指令的组合。
三个关键命令

如何制作工具链

现代游戏引擎的工具链往往包含成百上千个不同的工具程序,这些程序会面向不同背景的开发人员并实现相应的功能。
针对不同用户的各种工具

而对于工具链来说,一个基本要求是要保证不同工具之间的沟通以及整个系统的可拓展性。
我们不希望每个工具程序都使用单独的一套数据定义方式,这会导致整个工具链系统过于庞大而且难以进行维护。
单独开发所有工具?

因此我们需要去寻找不同工具中的一些共性,并把这些共同的数据封装为基本的单元。利用对这些基本单元的组合来描述更加复杂的数据结构。
查找公用块

提要(Schema)

schema是一种对数据进行描述的结构,它描述了具体的数据结构是由哪些基本单元构成的。
在工具链系统中所有流动的数据都要通过schema来进行描述,从而保证不同的程序都可以对数据进行解读。
描述结构
基本要素

在schema的实现中一般也需要实现继承和引用功能来方便定义新的数据类型。
继承
数据引用

不难发现schema与高级语言有着很多相似之处,实际上schema确实可以直接使用高级语言来进行定义。
目前游戏引擎中的schema系统主要有两种实现方式,其一是单独实现schema的定义,而另一种则是使用高级语言进行定义。
两种实现方式

这两种实现方式各有各的优缺点,它们的特点可以总结如下:
两种实现方式优缺点

引擎数据的三个视图(Three Views for Engine Data)

基于schema系统我们可以发现同样的数据在游戏引擎的不同系统和工具中可能会有不同的表现形式。
引擎数据的三个视图

在runtime中一般会以运行和计算效率为第一要务。
运行时视图

而在进行存储时则要游戏考虑数据的读写速度和空间需求。
储藏视图

而在面向开发者的工具程序中需要根据不同使用者的背景和需求来设计不同的数据表现形式。
工具视图
工具视图 - 容易理解的
工具视图 - 各种编辑器模式

所见即所得

所见即所得(what you see is what you get, WYSIWYG)是我们设计构建整个工具链系统的核心精神,
它的目标是保证设计师和艺术家在工具链中的设计结果能完美地重现在实际的游戏场景中。 在早期的游戏引擎中一般会设计一个独立工具层用来辅助开发者进行设计,
但这种设计方式往往会违背WYSIWYG原则因此在现代游戏引擎中基本已经被弃用。
独立的工具

目前商用级游戏引擎一般会把工具层设计在整个游戏引擎的最上层,换句话说工具层会调用底层的模块来辅助游戏开发者的工作。
这种设计方式的好处是可以严格遵循WYSIWYG原则,提高开发效率。当然其缺陷在于此时的工具层依赖于整个引擎的实现,当引擎崩溃时工具链也会直接崩溃。
游戏中的工具

在编辑器中播放(Play in Editor)

基于in game的设计模式就可以实现在工具层中进行游戏,当然这也要求我们在工具层上再设置一个编辑器来编辑游戏的功能
游戏中的工具 - 编辑模式

在编辑器中进行游玩时同样有两种实现方式,包括直接在编辑器中进行游戏或是基于编辑器当前的状态生成一个新的游戏窗口进行游戏。
PIE

直接在编辑器中进行游戏可以无缝地对当前游戏场景进行编辑,但需要注意在进行编辑时不要污染游戏场景中的数据。
PIE模式-在编辑器世界中播放

另一种实现方式是新建一个沙盒来重现当前的游戏环境,整个游戏过程都在沙盒中进行。
这种设计方式可以保证编辑器中的数据与实际游戏中的数据保持相互独立,避免出现数据污染的情况。在大型游戏引擎的开发中一般会使用这种模式。
PIE模式-在PIE世界中播放

可拓展性

在工具链中我们往往还需要允许用户根据自身的需要自行设计并开发新的工具,也即插件(plugin)
实际上这些插件定义了整个游戏引擎的可拓展性,在现代游戏开发过程中需要使用到各种各样的插件以实现不同的功能。
可拓展性
插件展示

目前市面上的商业级游戏引擎都对插件有很好的支持,方便用户自行开发和分享各种插件。
插件框架
插件框架2
插件-添加一个工具栏按钮
插件-在Unreal5中添加一个插件菜单
插件概要

引用

参考文章

课程视频

课件PPT

查看评论