Layered Architecture of Game Engine

引言

本课程将介绍现代游戏引擎所涉及的系统架构,技术点,引擎系统相关的知识。通过该课程,你能够对游戏引擎建立起一个全面且完整的了解。本节主要是介绍游戏引擎的分层架构。

同学们开始学习游戏引擎知识的时候,大家第一个困惑是恐惧感。那么多的文件,那么多的代码我该从何开始?这其实是学习游戏引擎一个很天然的感觉,而对一个事物的认知讲究由浅入深。

A Glance of Game Engine layers

所以先不用着急,先浮光掠影感受一下一个游戏引擎大概有哪些层次,下面来做一个快速的旅行。

Tool Layer

一个现代游戏引擎下载下来首先看到的不是源代码,是各种各样的编辑器。

我们可以在里面编辑我们的关卡、做角色动画……看上去眼花缭乱,编辑器本身功能非常丰富,拖拖拽拽好像真的可以做出想象中的游戏世界。这是大家对游戏引擎的最直观的第一印象,所以这个时候接触了引擎的最上层:工具层Tool Layer)。

Function Layer

做一款游戏首先要绘制出来,游戏的绘制过程本质上是把一个虚拟的三维世界转换为一帧一帧的二维图像,也就是渲染 (Rendering);这里面所有的东西不一定都是静止的,我们可能还需要一些动画 (Animation) 让它们动起来;如果这个世界需要碰撞,物理 (Physics) 的表达必不可少;除此之外可能还需要脚本 (Script)、状态机 (FSM)、AI 等等……

Resource Layer

游戏引擎不是只通过一堆代码就可以跑起来,其中还包含了大量的数据和文件。

而这些数据和文件来自不同的软件,例如 Photoshop、3dMax、Maya 等。这么多的图形、几何、音视频和各种复杂的数据,在游戏引擎里是由 资源层Resource Layer) 负责加载、管理这些资源。

Core Layer

到这里对于普通开发者引擎已经可以跑了,但可以观察到上面的动画、物理、渲染等系统会频繁调用一些很底层的代码,例如最基础的容器的创建、内存的分配和线程管理。

这些东西好像并没有出现在功能层或是资源层,意味着我们触碰到引擎的 核心层Core Layer)。核心层就像一个工具箱或者瑞士军刀,上面的游戏相关的逻辑管理全部是架在底座 Core 下面。

Platform Layer

还有一个非常容易被忽略的分层:平台层Platform Layer)。

游戏引擎生产的游戏最终是会发布给用户的,但用户的设备千千万万,有人可能是 PC,有的人可能是 iMac,还有的人可能是手机。输入设备的不同也会产生显著影响,常见的键盘鼠标显然要与手柄分开,更有甚者可能会使用体感设备。

所以无论来自什么样的输入设备,到我的游戏世界里面都要翻译成一个统一的语言。而这种平台的差异不仅体现在硬件设备上,还包括软件的发布平台比如我们的 STEAM 和 EPIC,它们的接口和收费模型完全不一样。

3rd Party Libraries

Explore Game Engine layers

Practice is the Best Way to Learn

人们常说实践是最好的老师,那么现在有一个挑战:如何在引擎中写出一个简单的角色动画控制器?

Resource Layer

How to Access My Data

首先拜托美术同学用三维建模软件制作了角色,还做了很多的贴图和动画。

这些资源每个的数据格式都是不一样的:.max.maya……这些数据肯定不可能在引擎中一一打开读取这些数据格式,并且这些数据格式实际上是为自己的工具服务的,其中包含了大量无效的信息,如果全部在引擎中加载效率会非常低。所以我们会对 Resource 做一步转换,变为引擎的高效格式:资产Access)。

所以第一步是要把这些数据进行引擎化,变成我们的资产。

Runtime Asset Manager

当我们把原始散乱的文件变成引擎的资产时,会发现其实还需要一个实时的资产管理器。

这些资产在 Runtime Resource Management 中它们会互相指向对方,这个时候在游戏引擎设计中会经常有一个重要的 handle 系统。简单解释它就像一个邮箱,我始终有邮箱的钥匙,这样邮箱主人有没有、邮箱主人在不在只要问这个邮箱就知道了。

Manage Asset Life Cycle

简而言之,资源层在游戏中最核心的是管理所有这些资产的生命周期。

为什么资产的生命周期如此重要?可能大家没有注意,现代游戏关卡中随着进度的推进许多资产会无效,同时要加载许多新的资产,其中的关系是非常复杂的。

同样的 GC 垃圾回收概念也是如此,现代游戏如果 GC 做不好会让整个系统效率变得非常低下。

