跳转到内容

13 · 游戏引擎专项概念

定位:第 7 周扫盲章。这章讲“引擎是什么、渲染怎么工作、UE/Unity 的核心机制”。要求:概念清晰 + 每个话题能说出 1-2 句亮点,不需要会写引擎代码。DrawCall、UE 的 UObject/反射/GC、Unity IL2CPP vs Mono 是最高频三项。 学完标准:能解释什么是 DrawCall 及 3 种减少方法;能讲清 UE 的 UObject/反射/GC 各解决什么问题;能对比 Unity 的 Mono 与 IL2CPP。


1. 游戏引擎是什么?(先建立整体图景)

Section titled “1. 游戏引擎是什么?(先建立整体图景)”

引擎 = 游戏开发所需公共能力的集合,让你专注游戏内容而不是重复造轮子:

┌─────────────────────────────────────────────┐
│ 引擎核心层(每一层都依赖下一层) │
│ 1. 平台层:窗口、输入、文件、图形 API 封装 │
│ 2. 渲染层:场景图、渲染管线、材质、光照、特效 │
│ 3. 游戏逻辑层:场景/实体管理、脚本、事件系统 │
│ 4. 工具层:编辑器、资源导入、序列化、调试 │
│ 横向支撑:物理、音频、动画、网络、内存管理、任务系统│
└─────────────────────────────────────────────┘

主流引擎一句话

  • UE(虚幻):C++ 为主 + 蓝图可视化脚本,重渲染/大型项目,AAA 标配
  • Unity:C# 为主,跨平台轻量,中小型/独立游戏多
  • 自研引擎:大厂为特定游戏/技术定制(《原神》Unity 魔改、腾讯/网易自研)

亮点一句:引擎的本质是”平台隔离 + 公共能力复用“——同样的游戏逻辑在 PC/主机/手机上都能跑,靠的就是引擎把硬件差异封装掉了。


2. 渲染管线(Render Pipeline)——GPU 怎么把 3D 画到屏幕(必考)

Section titled “2. 渲染管线(Render Pipeline)——GPU 怎么把 3D 画到屏幕(必考)”
顶点数据(顶点位置/法线/UV)
│ ① 顶点着色器 Vertex Shader(CPU→GPU)
│ 每个顶点一次:模型→世界→相机→投影变换,算最终屏幕位置
② 图元装配 + 光栅化 Rasterization
│ 把三角形转成像素片元(判断哪些像素被覆盖)
③ 片元着色器 Fragment/Pixel Shader
│ 每个像素一次:算颜色(光照、纹理采样、贴图)
④ 输出合并(深度测试/混合/模板测试)
帧缓冲 → 屏幕
阶段 谁执行 频率 干什么
顶点着色 GPU 并行 每顶点 顶点变换
光栅化 GPU 固定单元 每三角形 三角形→像素
片元着色 GPU 并行 每像素 计算像素颜色
输出合并 GPU 固定单元 每像素 深度/颜色混合

亮点一句:现代 GPU 是极度并行的流处理器——顶点着色器和片元着色器都是成百上千个核同时跑,所以“减少着色器复杂度”和“减少绘制调用”是渲染优化的两大方向。

2.2 渲染路径:前向 vs 延迟(Forward vs Deferred)

Section titled “2.2 渲染路径:前向 vs 延迟(Forward vs Deferred)”
前向渲染 延迟渲染
思路 每个物体直接算光照 先渲出 GBuffer(位置/法线/颜色/金属度),再统一算光照
灯光数量 灯光多时开销爆炸 支持大量光源
缺点 多光源贵 显存带宽大、MSAA 抗锯齿支持差、透明物体要回前向
适用 移动端/少光源/透明物体 PC/主机、大量点光源场景

亮点一句:延迟渲染把“光照计算”从“每物体”改成“每像素一次”,所以灯光再多也只多几次全屏 pass;代价是 GBuffer 吃带宽 + 透明/边缘问题——所以主流引擎常“延迟为主体 + 前向管透明”。

2.3 渲染优化的几个方向(概念)

