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 画到屏幕(必考)”2.1 图形管线各阶段(背流程)
Section titled “2.1 图形管线各阶段(背流程)”顶点数据(顶点位置/法线/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(见下节)
- 纹理图集/压缩:减少状态切换与显存
3. DrawCall(必考中的必考)
Section titled “3. DrawCall(必考中的必考)”3.1 是什么?
Section titled “3.1 是什么?”DrawCall = 一次绘制调用:CPU 向 GPU 提交一次“画这个物体”的命令。每次切换材质/纹理/网格都要一次 DrawCall。
为什么 DrawCall 贵?
- CPU-GPU 通信:每次提交要穿过总线、可能有同步等待
- 状态切换:换材质/纹理 = GPU 重新配置状态(很慢)
- GPU 没事干等 CPU → GPU 利用率下降
性能量级:现代引擎经验:DrawCall 数从几百到几千,超了帧时间就爆;移动端更敏感(几百个就吃力)。
3.2 怎么减少?(背 5 招)
Section titled “3.2 怎么减少?(背 5 招)”| 方法 | 原理 |
|---|---|
| 合批/合并网格 | 同材质物体合并成一个网格一次画完(静态合批) |
| 动态合批 | 动态物体每帧合并(有顶点数/材质限制) |
| 纹理图集(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(必考)”4.1 UObject 是什么?
Section titled “4.1 UObject 是什么?”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← 因为反射+序列化
4.3 UE 的 GC 怎么做?(概念)
Section titled “4.3 UE 的 GC 怎么做?(概念)”UE 的 GC 是可达性分析(Mark-Sweep):
- 从“根对象”(UObject 系统持有的引用)出发
- 标记所有可达对象(被引用 = 可达)
- 清扫:未标记的对象回收
为什么 UE 要 GC 而不是智能指针?
- 关卡/蓝图/编辑器里对象关系网状复杂,手动 delete 容易泄漏
- UPROPERTY 标记的引用 = 根,没标记的引用 GC 不认(会被回收!)
UObject 与智能指针对比:
| UE GC | shared_ptr | |
|---|---|---|
| 循环引用 | 可达性分析天然解决 | 要 weak_ptr 打破 |
| 性能 | 有停顿(小对象多时) | 无 GC 停顿 |
| 场景 | 引擎对象(Actor/组件) | C++ 原生资源 |
面试金句:UE 用“宏 + 代码生成(UHT)“给 C++ 补上了反射,这是 UObject 一切能力(编辑器、蓝图、序列化、GC)的地基;GC 用可达性分析,所以用 UPROPERTY 标记引用很重要,否则对象可能被当成垃圾回收掉。
5. Unity:Mono vs IL2CPP(必考)
Section titled “5. Unity:Mono vs IL2CPP(必考)”5.1 背景
Section titled “5.1 背景”Unity 用 C# 写游戏逻辑,C# 编译成中间语言 IL。IL 怎么变成机器码跑起来?两条路:
| Mono | IL2CPP | |
|---|---|---|
| 执行方式 | IL 在运行时 JIT 编译成机器码 | 先 AOT:IL 转 C++ 再编译成原生机器码 |
| 性能 | 启动快、热更迭代快 | 运行性能好、启动稍慢 |
| 平台适配 | 部分平台受限(iOS 曾禁止 JIT) | 所有平台都行(生成原生代码) |
| 包体积 | 小 | 大(生成代码膨胀) |
| 安全 | 可反编译 | 难反编译(逆向难度高) |
| 热更新 | 支持(运行时改 IL) | 原生代码难热更(要补丁方案) |
一句话对比:Mono = 运行时解释/编译(快迭代、易热更);IL2CPP = 提前转 C++ 编译(性能好、平台稳、防破解)。发布正式包基本用 IL2CPP。
5.2 为什么 IL2CPP 快?
Section titled “5.2 为什么 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. 状态同步和帧同步各传什么?
我的原答:各举一个游戏类型。
📖 答案(先自己答完再展开)
- 顶点着色 → 图元装配/光栅化 → 片元着色 → 输出合并(深度/混合)→ 帧缓冲。
- CPU-GPU 通信开销 + GPU 状态切换(换材质/纹理)慢 + 可能同步等待。
- 合批、纹理图集、GPU 实例化、减少状态切换(任 4)。
- 宏标记(UPROPERTY/UFUNCTION/UCLASS)+ UHT(Unreal Header Tool)扫描头文件生成反射代码。
- 反射、GC、序列化、编辑器集成(还有网络复制)。
- 可达性分析 Mark-Sweep;shared_ptr 要手写所有权、循环引用要 weak_ptr,而引擎对象网状引用关系用 GC 更省心、不泄漏。
- Mono:开发迭代/热更新/中小项目;IL2CPP:发布正式包/性能/防破解/全平台。
- 前向:移动端、透明物体、少光源;延迟:PC/主机、大量点光源场景。
- 视锥剔除 = 相机看不见的物体不画;遮挡剔除 = 被其他物体挡住的不画(更深一层)。
- 状态同步传对象状态(MMO/射击);帧同步传输入(RTS/格斗)。
- GPU Instancing 的完整流程(CPU 侧缓冲 + GPU 侧 instanceID 取数据)?
- UE 的 TArray/TMap 和 std::vector 的区别(UE 内存分配器、强类型)?
- 渲染线程架构:主线程提交 vs 渲染线程 vs GPU 队列(帧同步、命令缓冲)?
- 什么是 PBR(基于物理的渲染)?金属度/粗糙度贴图干嘛的?
- UE 蓝图 vs C++:为什么蓝图慢?(反射调用开销 + 虚拟机执行)
- 移动端渲染优化为什么和 PC 思路不同?(带宽/发热/功耗限制)