Function Layer

How to Make the World Alive

那怎么让角色动起来呢?

这里面有一个很重要的概念:Tick。它会每隔一个非常短的时间把世界往前推一小格,可以类比普朗克时间,我们认为任何一个物理过程不可能小于普朗克时间。

在游戏中的话每经历一个 Tick 我们的系统会把该做的事情做完,比如读入输入输出、动一下相机、动一下人物和角色、绘制一帧的画面、做一下 Memory GC…..整个逻辑往前走一点,在游戏的世界其实就是利用现代计算机非常高的计算速度每隔 1/30 秒把整个世界的逻辑和绘制全部跑了一遍,这就是 Tick 的魔力。

Dive into Ticks

现代游戏引擎的 mian() 函数中一般会出现两个非常重要的函数:tickLogic()tickRender()。我们以模拟世界为先,随后再把它表现渲染出来。

Tick the Animation and Renderer

动画的基础理论是视觉残留,而这一点在现代游戏中会充分利用。

Heavy-duty Hotchpotch

Multi-Threading

现代计算机架构逐渐从单核走向多核,未来的多核时代会是游戏引擎很重要的一个方向。

最早的游戏引擎是单线程的,后面多核最简单的做法是把 Logic()Render() 分到两个线程里面去。现在的商业引擎则会把一些特别容易并行化的物理或动画计算单独 Fork 出来分散到线程中,如中图所示。

未来引擎架构则考量能否把 Job 任务变成原子的,把每个核物尽其用安排满满的。

Core Layer

Math Library

核心层最容易引起关注的就是数学库。

游戏引擎中大部分的数学并不是特别的高深,大学的线性代数基本上够用了。那为什么游戏引擎里这些数学库会单独列出来呢?

Math Efficiency

这就牵扯到游戏引擎中一个独特的需求:游戏引擎的一切都是为效率服务的,它是一个 Real-Time 的 Application,所有用户的输入和反馈必须是实时的,玩家对效率是非常敏感的。一个经典的例子就是 Quake 引擎中的求倒数平方根:Fast Inverse Square Root — a Quake III Algorithm

SIMD 也是现代计算机需要注意的一个东西。在游戏引擎中很多时候就是矩阵和向量的加减乘除,于是诞生了 Single Instruction Multiple Data 的概念,也是在数学库中广泛使用的技术。

Data Structure and Containers

核心层不止数学,它为上层的所有逻辑提供基础服务,其中最重要的就是数据结构。

C++ 的 STL 提供了标准容器,那为什么引擎核心层还要再做一遍?不难发现当容器被高频访问、添加删除数据时,它会在内存中产生大量碎片,访问效率会受到影响。

Memory Management

所以游戏引擎的开发可以类比于操作系统,尤其是在内存管理方面。C++17 或者 C++21的高端功能许多人学得感觉云里雾里,但实际上的底层逻辑无非三条:

  • 把数据放在一起
  • 尽可能顺序访问数据
  • 尽可能一起读写数据

Foundation of Game Engine

Platform Layer

Target on Different Platform

不同的平台甚至连文件路径的格式都不一样,但又不可能把引擎中的这些代码从头到尾改一遍,这个时候平台层就会变得非常重要。

平台层本质上就是人们在上面写核心写功能,可以无视这些平台的区别而直接写它核心的逻辑。平台无关性就是把所有平台的差异全部掩盖掉,这一层也是现代游戏引擎非常核心的东西。

Graphics API

现代游戏引擎中有一个非常重要的东西:Render Hardware Interface(RHI),它重新定义图形 API,把各个硬件的 SDK 区别封装起来。所以平台层是一个很容易被大家忽略的一层,但其实也是体现引擎水平高下很重要的一层。

Hardware Architecture

Tool Layer

Allow Anyone to Create Game

Unleash the Creativity

  • Build upon game engine
  • Create, edit and exchange game play assets

工具层代码选择以开发效率优先如 C++ 和 QT,而不是以运行效率为优先。

Digital Content Creation

DCC(Digital Content Creation),翻译一下就是别人开发的资产生产工具。大名鼎鼎的 MAYA、3DMAX、Houdini 生产的数字资产通过一条 Asset Conditioning Pipline 导出管线变为引擎统一的 Asset。

Why Layered Architecture

那么引擎为什么要分层呢?主要有两点:

  • Decoupling 复杂度
  • Response 不断变化的需求

各个层次之间的调用一般只允许上面的层次调用下面的层次,而绝对不允许反向调用。

Mini Engine - Pilot

Neat PILOT Engine

Release Plan


GAMES104_Lecture_02