Section titled “2.3 渲染优化的几个方向(概念)”
  • 视锥剔除(Frustum Culling):相机看不到的物体不提交
  • 遮挡剔除(Occlusion Culling):被挡住的不画
  • LOD(细节层次):远处用低模,近处用高模
  • 合批(Batching):减少 DrawCall(见下节)
  • 纹理图集/压缩:减少状态切换与显存

DrawCall = 一次绘制调用:CPU 向 GPU 提交一次“画这个物体”的命令。每次切换材质/纹理/网格都要一次 DrawCall。

为什么 DrawCall 贵?

  1. CPU-GPU 通信:每次提交要穿过总线、可能有同步等待
  2. 状态切换:换材质/纹理 = GPU 重新配置状态(很慢)
  3. GPU 没事干等 CPU → GPU 利用率下降

性能量级:现代引擎经验:DrawCall 数从几百到几千,超了帧时间就爆;移动端更敏感(几百个就吃力)。

方法 原理
合批/合并网格 同材质物体合并成一个网格一次画完(静态合批)
动态合批 动态物体每帧合并(有顶点数/材质限制)
纹理图集(Atlas) 多张小图拼一张大图 → 同一纹理可合批
实例化(Instancing) 同一网格不同变换(位置/颜色)一次提交 N 个(草地、粒子)
减少状态切换 按材质/纹理排序渲染,减少切换次数

GPU Instancing 示意

// 一颗草一个 DrawCall ?→ 10 万颗草用 1 次 Instancing
// CPU 提交:网格(草) + 10 万组变换数据
// GPU 一次绘制,顶点着色器里按 instanceID 取各自的变换
DrawIndexedInstanced(顶点数, 实例数=100000, ...);

面试金句:DrawCall 瓶颈在CPU 提交命令 + GPU 状态切换,不是 GPU 算不过来;优化思路 = “能合并就合并、能一次画就一次画”,实例化和图集是最常用的两招。


4. UE 的核心机制:UObject / 反射 / GC(必考)

Section titled “4. UE 的核心机制:UObject / 反射 / GC(必考)”

UE 里几乎所有对象都继承自 UObject,它提供:反射、GC、序列化、编辑器集成、网络复制等引擎级能力。

UObject 提供的能力 干什么
反射(Reflection) 运行时能查到类的属性/方法(名字、类型、可编辑性)
GC 垃圾回收 自动管理 UObject 生命周期,不用手写 delete
序列化 保存/加载(存档、关卡、资源)
网络复制 属性标记后自动同步到客户端
编辑器集成 属性面板能显示/编辑对象

4.2 反射(Reflection)是什么?(重点)

Section titled “4.2 反射(Reflection)是什么?(重点)”

反射 = 程序在运行时“了解自己”的能力:能枚举一个类有哪些属性、方法、它们的类型和元信息。

C++ 本身没有反射(运行时不知道类的成员列表),UE 靠宏 + 生成代码实现:

// UE 里用宏标记,UHT(Unreal Header Tool)扫描头文件生成反射代码
UCLASS()
class AMyActor : public AActor {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere) // ← 这个宏让属性进反射系统
float Speed = 10.0f;
UFUNCTION(BlueprintCallable) // ← 这个宏让方法可被蓝图调用
void Jump() { ... }
};
  • 编辑器属性面板能显示 Speed ← 因为反射
  • 蓝图能调用 Jump() ← 因为反射
  • 存档能保存 Speed ← 因为反射+序列化

UE 的 GC 是可达性分析(Mark-Sweep)

  1. 从“根对象”(UObject 系统持有的引用)出发
  2. 标记所有可达对象(被引用 = 可达)
  3. 清扫:未标记的对象回收

为什么 UE 要 GC 而不是智能指针?

  • 关卡/蓝图/编辑器里对象关系网状复杂,手动 delete 容易泄漏
  • UPROPERTY 标记的引用 = 根,没标记的引用 GC 不认(会被回收!)

UObject 与智能指针对比

UE GC shared_ptr
循环引用 可达性分析天然解决 要 weak_ptr 打破
性能 有停顿(小对象多时) 无 GC 停顿
场景 引擎对象(Actor/组件) C++ 原生资源

