引擎架构分层
引言:如何克服对庞大引擎代码的“恐惧感”
对于初次接触现代游戏引擎(如 Unreal Engine 或 Unity)的开发者来说,其庞大的代码库和复杂的系统往往会带来一种“无从下手”的恐惧感。这篇笔记旨在通过一个分层架构的视角,帮助你建立一个清晰的宏观认知,理解引擎各个模块的职责与相互关系,从而由浅入深地探索游戏引擎的内核。
我们将遵循一个自底向上的逻辑顺序来剖析引擎的五大核心层次,这与引擎的构建和依赖关系一致。
游戏引擎的五层核心架构
一个现代游戏引擎可以被看作一个层层堆叠的结构,每一层都建立在下一层提供的服务之上。
1. 平台层 (Platform Layer)
这是引擎的最底层,也是最基础的一层。
-
核心观点: 平台层是引擎与外部世界沟通的桥梁,其核心职责是封装和抽象不同硬件设备与操作系统的差异,为上层系统提供一套统一、标准的接口。
-
关键职责与术语:
- 硬件抽象 (Hardware Abstraction): 屏蔽不同硬件平台的细节。
- 设备差异: PC, Mac, 游戏主机 (PlayStation, Xbox), 移动设备 (iOS, Android)。
- 输入设备: 键盘鼠标、游戏手柄、触摸屏,甚至是VR控制器、方向盘等非常规输入设备。平台层需要将这些五花八门的输入信号,翻译成引擎上层可以理解的统一事件(如“向前移动”、“攻击”)。
- 软件平台抽象 (Software Platform Abstraction): 适配不同的软件生态和分发渠道。
- 分发平台 SDK: 集成 Steam, Epic Games Store (EGS), XGP 等平台的特定功能,如成就系统、好友列表、支付接口等。
- 操作系统接口: 文件读写、窗口管理、网络通信等基础功能的跨平台实现。
- 硬件抽象 (Hardware Abstraction): 屏蔽不同硬件平台的细节。
2. 核心层 (Core Layer)
核心层是引擎的基石,为所有上层模块提供通用、高效的基础工具集。
-
核心观点: 核心层提供了一系列与具体游戏逻辑无关的、可复用的基础功能模块,它就像一个“瑞士军刀”或“工具箱”,支撑着整个引擎的运转。
-
关键职责与术语:
- 数学库 (Math Library): 提供向量、矩阵、四元数等线性代数运算,是进行3D坐标变换、物理计算等一切图形学操作的基础。
- 内存管理 (Memory Management): 高效、安全的内存分配与回收机制,对于需要长时间稳定运行且性能要求极高的游戏至关重要。
- 容器 (Containers): 提供如动态数组、链表、哈希表等常用数据结构的高性能实现。
- 多线程管理 (Multithreading): 线程的创建、同步与管理机制,充分利用现代多核CPU的计算能力。
- 字符串处理、反射系统 等其他基础工具。
3. 资源层 (Resource Layer)
这一层负责管理游戏项目中所有非代码的“数据”。
-
核心观点: 资源层是引擎的“弹药库”,负责所有游戏资产(Assets)的加载、卸载和生命周期管理,为功能层提供渲染、动画、物理等系统所需的各种数据。
-
关键职责与术语:
- 资源管理 (Resource Management): 统一处理成千上万的资源文件。
- 资产类型 (Asset Types):
- 几何模型:
.fbx,.obj(来自 3ds Max, Maya, Blender) - 纹理贴图:
.png,.tga,.dds(来自 Photoshop, Substance Painter) - 音频文件:
.wav,.mp3 - 视频、字体、配置文件 等。
- 几何模型:
- 核心功能: 将磁盘上的原始文件加载到内存中,并转换为引擎内部可识别和使用的数据格式。
4. 功能层 (Functionality Layer)
这是引擎中最“游戏”的部分,包含了实现游戏世界交互的核心系统。
-
核心观点: 功能层利用底层提供的服务和资源,构建出一个可交互、可感知、可游玩的虚拟世界。这一层直接决定了游戏的具体表现和玩法。
-
关键职责与术语 (可概括为三点):
- 让世界“看得见” (Visible):
- 渲染系统 (Rendering System): 核心中的核心,负责将三维场景转换为二维图像,涉及到光照、材质、后处理等复杂技术。
- 让世界“动起来” (Animated & Interactive):
- 动画系统 (Animation System): 控制角色和物体的运动,包括骨骼动画、状态机等。
- 物理系统 (Physics System): 模拟现实世界的物理规律,如重力、碰撞检测、刚体动力学等。
- 让世界“玩得起来” (Playable):
- 脚本系统 (Scripting System): 提供一种方式(如Lua, C#, 蓝图)来编写游戏逻辑和规则。
- 人工智能 (AI): 控制非玩家角色(NPC)的行为。
- UI 系统: 负责渲染用户界面,如血条、菜单、计分板等。
- 让世界“看得见” (Visible):
5. 工具层 (Tool Layer)
工具层是引擎最外显、用户最先接触到的一层。
-
核心观点: 工具层为游戏开发者(策划、美术、程序)提供了一套可视化的编辑器环境,让他们能够高效地创建、组织和调试游戏内容,而无需直接编写所有代码。
-
关键职责与术语:
- 编辑器 (Editors):
- 关卡编辑器 (Level Editor): 用于搭建游戏场景。
- 材质编辑器 (Material Editor): 通过节点图等方式创建复杂的物体表面材质。
- 动画编辑器 (Animation Editor): 调整和预览角色动画。
- 蓝图/脚本编辑器 (Blueprint/Script Editor): 可视化地编写游戏逻辑。
- 核心价值: 极大地提升了开发效率,并使得团队中的非程序员也能深度参与到游戏制作中。
- 编辑器 (Editors):
总结
通过这五个层次的划分,我们可以清晰地看到一个现代游戏引擎的内部结构和依赖关系:
工具层 → 功能层 → 资源层 → 核心层 → 平台层
- 自底向上是构建和依赖关系:平台层是基础,核心层提供工具,资源层提供数据,功能层实现玩法,工具层赋能创造。
- 自顶向下是用户的认知过程:用户首先接触工具,通过工具调用功能,功能驱动资源,而这一切都运行在核心层和平台层之上。
理解这个分层模型,是深入学习和掌握任何一个现代游戏引擎的第一步。
从理论到实践的挑战
在了解了现代游戏引擎的五层架构理论后,本部分将通过一个具体的实践挑战——“创建一个可动的游戏角色”,来深入剖含引擎各层,特别是资源管理与数据组织的实际工作方式。
一、 游戏引擎的 “5+1” 架构回顾
在深入实践之前,我们先快速回顾一下引擎的宏观架构,这为我们理解后续所有工作提供了上下文。
-
五层核心架构:
- 平台层 (Platform Layer): 操作系统、硬件的抽象。
- 核心层 (Core Layer): 基础数据结构、数学库等。
- 资源层 (Resource Layer): 文件加载与管理。
- 功能层 (Functionality Layer): 游戏玩法的具体实现,如渲染、物理、动画等。
- 工具层 (Tool Layer): 为开发者和美术师提供的编辑器环境。
-
“+1” 第三方库 (Third-party Libraries):
- 现代引擎并非所有模块都从零开发,而是会集成大量成熟的第三方库来强化特定功能。
- 集成方式:
- SDK集成: 直接编译进引擎代码,成为引擎的一部分(如 Havok Physics)。
- 独立工具: 作为外部工具存在,通过特定的文件格式与引擎交换数据(如 Simplygon 用于模型减面)。
二、 实践挑战:创建一个可动的游戏角色
理论学习的最好方式是实践。现在,我们面临一个具体任务: 从零开始,利用引擎架构,将一个美术制作的角色在游戏中动起来。这个过程将引导我们深入引擎的“深水区”。
第一步:理解资源(Resource)与资产(Asset)的转换
这是角色进入引擎的第一道门槛,也是资源层的核心工作。
-
核心观点: 引擎不直接使用艺术家创建的源文件(Resource),而是将其转换为一种引擎专用的、高效的格式,称为 资产(Asset)。
-
关键术语:
- Resource (资源): 美术师使用的源文件,如
.max,.ma,.psd等。它们为创作而生。 - Asset (资产): 经过转换后,引擎内部使用的数据格式。它们为运行效率而生。
- Resource (资源): 美术师使用的源文件,如
-
为何必须进行转换?
- 格式繁杂: 引擎不可能原生支持市面上所有的DCC(Digital Content Creation)软件格式。
- 包含无关信息: 源文件(Resource)中含有大量仅用于编辑的辅助数据(如操作历史、图层信息),这些对于游戏运行是冗余的。这好比我们只需要一篇文章的纯文本(
.txt),而不需要Word文档(.docx)中的字体、页边距等排版信息。 - 运行效率低下: 源文件的存储方式未针对GPU读取进行优化,直接加载会严重影响性能。
-
转换过程 (Importing / Cooking):
- 这个将 Resource 转换为 Asset 的过程,通常被称为 导入(Importing) 或 烘焙(Cooking)。
- 经典案例:贴图转换
- Resource:
.png,.jpg,.tga等常见图片格式。 - Asset:
.dds(DirectDraw Surface) 格式。 - 优势: DDS是一种 块压缩(Block Compression) 格式(如DXT/BCn),GPU可以极快地解压和采样,无需在CPU端进行复杂的解码。它可以被“直接灌入”显存,极大提升了纹理加载和渲染效率。
- Resource:
第二步:如何管理资产间的关联?
一个完整的游戏角色并非由单个文件构成,而是由模型、贴图、骨骼、动画等多个独立的 资产(Asset) 组合而成。引擎如何知道它们之间的关系?
-
核心观点: 现代引擎是 数据驱动 (Data-Driven) 的。资产之间的关联关系本身,也是一种需要被管理的数据。
-
1. 组合资产 (Composed Asset)
-
为了描述“一个角色由哪些部分组成”,引擎通常会定义一种特殊的“元资产”或“组合资产”。
-
实现方式: 它通常是一个轻量级的文本文件(如
XML,JSON,YAML),内容清晰地定义了各个子资产之间的引用关系。 -
示例: 一个描述机器人角色的组合资产可能如下所示:
<CharacterAsset guid="char_robot_001"> <Mesh>Assets/Meshes/robot.mesh</Mesh> <Material>Assets/Materials/robot_mat.material</Material> <Animations> <Animation name="idle">Assets/Anims/robot_idle.anim</Animation> <Animation name="run">Assets/Anims/robot_run.anim</Animation> </Animations> </CharacterAsset> -
当引擎加载这个
CharacterAsset时,就会根据其中的描述去加载对应的模型、材质和动画文件,将它们组合成一个完整的可动角色。
-
-
2. 引用 (Reference) 与 GUID
-
在上面的例子中,我们使用了文件路径来引用其他资产。但这会带来一个严重的问题: 如果美术师重命名或移动了文件,引用就会失效!
-
解决方案: 使用 全局唯一标识符 (Globally Unique Identifier, GUID)。
-
核心观点: 不通过易变的文件路径来识别资产,而是为每个资产分配一个 永不改变的唯一ID。
-
关键术语: GUID (全局唯一标识符)
-
绝佳类比:
- 文件路径 就像一个人的家庭住址,是会改变的(搬家)。
- GUID 就像一个人的身份证号,是终身不变的。
-
通过GUID,无论资产文件在磁盘上的哪个位置,叫什么名字,引擎总能通过这个唯一的“身份证号”准确地找到它,从而建立起一套健壮、可靠的资产引用网络。这是现代大型游戏项目能够高效协作的基石。
-
游戏资产管理与引擎核心循环
本部分深入探讨了现代游戏引擎中两个至关重要的基础系统:资产管理系统和 游戏主循环(Game Loop)。理解这两个系统是掌握引擎架构的关键。
一、 游戏资产的现代化管理 (Modern Management of Game Assets)
在游戏开发中,我们将美术、声音等原始文件(Source Files)转换为引擎可识别的 资产(Assets)。如何高效、稳定地管理这些成千上万的资产,是引擎设计的核心挑战之一。
1. 资产的“身份证”:GUID 系统
-
核心观点: 传统的基于文件路径的资产引用方式非常脆弱。一旦文件移动或重命名,所有引用都会失效,就像一个朋友搬家后,你只记他的住址就再也找不到他一样。
-
解决方案: 为每个资产分配一个 全局唯一标识符(GUID - Globally Unique Identifier)。
- GUID 就像是资产的身份证号,独一无二且永久不变。
- 引擎内部通过 GUID 来引用资产,而不是通过其文件路径。
- 这样,无论资产文件在磁盘上的位置如何变化,引擎总能通过其 GUID 准确地找到它,大大增强了项目的健壮性。
2. 运行时的资产组织:Handle 系统与资产管理器
-
核心观点: 资产加载到内存后,需要一个系统来管理它们的实时状态和相互之间的引用关系。直接使用内存指针(Raw Pointers)进行引用是危险的,因为资产可能随时被卸载,导致指针悬空。
-
关键术语与系统:
- 运行时资产管理器 (Runtime Asset Manager): 这是一个核心模块,负责在游戏运行时跟踪所有已加载的资产,维护它们之间的依赖关系图(例如,模型引用了哪些贴图和材质)。
- 句柄系统 (Handle System): 这是一种间接引用机制,是直接指针的替代方案。
- 工作原理类比: Handle 就像一个邮箱的钥匙或号码。你不需要知道邮箱的主人(资产)具体住在哪里(内存地址),你只需要通过邮箱号码(Handle)去访问。
- 优势: 资产管理器可以通过 Handle 判断一个资产是否仍然有效(邮箱主人是否还在)、是否已加载到内存。这使得资产的加载、卸载和移动对上层逻辑透明,避免了悬空指针等致命错误。
3. 核心挑战:生命周期管理 (Life Cycle Management)
-
核心观点: 现代游戏的场景非常庞大,内存和显存资源有限,不可能将所有资产一次性全部加载。因此, 动态地管理资产的加载与卸载(即生命周期) 至关重要。
-
关键技术与挑战:
- 垃圾回收 (Garbage Collection, GC):
- 当玩家从一个关卡切换到另一个关卡时,旧关卡的许多资产需要被卸载以释放内存。
- 如果 GC 机制设计不佳,一次性卸载成千上万个资产会导致 明显的性能卡顿(Hitch/Stall),严重影响玩家体验。这是引擎性能优化的一个关键难点。
- 延迟加载 (Lazy Loading / Streaming):
- 一种按需加载策略,只在玩家即将看到或用到某个资产时才将其从硬盘加载到内存。
- 常见现象: 在一些开放世界游戏中,当玩家快速移动时,可能会看到远处的模型或贴图先以低分辨率模糊状态出现,然后迅速变清晰。这就是延迟加载和 Mipmap Streaming 在起作用。
- 垃圾回收 (Garbage Collection, GC):
专家洞见: 构建一个游戏引擎,首要任务往往不是实现酷炫的渲染特性,而是 定义数据结构、资产格式 (Schema) 以及一整套稳健的资产管理流程。数据是引擎的基石。
二、 驱动世界的脉搏:游戏循环 (The Game Loop)
当资产管理系统就位后,我们就可以开始构建游戏世界的核心驱动力——游戏循环。
1. 核心概念:Tick
-
核心观点: Tick 是游戏世界的基本时间单位或心跳。它是一个以固定或可变时间间隔(例如每 1/60 秒)不断重复执行的过程。
-
类比: 讲座中将其比作物理学中的普朗克时间——推动我们虚拟世界前进的最小时间片段。
-
功能: 在每一个 Tick 内部,引擎会完成更新整个世界状态并渲染一帧画面的所有工作,包括处理输入、更新动画、物理模拟、执行游戏逻辑、渲染场景等。正是这种高速的循环往复,创造了运动和交互的幻觉。
2. 游戏循环的两大支柱:逻辑 (Logic) 与渲染 (Render)
-
核心观点: 一个设计良好的游戏循环会严格区分游戏逻辑和场景渲染。这两部分通常被称为引擎的“两大神兽”,并且执行顺序是 先更新逻辑,再进行渲染。
-
执行流程与哲学:
Tick Logic(模拟世界): 首先,引擎根据物理规则、玩家输入和AI算法,计算出世界在当前时间点 应该处于什么状态。这个过程与“谁在看”或“从哪个角度看”无关。它是在客观地推进世界状态。Tick Render(观察世界): 在世界状态更新完毕后,渲染模块会根据玩家的摄像机(观察者)的视角,将这个已经确定的世界状态绘制成一个二维图像并呈现到屏幕上。
3. 职责划分:什么属于逻辑?什么属于渲染?
清晰地划分逻辑和渲染的职责,是构建可扩展、可维护引擎架构的基石。
-
逻辑 (Logic) 负责 “是什么” (The State of the World):
- 输入处理: 键盘、鼠标、手柄的输入。
- 物理模拟: 物体运动、重力、碰撞。
- 碰撞检测: 判断“角色A是否击中了敌人B”。
- 游戏规则: 敌人B被击中后扣除5点生命值。
- 动画状态机: 根据角色状态决定播放哪个动画。
- AI行为树: NPC的决策。
-
渲染 (Render) 负责 “看起来像什么” (The Presentation of the World):
- 裁剪 (Culling): 剔除摄像机视野外或被遮挡的物体,避免不必要的绘制开销。
- 光照计算: 计算光线如何与物体表面相互作用。
- 阴影生成 (Shadowing): 投射和绘制阴影。
- 材质与着色 (Shading): 应用材质,执行 Shader 代码,决定物体最终的颜色和质感。
- 后期处理 (Post-Processing): 应用全屏效果,如景深、辉光、色彩校正等。
核心原则: 模拟与表现分离 (Separation of Simulation and Presentation)。无论你是否看见,子弹击中敌人并造成伤害这一“事实”已经发生(逻辑层);而你是否能从你的视角看到这一幕的火花和特效,则是“表现”(渲染层)的工作。
功能层、多核架构与核心层
在上一部分我们了解了游戏循环(Game Loop)中的逻辑(Logic)与渲染(Render)分离的基本概念。这一部分我们将深入探讨引擎的功能层、现代引擎架构的核心趋势——多核并行计算,并开始接触最底层的核心层及其关键组件——数学库。
一、 功能层 (Function Layer): 引擎与游戏的边界
功能层是游戏引擎中内容最庞大、最丰富的部分,它包含了实现游戏玩法的各种子系统。
核心观点
-
功能层的模糊边界: 功能层的一个显著特点是,引擎的功能和特定游戏的功能之间的界限常常是模糊的。
- 案例:相机系统。引擎提供基础的相机矩阵变换和绘制能力是明确的。但是,一个具有特殊“手持摇晃感”、“镜头模糊”、“动态变焦”效果的第三人称射击游戏相机,其复杂的控制逻辑究竟应该属于引擎的通用功能,还是特定游戏的代码?这在实践中经常需要权衡。
-
明确的引擎模块: 尽管存在模糊地带,但功能层中仍有一些模块是毫无疑问属于引擎核心的。
- 渲染管线 (Rendering Pipeline): 负责将三维场景转换成二维图像的整个流程。
- 资源管理 (Asset Management): 高效地加载、卸载和管理模型、贴图、声音等游戏资源。
- 物理模拟 (Physics)
- 动画系统 (Animation)
二、 现代引擎的脉搏:多核并行架构的演进
随着 CPU 从单核走向多核,游戏引擎的架构也随之发生了深刻的变革,以充分利用硬件性能。
1. 演进的三个阶段
-
单线程时代 (Single-Threaded): 最早期的架构,所有逻辑、渲染、输入等任务都在一个线程里串行执行,效率低下。
-
简单多线程 (Simple Multi-threading): 利用多核最直接的方式。将大的、独立的任务拆分到不同线程。
- 典型代表: 逻辑线程 (Logic Thread) + 渲染线程 (Render Thread)。
- 还可以有专门的 IO线程 (如资源加载)、网络线程 等。
- 这是许多引擎的基础多线程模型。
-
高级多线程 / 基于任务的并行 (Job-Based Parallelism): 这是现代及未来引擎架构的核心方向。
- 核心思想: 将所有计算任务,无论大小,都拆分成微小的、原子的 任务(Job)。然后由一个任务调度系统(Job Scheduler)将这些 Job 动态分配给所有可用的 CPU核心。
- 目标: 让所有 CPU 核心都“吃满”,最大化并行度,避免“某些核心忙死,某些核心围观”的现象。
2. 未来方向:基于任务 (Job-Based) 的并行计算
这是一个非常高效但也极具挑战的架构。
- 核心挑战: 依赖管理 (Dependency Management)。
- 许多任务之间存在严格的先后顺序,无法完全并行。
- 案例: 角色挥刀并打出粒子特效。
- 动画系统 (Animation Job) 必须先计算出当前帧骨骼的位置,确定“手”在三维空间中的精确坐标。
- 粒子系统 (Particle Job) 必须等待动画系统完成后,才能从“手”的位置生成并发射粒子。
- 设计一个高效、无锁、能正确处理复杂依赖关系的 Job 系统是现代引擎开发的顶级难题之一。
3. 核心建议
对于有志于进行底层引擎开发的工程师,讲座强烈建议:从项目初期就以多核架构的思路来设计和思考整个底层代码,而不是在单线程代码上进行“多线程改造”。
三、 核心层 (Core Layer): 为效率而生的数学库
当我们深入到引擎的最底层——核心层时,首先遇到的就是无处不在的 数学库 (Math Library)。
1. 为什么需要定制数学库?
- 基础要求: 对于大部分游戏逻辑和基础渲染,大学级别的线性代数知识(向量、矩阵、点乘、叉乘、矩阵变换等)已经足够。
- 核心原因: 极致的性能 (Performance)。游戏引擎是 实时应用 (Real-Time Application),每一毫秒都至关重要。系统自带的数学函数库通常为了通用性和精度,牺牲了速度。而游戏引擎的数学库,一切都为效率服务。
2. 经典案例:Carmack 的“快速平方根倒数”
这是游戏开发史上最著名的性能优化代码之一,出自 id Software 的约翰·卡马克 (John Carmack) 为《雷神之锤III》编写的代码。
-
问题: 在光照计算等频繁操作中,需要大量计算一个值的平方根倒数,即
1 / sqrt(x)。这在当时的 CPU 上是一个非常缓慢的操作(浮点数除法和开方指令周期很长)。 -
解决方案: 一段看似“黑魔法”的代码,它比标准库快了数倍。
float Q_rsqrt( float number ) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = * ( long * ) &y; // evil floating point bit level hacking i = 0x5f3759df - ( i >> 1 ); // what the f*ck? (magic number) y = * ( float * ) &i; y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration (Newton's method) // y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration (optional) return y; } -
算法原理:
- 通过一个 “魔法数字” (Magic Number)
0x5f3759df和位操作,快速得到一个平方根倒数的近似解。 - 利用 牛顿迭代法 (Newton's method) 对这个近似解进行一次或两次迭代,快速逼近更精确的结果。
- 通过一个 “魔法数字” (Magic Number)
-
核心思想: 在游戏中, “足够好”的近似解远比“绝对精确”的慢速解更有价值。
3. 现代优化技术:SIMD (单指令多数据流)
在现代 CPU 架构下, SIMD 是数学库性能优化的关键。
- 核心概念: Single Instruction, Multiple Data。即用一条指令同时对多个数据执行相同的操作。
- 应用场景: 完美契合图形学中频繁的向量和矩阵运算。
- 例如,计算两个四维向量(
vec4)的加法:C = A + B。 - 非 SIMD: 需要 4 次独立的浮点数加法指令。
- 使用 SIMD: 仅需 1 条 SIMD 加法指令,就可以同时完成
C.x = A.x + B.x,C.y = A.y + B.y,C.z = A.z + B.z,C.w = A.w + B.w四个操作。
- 例如,计算两个四维向量(
- 关键术语: SSE (Streaming SIMD Extensions) 是 Intel CPU 上的 SIMD 指令集,在引擎代码中经常能看到相关的注释或函数命名。
核心层与平台抽象层
一、 引擎核心层 (Core Layer) - 性能的基石
引擎的核心层(Core Layer)是整个引擎的基座,它为所有上层功能提供最基础、最高效的服务。这一层的代码质量、稳定性和性能直接决定了引擎的上限。
1. 高效的数学库:榨干CPU的每一分性能
游戏引擎中包含海量的数学运算(如矩阵、向量运算),常规的逐元素浮点数计算无法满足性能要求。
- 核心观点: 必须利用CPU的并行计算能力来加速数学运算。
- 关键术语: SIMD (Single Instruction, Multiple Data)
- 这是一种CPU指令集技术,允许一条指令同时对多个数据执行相同的操作。例如,一条指令完成两个四维向量(每个向量4个浮点数)的相加。
- 具体实现: SSE (Streaming SIMD Extensions)
- 这是Intel CPU上广泛使用的一种SIMD指令集。当你在引擎代码中看到
SSE相关的注释或函数时,就意味着这段代码正在使用CPU的向量化运算能力,其性能通常比传统的浮点运算快好几倍。
- 这是Intel CPU上广泛使用的一种SIMD指令集。当你在引擎代码中看到
2. 自定义数据结构:掌控内存的主动权
虽然C++标准模板库(STL)提供了如vector, list等方便的数据结构(容器),但在游戏引擎中,我们通常会重新实现一套自己的数据结构。
- 核心观点: 标准库(STL)容器为了通用性牺牲了对内存布局和分配策略的精确控制,这对于追求极致性能和内存效率的游戏引擎是不可接受的。
- STL容器的主要问题:
- 内存碎片 (Memory Fragmentation):高频率的增删操作会在内存中产生大量不连续的“空洞”,降低内存使用效率和访问速度。
- 不可控的内存增长 (Uncontrolled Memory Growth):以
std::vector为例,当容量不足时,它可能会粗暴地将容量翻倍。这种策略会导致内存的巨大浪费和不可预测的峰值。
3. 内存管理:性能优化的终极战场
内存管理是核心层最关键、最复杂的任务之一,其重要性堪比一个微型操作系统。
-
核心观点: 现代计算机的性能瓶颈往往不在于CPU主频或内存大小,而在于CPU访问内存的速度。
-
关键术语: CPU Cache (CPU高速缓存)
- Cache是紧贴CPU的一小块、速度极快但价格昂贵的内存。CPU处理数据时,会先从Cache中读取。如果Cache中没有(Cache Miss),才去访问速度慢得多的主内存(RAM)。
- 一个优秀的内存管理策略,其核心目标就是最大化CPU Cache的命中率(Cache Hit)。
-
内存管理的黄金三法则 (The Three Golden Rules) 讲座将复杂的内存管理策略总结为三条源于“图灵机”模型的简单直观法则:
-
数据聚合 (Data Aggregation)
- 法则: 将逻辑上相关的数据,在物理内存中也紧凑地存放在一起。
- 目的:当CPU加载一块数据到Cache时,其邻近的相关数据也一同被加载,极大提高后续访问速度。
-
顺序访问 (Sequential Access)
- 法则: 尽可能按照数据在内存中的物理顺序进行访问。
- 目的:这符合CPU Cache的预取(Prefetch)机制,CPU会猜测你将要访问后续数据并提前加载,从而避免等待。避免“东一榔头西一棒子”的跳跃式访问。
-
批量处理 (Batch Processing)
- 法则: 成批地申请或释放内存/数据,而不是零散地、一个一个地操作。
- 目的:减少内存管理的开销,降低产生内存碎片的概率。
-
掌握这三条法则,你就理解了游戏引擎底层内存优化的核心精髓,足以应对许多复杂的性能问题。
二、 平台抽象层 (Platform Abstraction Layer) - 化解差异,拥抱世界
当引擎的核心功能完成后,下一个挑战就是如何让它在不同操作系统和硬件上运行,例如从Windows移植到macOS、Linux、iOS或Android。
- 核心观点: 通过建立一个抽象层,将所有与平台相关的差异进行封装和隔离,使得上层应用代码可以“一次编写,到处运行”。
- 关键术语: 平台无关性 (Platform Independence)
1. 平台差异的体现
- 文件系统:路径分隔符(
\vs/)、盘符(C:vs/volumes/)。 - 窗口系统:窗口创建、消息循环等。
- 输入设备:键盘、鼠标、手柄的处理。
- 图形API(最复杂的部分):不同平台有完全不同的图形接口。
- PC (Windows): DirectX (DX11, DX12)
- Apple (macOS/iOS): Metal
- Android/Linux: OpenGL ES, Vulkan
2. 渲染硬件接口 (RHI - Render Hardware Interface)
为了解决图形API的碎片化问题,现代引擎都设计了RHI层。
-
核心观点: RHI定义了一套引擎自己的、统一的图形API,将底层具体硬件接口(DX, Metal等)的差异彻底屏蔽。
-
工作原理:
- 定义统一接口:RHI层会定义一套自己的函数,如
RHICreateTexture,RHIDrawPrimitive等。这些通常是 虚函数(Virtual Functions) 或接口。 - 平台特定实现:针对每一个目标平台,开发者需要编写一套具体的实现。例如:
- 在Windows上,
RHICreateTexture内部会调用DirectX 12的CreateCommittedResource。 - 在macOS上,
RHICreateTexture内部会调用Metal的makeTexture。
- 在Windows上,
- 上层统一调用:上层的渲染工程师在开发新功能(如PBR材质、全局光照)时,只需要调用RHI提供的接口,完全无需关心底层是DX12还是Metal。
- 定义统一接口:RHI层会定义一套自己的函数,如
-
开发的巨大挑战: 构建RHI层极其困难。即使是同一家公司推出的API,如 DX11和DX12,其设计理念也发生了根本性的变化 (从驱动代劳到开发者掌控),将这些设计哲学迥异的API统一到一套接口下,是对架构师能力的巨大考验。
平台层与工具链的艺术
在之前的探讨中,我们已经构建了游戏引擎的渲染、功能与资源管理等核心部分。然而,要让引擎真正落地并具备生产力,还有两个至关重要的层次不容忽视: 平台抽象层 (Platform Layer) 和 工具层 (Tool Layer)。这一部分,我们将深入探讨这两个层次的设计哲学与技术挑战。
一、 平台抽象层 (Platform Layer): 看不见的基石
平台层是引擎架构中最底层、最接近硬件的部分。它常常被上层开发者忽略,但其设计优劣直接决定了引擎的可移植性、性能和稳定性,是衡量引擎水平高下的重要分水岭。
核心观点:不仅仅是“编译打包”
平台层的核心职责是封装与抽象不同硬件和操作系统的差异,为上层模块提供一套统一的接口。然而,这绝非简单的API“翻译”工作。
-
挑战1:图形API的深层差异
- 案例:DirectX 11 vs. DirectX 12
- 虽然都来自微软,但DX11和DX12的设计理念截然不同。DX11是高度抽象的API,驱动程序会帮你处理很多底层的资源管理和同步。而DX12则是一个更底层的、“显式”的API,它将大量控制权交还给开发者,要求开发者自行管理内存、同步、指令提交等。
- 错误实践:如果仅仅将DX11的调用逻辑“翻译”成DX12的API,性能不仅不会提升,反而会因为未能利用DX12的多线程渲染、显式资源管理等特性而急剧下降。
- 正确实践:一个优秀的平台层需要为DX12设计一套全新的、能够发挥其底层优势的渲染后端(Render Backend),同时对上层保持接口兼容。这对底层开发者的技术能力要求极高。
- 案例:DirectX 11 vs. DirectX 12
-
挑战2:异构CPU架构的适配
- 案例:PlayStation 3的Cell处理器
- PS3的CPU架构非常独特,它包含一个主核心 PPU (Power Processing Unit) 和多个协处理核心 SPU (Synergistic Processing Unit)。
- 索尼的设计初衷是让开发者将一些独立的计算密集型任务(如AI逻辑、物理模拟、数据预处理)放到高速的SPU上执行,从而为主PPU减负。
- 带来的问题:这破坏了上层代码的平台无关性。难道写AI和逻辑的程序员需要关心他的代码具体运行在哪个核心上吗?如果游戏要移植到PC或Xbox,这些平台的CPU架构(如x86、ARM的大小核)完全不同,为SPU写的代码将无法运行,整个任务调度系统都需要重构。
- 平台层的职责:一个健壮的平台层需要设计一个 通用的任务调度系统 (Job System),能够智能地将计算任务分发到不同平台的可用核心上,对上层程序员屏蔽底层硬件的复杂性。
- 案例:PlayStation 3的Cell处理器
关键术语:
- 平台抽象 (Platform Abstraction): 隐藏不同硬件和操作系统细节,提供统一接口的过程。
- 渲染后端 (Render Backend): 平台层中专门负责与特定图形API(如DX12, Vulkan, Metal)交互的部分。
- 异构计算 (Heterogeneous Computing): 使用不同类型处理单元(如CPU, GPU, SPU)协同工作的计算模式。
二、 工具层 (Tool Layer): 释放创造力的引擎
如果说引擎的运行时(Runtime)部分是游戏的“骨架与血肉”,那么工具层就是赋予游戏“灵魂”的创作平台。它是引擎生产力的直接体现,其重要性和代码量甚至可能超过运行时本身。
核心观点:工具是生产力,是创意的放大器
工具层的核心目标是 为非程序员(美术、策划、关卡设计师)提供创建和编辑游戏内容的能力。
-
以编辑器为中心
- 关卡编辑器 (Level Editor) 是整个工具链的枢纽,它整合了场景搭建、对象放置、逻辑编排等所有功能。
- 专用编辑器 则负责特定类型的资产创作,例如:
- 材质编辑器 (Material Editor): 它的核心价值在于 所见即所得 (WYSIWYG)。确保美术师在编辑器中调整出的材质效果,与最终在游戏运行时看到的效果完全一致,这对于艺术创作至关重要。
- 蓝图/可视化脚本编辑器 (Blueprint/Visual Scripting Editor): 允许策划和设计师不写代码就能实现复杂的游戏逻辑。
-
工具开发的灵活性
- 与追求极致运行效率的运行时不同,工具层的开发更看重开发效率和用户体验。
- 因此,工具的技术选型非常灵活:
- C++/Qt: 传统的、性能优秀的GUI开发方案。
- C#/WPF: 利用.NET生态,快速构建功能强大的现代化UI。
- Web技术 (HTML5/Electron): 甚至可以将整个工具链Web化,实现跨平台的编辑体验。
资产管线 (Asset Pipeline): 连接创意与现实的桥梁
游戏资产并非凭空产生,它们大多来自于专业的第三方软件。如何将这些外部资产高效、正确地导入引擎,是工具层必须解决的核心问题。
-
DCC (Digital Content Creation) 工具:
- 这是对外部专业内容创作软件的统称。
- 常见例子: 3ds Max, Maya (建模/动画), Photoshop (贴图), Houdini (特效), FMOD (音频)。
-
资产调节管线 (Asset Conditioning Pipeline):
- 这是一个将DCC工具产出的原始资产,转换为引擎内部优化格式的自动化流程。
- 本质: 一系列的 导出器 (Exporter) 和 导入器 (Importer)。
- 流程示例:
- 美术师在Maya中完成一个角色模型。
- 使用引擎提供的 Maya导出器插件,将模型导出为一种通用格式(如FBX)或引擎自定义的中间格式。
- 引擎的资产导入器监测到新文件,自动或手动将其导入。
- 导入器会进行一系列处理:转换数据格式、压缩纹理、生成LOD、优化网格等,最终生成引擎运行时使用的、高度优化的 内部资产格式 (Internal Asset Format)。
-
数据互通性的挑战:
- 在复杂的开发流程中,几十种工具产生的数据需要顺畅地交互。这要求有一个统一或兼容的数据标准。
- 行业标准: FBX (Autodesk主导,流行于游戏业多年)、 USD (Universal Scene Description) (Pixar开创,正成为影视和游戏领域的新趋势)。
- 引擎实践: 尽管有通用标准,但大多数商业引擎为了追求极致的加载速度和内存效率,最终都会将资产转换为自己高度优化的私有二进制格式。
关键术语:
- 所见即所得 (WYSIWYG - What You See Is What You Get): 编辑器中的视觉呈现与最终产品中的完全一致。
- DCC (Digital Content Creation): 数字内容创作工具。
- 资产调节管线 (Asset Conditioning Pipeline): 将外部DCC资产转换为引擎内部格式的自动化流程。
- FBX / USD: 行业通用的三维数据交换格式。
三、 总结:为何要分层?—— 封装复杂性的艺术
现代游戏引擎是一个极其复杂的系统,其复杂度堪比模拟一个微缩的“黑客帝国”。分层架构是管理这种复杂度的唯一有效手段。
-
核心思想:解耦 (Decoupling)
- 通过分层,每一层都只关注自己的核心职责,依赖下层提供的服务,并为上层提供服务。
- 这种 关注点分离 (Separation of Concerns) 使得开发者可以在不了解整个系统的情况下,专注于修改和优化某一个特定层,极大地降低了心智负担。
-
城市生态系统类比 (Analogy)
- 平台/核心层 就像城市的 基础设施 (Infrastructure):供水、供电、排污系统。市民(上层开发者)无需关心其运作原理,但离开它城市便无法运转。
- 资源管理层 就像城市的物流与调度系统:确保物资供应、垃圾处理等资源高效流转。
- 功能/游戏逻辑层 就像城市的商业与社会活动:人们在基础设施之上,利用物流系统,创造财富和价值,这才是城市(游戏)的最终目的。
这种 封装 (Encapsulation) 的思想是现代系统工程的基石。它将一个巨大到无法理解的复杂问题,分解成若干个可被理解、可被管理、可被迭代的子问题,最终构建出宏伟而有序的系统。对于小明来说,要让他心爱的角色在屏幕上动起来,背后正是这样一套庞大而精密的工程体系在支撑。
分层架构与实践
本讲座的核心在于阐述现代复杂系统(尤其是游戏引擎)设计的基石—— 分层架构 (Layered Architecture)。通过理解分层思想,我们不仅能掌握引擎的宏观结构,更能学会在实际开发中如何思考和组织代码。最后,讲座将引出一个专为本课程设计的教学引擎“Pilot”,作为理论联系实际的桥梁。
一、 核心设计哲学:封装与分层
复杂的世界和系统之所以能被我们理解和管理,得益于一个关键概念: 封装 (Encapsulation)。
- 核心观点:通过将底层的复杂性封装成更高层次、更易于理解和操作的单元,我们可以构建出极其复杂的系统。
- 类比说明:
- 原子 (Atoms) → 篮球 (Basketball) → 篮球比赛 (Basketball Game)
- 在这个过程中,我们关注的焦点从微观的原子运动,上升到宏观的物体交互,再到更高层的规则与策略。每一层都隐藏了下一层的细节,使我们能专注于当前层面的问题。
- 游戏引擎的启示:游戏引擎正是这种思想的完美体现。它将硬件、操作系统、图形API等底层细节封装起来,为上层的游戏逻辑开发提供了一个稳定且功能强大的平台。
二、 游戏引擎的分层架构原则
一个设计精良的游戏引擎,其分层结构通常遵循以下几个至关重要的原则。
1. 原则一:稳定性与灵活性 (Stability vs. Flexibility)
- 核心观点: 越底层的代码越稳定,改动越少;越上层的代码越灵活,改动越频繁。
- 具体分层表现:
- 底层 (平台层、核心层):这些是引擎的基石,如内存管理、数学库、平台抽象等。它们一旦稳定下来,数年都不会有大的改动。对它们的任何修改都可能引发上层系统的连锁反应。
- 上层 (功能层、工具层):这些层直接服务于游戏玩法和开发效率。例如,某个特定的游戏玩法功能、一个新的编辑器工具等,这些需求变化快,代码也随之频繁迭代。
- 架构优势:这种设计使得引擎在面对不断变化的游戏需求时,无需重构整个系统,只需在上层进行扩展和修改,保证了项目的稳定性和开发效率。
2. 原则二:单向依赖 (Unidirectional Dependency)
- 核心观点: 上层可以调用下层的功能,但下层绝对不允许反向调用上层的功能。
- 重要性:这是维持分层结构清晰、避免“循环依赖”和“意大利面条式代码”的关键。它保证了系统的依赖关系是单向的、可预测的,使得每一层都可以被独立地理解和测试。
3. 原则三:职责定位 (Responsibility Placement)
- 核心观点: 在实现一个新功能前,首要任务是思考“这个功能应该属于哪一层”,而不是急于编写算法。
- 开发实践:一个受过良好系统训练的工程师,在接到需求后,会首先进行架构层面的思考。例如,一个“新的资源类型”需求,他会清晰地规划:
- 资源层:需要添加哪些代码来解析和管理这种新资源。
- 核心层:是否需要新的数据结构或算法支持。
- 功能层:如何使用这种新资源来实现游戏逻辑。
- 工具层:编辑器需要做什么样的扩展来支持新资源的导入和编辑。
教学引擎介绍
为了将理论付诸实践,课程组专门开发了一个小而精的教学引擎,旨在作为学习者探索复杂引擎世界的“领航员”。
1. 引擎概览
- 愿景:帮助开发者理解一个完整的游戏引擎是如何从零开始一步步构建起来的。
- 技术栈:C/C++
- 代码规模:总计约 15,100 行
- Runtime (运行时): ~13,000 行
- Editor (编辑器): ~2,100 行
- 平台支持:跨平台,已支持 Linux、Mac,并正在适配 M1 芯片。
- 架构特点:严格遵循课程中讲解的分层架构,代码结构清晰工整,是学习引擎架构的绝佳范例。
2. 核心功能与亮点
尽管“麻雀虽小”,但“五脏俱全”,包含了现代引擎的诸多核心特性:
- 可视化编辑器 (Editor):支持场景的拖拽、编辑和属性修改。
- 属性反射系统 (Property Reflection):在编辑器中点击任何对象,其所有可编辑属性都会动态显示出来,无需为每个类硬编码UI。
- 一键运行 (One-Click Play):实现编辑器与游戏运行模式的无缝切换。
- 分阶段开源计划:
- 初期版本 (本周五发布):包含编辑器、相机控制、基础渲染、UI系统和一键运行功能。
- 后续版本:将随课程进度逐步放出 动画系统 (Animation) 、 物理碰撞 (Physics) 、 网络同步 (Networking) 等高级功能。
- 最大亮点:完整的ECS架构:
- 引擎内置了一套小而完整的 ECS (Entity Component System, 实体组件系统) 架构。
- ECS 是现代游戏引擎(如Unity DOTS, Bevy)中用于构建游戏对象和逻辑的主流模式,具有高性能和高数据局部性的优点。
- ECS实现甚至支持 多线程 (Multi-threading),这对于一个教学引擎来说是非常超前和宝贵的。
四、 本讲总结:三个必须记住的核心概念
即使忘记了所有细节,也请务必记住以下三点,它们是理解现代游戏引擎架构的基石。
-
引擎是分层架构 (Layered Architecture)
- 五层模型:平台层 → 核心层 → 资源层 → 功能层,以及贯穿其上的工具/编辑器层。
-
底层稳定,上层灵活 (Stability vs. Flexibility)
- 引擎设计的核心权衡:底层追求极致的稳定和性能,上层追求高度的灵活性和可扩展性,以适应多变的游戏玩法需求。
-
Tick函数是世界的心跳 (The
TickFunction is the World's Heartbeat)- 整个游戏世界是通过一个不断循环的
Tick函数来驱动更新和渲染的,它是引擎的“主心跳”。
- 整个游戏世界是通过一个不断循环的
引擎架构答疑与前沿探讨
本部分是讲座的 Q&A 环节,讲师针对同学们提出的关于课程自研引擎、行业实践、技术选型以及未来趋势等问题进行了解答。这些问答对于深入理解游戏引擎的底层设计哲学与开发实践极具价值。
一、引擎源码与学习建议
这是针对课程配套的自研小引擎的说明和学习指导。
- 核心观点: 引擎源码将采用分步释放的策略,与课程进度保持同步,以避免初学者被庞大的代码量淹没。
- 关键信息:
- 代码发布: 源码目前尚未上传至 GitHub,但会很快发布初版。
- 平台支持: 引擎支持 Windows 平台,并可使用 Visual C++ 进行编译。
- 脚本系统: 当前发布的版本功能较为基础,场景逻辑 硬编码 (Hardcode) 在 C++ 中,尚未开放脚本系统。未来可能会随着课程进度开放脚本与 C++ 的交互接口。
- 给开发者的学习建议:
- 首要任务: 成功编译并运行引擎。
- 核心入口: 找到并理解引擎的主循环函数
tick()。 - 关键分支: 在
tick()函数中,重点关注两个核心逻辑分支:tick_render(): 负责渲染逻辑的更新。tick_logic(): 负责游戏逻辑的更新。
- 学习心法: 理解了
tick驱动的渲染与逻辑分离的更新模式,就抓住了游戏引擎的脉搏,后续的学习将事半功倍。
二、游戏引擎分层架构的共性与差异
讲师对现代游戏引擎(如 UE, Unity)的架构分层进行了总结,指出了其共通的设计模式和主要的差异点。
- 核心观点: 几乎所有现代游戏引擎都遵循相似的分层设计思想,但在具体的功能层实现上会因其设计目标而产生巨大差异。
- 引擎架构的共性 (Commonality):
- 平台无关层 (Platform Abstraction Layer): 封装操作系统和硬件的差异(如文件IO、窗口管理、输入设备),是引擎跨平台能力的基础。
- 核心层 (Core Layer): 提供最基础的服务,如内存管理、数学库、容器、字符串处理、多线程等。
- 资源层 (Resource Layer): 负责管理游戏中的各种资源(模型、贴图、声音等)的加载、卸载和生命周期。
- 核心驱动循环 (Main Loop): 所有引擎都由一个核心循环(通常命名为
tick()或update())以固定的时间步长驱动整个世界的更新,包括逻辑、物理、渲染和网络。
- 引擎架构的差异 (Difference):
- 功能层 (Function Layer): 这一层是引擎最具特色的部分,不同引擎在此的实现差异最大。它包含了渲染器、物理系统、动画系统、UI 系统、脚本系统等高级功能模块。
三、C++ STL 库在游戏开发中的性能瓶颈
这是一个经典的性能话题,解释了为什么高性能游戏引擎倾向于自建一套基础容器库,而不是直接使用标准模板库 (STL)。
- 核心观点: 标准 STL 库在设计时未充分考虑游戏引擎对内存管理效率和缓存友好性的极端要求,直接使用可能导致显著的性能下降。
- 关键问题:
- 内存分配: STL 容器的默认内存分配器可能导致频繁的堆内存申请和释放,引发内存碎片和性能开销。
- 性能敏感度: 在游戏引擎中,不当的内存使用模式可能轻易导致性能下降一个数量级(慢 10 倍)。
- 业界的解决方案:
- ESTL (Electronic Arts STL): 由 EA 寒霜引擎团队开发的 高性能 STL 替代品,它对内存布局和分配策略进行了深度优化,以适应游戏开发的严苛需求。
- 现代 C++ 的演进: 值得庆幸的是,新的 C++ 标准(如 C++17)已经开始吸纳这些高性能编程思想,提供了更多内存管理和性能优化的工具,使得标准库在某些场景下变得更加可用。
四、VR 游戏开发的现状与未来
讲师分享了他对 VR 游戏开发的看法,分析了当前的技术瓶颈和未来的发展潜力。
- 核心观点: 当前 VR 游戏受限于终端硬件算力,开发重点在于强交互体验而非顶级视觉效果;但未来随着技术突破,VR 将成为 超 3A 级 沉浸式体验的终极平台。
- VR 开发的现状与挑战:
- 算力瓶颈: 主流 VR 一体机(如 Quest)的计算能力有限,无法与搭载高端显卡(如 RTX 3090)的 PC 相比,因此难以实现 3A 级的视觉细节。
- 开发策略: 当前 VR 开发更侧重于强交互和沉浸式体验设计,而非堆砌视觉奇观。
- 传输难题:
- 无线传输: PC 到头显的无线串流存在 延迟 (Latency) 问题,影响体验。
- 有线传输: 虽然延迟低,但线缆束缚感强,破坏沉浸感。
- VR 开发的未来展望:
- 硬件升级: 一旦硬件算力、电池技术和无线传输问题得到解决。
- 超 3A 体验: VR 将能够提供前所未有的 超 3A 级游戏体验。届时,引擎需要为双眼渲染超高分辨率(如 8K)、超高帧率(如 120 FPS)的画面,这对渲染技术提出了更高的要求。
五、从零开始搭建小型引擎的思路
针对如何启动一个引擎项目的问题,讲师给出了实践性的建议。
- 核心观点: 现代引擎开发并非严格的自底向上的串行过程,而更像是一个并行开发的小型项目。
- 正确的开发模式:
- 定义规范: 项目初期,团队首先要共同定义好各个模块间的 接口 (Interface) 和 规范 (Specification)。
- 并行开发: 随后,不同的开发者可以同时进行不同层次的开发工作。例如,一人负责平台层,一人负责核心层,另一人负责功能层的某个模块。
- 集成整合: 通过预先定义的接口,将各个模块最终整合在一起。