面试金句:UE 用“宏 + 代码生成(UHT)“给 C++ 补上了反射,这是 UObject 一切能力(编辑器、蓝图、序列化、GC)的地基;GC 用可达性分析,所以用 UPROPERTY 标记引用很重要,否则对象可能被当成垃圾回收掉。


Unity 用 C# 写游戏逻辑,C# 编译成中间语言 IL。IL 怎么变成机器码跑起来?两条路:

Mono IL2CPP
执行方式 IL 在运行时 JIT 编译成机器码 先 AOT:IL 转 C++ 再编译成原生机器码
性能 启动快、热更迭代快 运行性能好、启动稍慢
平台适配 部分平台受限(iOS 曾禁止 JIT) 所有平台都行(生成原生代码)
包体积 大(生成代码膨胀)
安全 可反编译 难反编译(逆向难度高)
热更新 支持(运行时改 IL) 原生代码难热更(要补丁方案)

一句话对比Mono = 运行时解释/编译(快迭代、易热更)IL2CPP = 提前转 C++ 编译(性能好、平台稳、防破解)。发布正式包基本用 IL2CPP。

  • AOT 编译成原生机器码,无运行时 JIT 开销
  • 但“转 C++“不是真的写 C++ 代码——是用 C++ 模拟 CLR(对象模型、GC 都在里面),所以性能提升有限,主要赢在”无需 JIT + 平台一致性”

亮点一句:IL2CPP 是 Unity 为解决“iOS 禁 JIT + 需要原生性能 + 防反编译”做的 AOT 方案——把 C# IL 转成 C++ 再编译,最终跑的是原生代码,代价是打包大、热更难。


6. 引擎其他高频概念(一句话速览)

Section titled “6. 引擎其他高频概念(一句话速览)”
概念 一句话
帧率 vs 刷新率 帧率=引擎产帧数(fps),刷新率=显示器 Hz;不匹配会撕裂/卡顿(垂直同步 VSync 解决)
固定时间步 逻辑/物理固定频率推进(见 12 章)
资源/资产管线 美术产出(模型/贴图/音频)导入引擎 → 压缩/转换/打包成引擎格式
热更新 不重新发布安装包更新内容(Lua/AssetBundle/补丁)
序列化 对象状态 ↔ 字节流(存档、网络、编辑器保存)
网络同步 状态同步(每帧同步位置)/帧同步(只同步输入,确定性执行)
多线程渲染 渲染提交与游戏逻辑并行(提交线程 + GPU 队列)
场景图 物体按父子层级组织,变换继承(局部→世界)

状态同步 vs 帧同步(游戏网络高频)

状态同步 帧同步
传什么 每秒 N 次同步对象状态(位置等) 只传玩家输入,两端各自模拟
优点 反作弊强、弱网适应好 带宽极小、逻辑一致
缺点 带宽大、有插值补偿 必须确定性(浮点一致),反作弊弱
代表 大多数 MMO/射击 RTS/格斗/部分 MOBA

7. 高频面试题 Q&A(合上书能讲)

Section titled “7. 高频面试题 Q&A(合上书能讲)”

Q1:什么是 DrawCall?为什么贵? 一次绘制命令。贵在:CPU 提交跨总线通信 + GPU 状态切换(换材质/纹理)+ 可能同步等待,GPU 干等 CPU。超量级(上千)会掉帧。

Q2:怎么减少 DrawCall?(至少 3 种) 合批(同材质合并)、纹理图集(一张大图省切换)、GPU 实例化(同网格一次画 N 个)、减少状态切换、静态/动态批处理。

Q3:UE 的 UObject 是干嘛的? 所有引擎对象的基类,提供反射、GC、序列化、编辑器集成、网络复制。C++ 本身没有这些,UE 用宏 + UHT 生成代码补上。

Q4:什么是反射?C++ 有吗? 运行时能查询类的成员/方法的元信息(名字、类型、可编辑性)。C++ 原生没有,UE 用宏(UPROPERTY/UFUNCTION)+ 代码生成实现,蓝图/编辑器/序列化都靠它。

Q5:UE 的 GC 怎么做?和 shared_ptr 比呢? 可达性分析(Mark-Sweep):从根引用出发标记可达对象,未标记的回收。优点:循环引用天然解决、适合网状对象;缺点:有 GC 停顿。shared_ptr 无停顿但循环引用要 weak_ptr。

Q6:Unity 的 Mono 和 IL2CPP 区别? Mono:IL 运行时 JIT,迭代快、支持热更、iOS 受限;IL2CPP:IL 转 C++ AOT 编译,性能好、平台全、防反编译、包大、难热更。发布正式包用 IL2CPP。

Q7:前向渲染和延迟渲染区别? 前向:每物体直接算光照,多光源贵;延迟:先渲 GBuffer 再统一算光照,支持大量光源,但吃带宽、透明物体麻烦。PC/主机多光源用延迟,移动端/透明用前向。

Q8:怎么优化渲染性能?(概念级) 视锥剔除、遮挡剔除、LOD、合批减少 DrawCall、纹理图集、GPU 实例化、降低阴影分辨率、减少后处理。

Q9:状态同步和帧同步区别? 状态同步传对象状态(带宽大、好反作弊、弱网好);帧同步只传输入两端确定模拟(带宽小、要确定性、难反作弊)。RTS/格斗用帧同步,射击 MMO 用状态同步。

Q10:垂直同步(VSync)干嘛的? 让帧率与显示器刷新率对齐,防止画面撕裂;代价是输入延迟和帧率锁定(电竞/竞技游戏常关 VSync 换低延迟)。


8. 本章自测(10 题,限时 15 分钟,先自己答再看答案)

Section titled “8. 本章自测(10 题,限时 15 分钟,先自己答再看答案)”

1. 图形渲染管线的主要阶段按顺序写出来。

2. DrawCall 为什么是性能瓶颈?

我的原答:(两个原因)

3. 减少 DrawCall 的 4 种方法。

4. UE 的反射是怎么实现的?

我的原答:(宏 + 什么工具)

5. UObject 提供哪 4 个核心能力?

6. UE GC 的原理是什么?为什么不用 shared_ptr 就够?

7. Mono 和 IL2CPP 各适合什么场景?

8. 前向 vs 延迟渲染,各举一个适合场景。

9. 视锥剔除和遮挡剔除有什么区别?

10. 状态同步和帧同步各传什么?

我的原答:各举一个游戏类型。

📖 答案(先自己答完再展开)
  1. 顶点着色 → 图元装配/光栅化 → 片元着色 → 输出合并(深度/混合)→ 帧缓冲。
  2. CPU-GPU 通信开销 + GPU 状态切换(换材质/纹理)慢 + 可能同步等待。
  3. 合批、纹理图集、GPU 实例化、减少状态切换(任 4)。
  4. 宏标记(UPROPERTY/UFUNCTION/UCLASS)+ UHT(Unreal Header Tool)扫描头文件生成反射代码。
  5. 反射、GC、序列化、编辑器集成(还有网络复制)。
  6. 可达性分析 Mark-Sweep;shared_ptr 要手写所有权、循环引用要 weak_ptr,而引擎对象网状引用关系用 GC 更省心、不泄漏。
  7. Mono:开发迭代/热更新/中小项目;IL2CPP:发布正式包/性能/防破解/全平台。
  8. 前向:移动端、透明物体、少光源;延迟:PC/主机、大量点光源场景。
  9. 视锥剔除 = 相机看不见的物体不画;遮挡剔除 = 被其他物体挡住的不画(更深一层)。
  10. 状态同步传对象状态(MMO/射击);帧同步传输入(RTS/格斗)。

## 9. 进阶追问(答不上来就回来复习)
  • GPU Instancing 的完整流程(CPU 侧缓冲 + GPU 侧 instanceID 取数据)?
  • UE 的 TArray/TMap 和 std::vector 的区别(UE 内存分配器、强类型)?
  • 渲染线程架构:主线程提交 vs 渲染线程 vs GPU 队列(帧同步、命令缓冲)?
  • 什么是 PBR(基于物理的渲染)?金属度/粗糙度贴图干嘛的?
  • UE 蓝图 vs C++:为什么蓝图慢?(反射调用开销 + 虚拟机执行)
  • 移动端渲染优化为什么和 PC 思路不同?(带宽/发热/功耗限制)