目录
欢迎来到《游戏引擎实现原理》卷三。卷一《引擎内核》里,我们从零写出了一个能跑的引擎内核 helio——它管内存、管句柄、管实体和组件、管并发。卷二《渲染引擎》里,我们给内核接上了一整套渲染能力,《微光岛》的小岛第一次被画到屏幕上,有光、有影、有后处理、有 UI。但到这一刻为止,这个世界还是静止的:资源要手动塞进去、角色不会动、撞不到东西、没有声音、按键没人听、改一行代码要重启整个游戏。
卷三《子系统与工具》要陪你做的,就是让《微光岛》真正活起来:用 TypeScript 从零实现一整套运行时子系统与工具链——资源管线与流式、动画接入与蒙皮、物理集成、音频、输入、脚本绑定与热重载,以及一个能用的编辑器。
为什么把这些放进同一卷?因为它们共享同一条底层规律:引擎要「认识」数据,才能驱动它、编辑它、热重载它。 资源管线的核心是「资源是什么、谁依赖谁、谁拥有谁」;反射驱动的属性暴露与编辑器属性面板,靠的是同一份类型描述符;命令栈、脏标记、文档模型,是编辑器与运行时之间那条「数据与代码分离」的接缝。把这些讲透,你拿到的不是七个互不相干的模块,而是一套「数据如何在引擎里流动」的统一视角。
市面上讲游戏引擎的书,大多把「子系统」当 API 手册来列。本卷走另一条路:每一节都写代码,每一节的代码都在本机真跑过,跑出来的是真实产物——真实字节数、真实耗时、真实像素、真实音频样本。 凡是给出「这样更好」的结论,背后都有一份能复现的测量。
这一卷写给谁
| 读者 | 现状 | 读完能拿到什么 |
|---|---|---|
| 读过卷一《引擎内核》的读者 | 内核能跑,但没有内容、没有交互 | 一套接在内核之上的子系统与工具,和「数据怎么被引擎驱动」的完整链条 |
| 读过卷二《渲染引擎》的读者 | 画面能画,但世界是死的 | 让渲染出来的世界动起来、碰得到、响得出、按得动、改得了 |
| 想做编辑器 / 工具链的开发者 | 会用现成引擎的编辑器,不知道它怎么搭 | 数据模型、命令栈、反射属性面板、视口与资产浏览器的从零实现 |
| 已懂子系统原理、想落地的读者 | 懂「为什么要有音频总线 / 状态机」,缺「怎么写出来」 | 一条从资源管线到编辑器的可复制实现路径,每节都能跑出产物 |
前置知识:读完卷一《引擎内核》,或至少了解句柄、ECS 与 Job System 的基本概念;会写 TypeScript。不需要安装任何游戏引擎——本卷全部代码在 Node 26 与 headless Chromium 下运行。
三类读者,三类收获
下面把这四类人分别展开,你可以对照自己的处境挑一段读——它决定了你该走哪条阅读路径。
如果你读过卷一、想把内核接出内容。 你手里已经有一个能算的 helio 内核,但它只会算、不会「用」。这一卷正好补上内容与交互那道接缝:内核提供世代句柄,资源层用它做资源引用与引用计数;内核提供固定步长循环,物理集成直接接在上面。读完你得到的是一条把「内容」喂进内核、再由内核驱动的完整通路。
如果你读过卷二、想让画面里的世界动起来。 你已经有了一台能画的渲染器,但小岛是静止的。这一卷给每个物体接上动画与蒙皮、给角色接上物理与输入、给场景接上音频——卷二渲染器一行都不用改,它拿到的仍是一批普通实例,只是这批实例的变换每帧都在变。
如果你想做编辑器 / 工具链。 编辑器四章(第 8–11 章)是本卷最独立的一块:场景文档模型、脏标记、可撤销命令栈、反射驱动的属性面板、视口与资产浏览器。它不是「某个引擎的编辑器怎么用」,而是「编辑器作者怎么从零搭出一个能编辑、能撤销、能存盘的最小编辑器」。
如果你已懂子系统原理、只缺落地。 你懂「为什么要有音频总线」「为什么状态机要配混合树」,缺的是「怎么写出一个可跑的」。这一卷的 33 节本身就是一份施工顺序表:先立资源管线,再补动画、物理、音频、输入、脚本,最后装配编辑器。每一节都能独立跑通,你可以在任何一步停下,把它接到自己的项目里。
与卷一《引擎内核》、卷二《渲染引擎》的关系
三卷不是「上中下三本」,而是**「地基 / 楼 / 生活」**。卷三里的每一行代码,几乎都能在卷一卷二里找到它依赖的那根桩:
| 卷三要建的 | 它接在卷一卷二的哪一处 |
|---|---|
| 资源对象与生命周期 | 复用卷一第 6 章的世代句柄做资源引用 |
| 资源管线与流式 | 消费卷二第 6 章的纹理 / 网格产物与第 4.2 章的上传路径 |
| 动画与蒙皮 | 复用卷二第 7 章的变换层级与第 6.1 章的顶点格式 |
| 物理集成 | 复用卷一第 15 章的固定步长循环与任务图 |
| 音频混音 | 复用卷一第 4 章的缓冲布局;DSP 数学不依赖前两卷 |
| 输入与动作映射 | 复用卷一第 16 章的类型描述符做数据化绑定 |
| 脚本绑定与热重载 | 复用卷一第 3 章的拓扑排序与第 16 章的反射 |
| 编辑器四章 | 复用卷一第 6 章句柄与第 16 章反射、卷二第 7 章场景图与第 11 章视口 |
内核、渲染与子系统的接缝只有一句话:内核产出「数据 + 句柄」,渲染把数据变成像素,子系统让数据动起来、并把数据暴露给工具。 这条缝越清晰,三层越能各自演进——所以卷三往 src/subsys/ 与 src/editor/ 里加代码时,卷一卷二一行都不用改。
还没读前两卷的读者,建议先去卷一目录 与卷二目录 过一遍;至少读完卷一第 4 章(内存布局)、第 6 章(句柄)、第 16 章(反射)与卷二第 6–7 章(几何与场景),再回来看卷三会轻松很多。
本卷的差异化
和「讲子系统原理」或「讲某个引擎编辑器」的书比,本卷有三条很硬的不同:
- 从零实现,不调框架。 我们不接现成的物理库、音频框架或编辑器 UI 框架,而是自己定义资源模型、混音图、命令栈、面板描述模型这一整套抽象,再为它写真实的实现(含一个真的 BC1 编码器、一条真的拓扑序混音链)。你看到的是「引擎作者怎么写子系统」,不是「怎么用某个库」。
- 每节可跑出真实产物。 每一节都产出一份能在本机运行、能读到数字的东西:一份逐帧的流式报告、一份蒙皮基准的计时、一段与浏览器逐样本一致的音频、一张回读像素核对过的视口。结论不是「理论上会更快」,而是「本机实测快了这么多」。
- 真实字节、耗时与样本。 凡是给出「这样更好」的结论,背后都有一份真实测量。看到数字时,请连着它的测量条件一起看,而不是只记那个倍数。
本卷的可运行性口径
本套书的每一节都标注它属于哪一层,你据此知道「这一节的东西能不能在你机器上跑起来」:
| 层级 | 含义 | 你怎么验证 |
|---|---|---|
| L1 | 纯 TypeScript 逻辑,零运行时依赖 | Node 26 直接跑 .ts,或 vitest 跑单测 |
| L2 | 需要 GPU / 浏览器 | 用 headless Chromium 跑(音频、视口、蒙皮 demo) |
| L3 | 只能做类型检查 | tsc --noEmit 通过 |
| L4 | 命令行不可复现 | 正文会明确标注「需手动验证」 |
| B | 性能基准 | 跑出计时 JSON,数字写进正文对照表 |
这五层各自意味着什么,值得展开说清楚:
- L1——能真跑。 资源依赖图、引用计数、关键帧求值、状态机转移、混音求和、输入缓冲、命令栈、文档模型与脏标记,这些都是纯 TypeScript,
node src/xxx.ts就能跑,配vitest就能断言。你看到的每一段 L1 代码,都是在本机跑过、被单测盯过的。 - L2——需要 GPU / 浏览器。 凡是碰 GPU 或 Web Audio 的代码——GPU 蒙皮、视口渲染、
OfflineAudioContext复算——都得靠 headless Chromium 跑起来验证。本卷会给出统一的跑法,让你在本机也能复现每一张图和每一段音频。 - L3——只能类型检查。 编辑器里的 DOM 胶水与控件创建(属性面板、控件映射)依赖只在浏览器里存在的类型,能通过
tsc --noEmit但无法直接执行。这类代码会被明确标注,正文会说明「为什么它跑不了」。 - L4——命令行不可复现。 涉及真实硬件或真实交互的观察(真实音频设备、真机输入、鼠标拖拽 gizmo、真实 HMR 刷新时序)命令行里复现不了。正文会标「需手动验证」,并给出你该观察什么。
- B——性能基准。 凡是给出「快了 N 倍」的结论,背后都有一份 B 级基准:它跑出计时 JSON,原始数字写进对照表。看到 B 级结论时,请连着原始数据一起看。
卷三编辑器 4 章是本卷 L3 / L4 的重灾区:数据模型(第 8 章)与命令栈(第 9 章)是 L1,必须真跑并有 vitest 用例;属性面板(第 10 章)与视口 / 资产(第 11 章)是 L3 / L4,正文会显式标注「需在浏览器中手动验证」,但其中的反射驱动逻辑与拾取几何仍被抽成 L1 部分真跑(10.1 的面板描述模型、11.1 的拾取射线都有 vitest 用例)。
贯穿项目:《微光岛》从静止走进生活
卷一里那个「会走动的方块」,在卷二里被真正画了出来;这一卷里,它要活。全书仍然只有这一个项目:《微光岛》——一款低多边形 3D 收集冒险小游戏。卷三要做的,是给这个小岛接上一整套子系统与工具:
| 需求 | 牵出的技术点 | 对应章节 |
|---|---|---|
| 美术资源进得来、存得下 | 资源模型、导入器、离线产物 | 1、2 |
| 世界很大、内存装不下 | 异步加载、缓存、增量构建、流式区块 | 1、2 |
| 角色会走路、会挥剑 | 剪辑、轨道、求值器、状态机、混合树 | 3 |
| 角色真的「有骨头」 | 骨骼、蒙皮矩阵、GPU 蒙皮、性能基准 | 3 |
| 角色撞得到墙、站得稳 | 物理边界、固定步长、插值、穿透 | 4 |
| 岛上能听见风声、脚步、BGM | 混音图、音频总线、音效池、流式 | 5 |
| 键鼠、手柄、触摸都能玩 | 设备归一化、动作映射、输入缓冲 | 6 |
| 脚本能读写组件、能热重载 | 反射属性、生命周期、模块替换与状态迁移 | 7 |
| 关卡能编辑、能撤销、能存盘 | 文档模型、命令栈、场景格式 | 8、9 |
| 属性面板、视口、资产浏览器 | 反射面板、控件映射、拾取、视口渲染 | 10、11 |
范围红线:本卷做到「《微光岛》在真实运行时的子系统驱动下活起来,并能在编辑器里被编辑、撤销与存盘」为止。网络同步、服务端权威、跨平台打包等更上层的内容留给后续卷次。
关于基准数字的诚实声明
本卷会出现不少性能数字——加载耗时、驻留字节、蒙皮倍数、调色板开销。它们都来自作者本机实测,并附带测量条件。但它们有一个必须说清楚的边界:
这些数字是同机相对量级,绝对数值不可移植。 Node 版本、CPU 调频、后台负载都会影响结果;同一段代码,换个并发上限、换个区块粒度,耗时可能差出一截。所以当你看到「并发 8 时加载耗时降到 94.6 ms」时,请把重点放在方向而不是倍数上。
真正稳定的结论是那些量级差异,比如「并发从 1 到 8,加载 64 个资源的墙钟耗时几乎线性下降」——这种结论不会因为环境微调而翻转。本卷会刻意把「方向性结论」和「具体倍数」分开表述:前者你可以直接采信,后者请当作「作者机器上的参考值」,并鼓励你在自己的机器上复现一遍。
三条阅读路径
本卷 11 章 33 节,一次读完大约 4 周(每天一小时),但不是每个人都该按同一种顺序读。下面三条路径覆盖了绝大多数人的需求。
路径 A:系统通读(约 4 周)
最完整的一条。按顺序走完 11 章,每章的代码都亲手敲一遍。
第 1–2 章(资源与流式)→ 第 3–5 章(动画、物理、音频)
→ 第 6–7 章(输入、脚本绑定与热重载)→ 第 8–11 章(编辑器)
适合谁:想把子系统与工具链从头搭到尾、或打算自研引擎 / 编辑器的读者。第 11 章会把前面所有零件装配成一个能编辑、能撤销、能存盘的最小编辑器,不要跳过。这条路径最慢,但拿到的是一套完整的、能自己复现的运行时与工具。
路径 B:只读运行时子系统(第 3–7 章,约 2 周)
已经有资源管线、只想让世界「动起来」的话:
3.1 → 3.2 → 3.3 → 4.1 → 4.2 → 4.3 → 5.1 → 5.2 → 5.3 → 6.1 → 6.2 → 6.3 → 7.1 → 7.2 → 7.3
适合谁:时间有限、目标是「让游戏跑起来」的读者。这 15 节从动画一路走到脚本热重载,是一条自洽的线。建议至少把第 3 章(动画与蒙皮)和第 4 章(物理)完整读——它们决定了角色「看起来对不对」和「撞起来稳不稳」。
路径 C:直奔编辑器(第 8–11 章,约 1–2 周)
只想知道「一个编辑器怎么从零搭出来」的话:
8.1 → 8.2 → 8.3 → 9.1 → 9.2 → 9.3 → 10.1 → 10.2 → 10.3 → 11.1 → 11.2 → 11.3
适合谁:已经有一个能跑的游戏、想给它配一个编辑器的读者。这条路径最独立,但会牺牲「资源与运行时怎么把数据准备成可编辑的形状」。建议把 8.1 编辑器与运行时的关系 当作前置先读,它给了整条「编辑器与运行时共享什么、隔离什么」的全貌。
路径 D:按问题查(随时)
不打算通读,只在遇到具体问题时翻对应章节。
加载慢 / 进度条不动 → 1.2;内存泄漏 → 1.3;改一个资源要重导全部 → 2.2
世界太大装不下 → 2.3;动画过渡生硬 → 3.2;角色动起来掉帧 → 3.3
物理抖动 / 穿透 → 4.2;第一次没声音 → 5.3;跳跃被「吃掉」 → 6.3
脚本回调顺序乱 → 7.2;编辑器 Play 后编辑态被污染 → 8.1;没有 Ctrl+Z → 9.1
每个组件都要手写面板 → 10.1;鼠标点不中物体 → 11.1
适合谁:已经有一个能跑的游戏、正被某个具体问题卡住的读者。下节「常见问题速查」就是为这条路准备的——它把「你想解决的问题」直接映射到「去看哪一节」。
33 节完整索引
下面把 33 节按三个部分分组,每节链到它的正文。想跳读时,从这里直接进。
第一部分:资源与流式(第 1–2 章)
让资源「进得来、存得下、装得对」——从资源模型与依赖图起步,经异步加载、引用计数、离线导入、增量构建,最后落到运行时的流式区块。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 1.1 | 1.1 资源模型、GUID 与依赖图 | GUID、类型与路径;Kahn 拓扑加载序 + DFS 三色循环检测 |
| 1.2 | 1.2 异步加载与进度 | 并发上限、优先级队列、in-flight 去重、字节进度回调 |
| 1.3 | 1.3 引用计数、生命周期与释放 | 引用计数表、延迟释放队列、帧末 collect 统一释放 |
| 2.1 | 2.1 资源导入器与离线处理 | 注册表分派、版本失效、PPM / BC1 / mip 链、自描述产物 |
| 2.2 | 2.2 缓存、热重载与增量构建 | FNV-1a 内容哈希、依赖键沿 DAG 传播、增量重导脏子树 |
| 2.3 | 2.3 流式加载与区块(《微光岛》的岛) | 4x4 区块、距离优先级、每帧字节预算、双半径滞回 |
第二部分:运行时子系统(第 3–7 章)
让《微光岛》「动起来、碰得到、响得出、按得动、改得了」——动画与蒙皮、物理集成、音频、输入、脚本绑定与热重载。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 3.1 | 3.1 剪辑、轨道与求值器 | 时间轴钳制 / 循环 / 乒乓、关键帧二分、step / linear / cubic 与 slerp |
| 3.2 | 3.2 状态机与混合树 | 参数驱动转移、exitTime、any-state、交叉淡化与一维混合树 |
| 3.3 | 3.3 蒙皮集成与性能基准 | CPU LBS + WGSL 蒙皮、CPU / GPU 逐顶点比对、蒙皮代价基准 |
| 4.1 | 4.1 引擎与物理库的边界 | 判别联合碰撞体、PhysicsBackend 接口、ColliderSync 版本同步 |
| 4.2 | 4.2 固定步长、插值与穿透 | 固定步长累加器、上一态与当前态插值、扫掠 AABB 防穿透 |
| 4.3 | 4.3 空间查询、分层与事件 | 位掩码分层、raycast 与 overlap、enter / exit / stay 三态事件 |
| 5.1 | 5.1 混音图与音频总线 | 有向图、拓扑序求和、OfflineAudioContext 逐样本比对 |
| 5.2 | 5.2 音效池、BGM 与流式 | voice 池、优先级抢占、淡入淡出包络、流式重采样与欠载检测 |
| 5.3 | 5.3 Web Audio 的限制与解锁 | 自动播放解锁、渲染量子量化、延迟补偿、设备缓冲 |
| 6.1 | 6.1 设备抽象与事件队列 | 四种设备归一化 InputEvent、帧内队列、边沿 / 电平语义 |
| 6.2 | 6.2 动作映射与重绑定 | 动作→控制绑定表、多设备并行、冲突检测、版本化持久化 |
| 6.3 | 6.3 输入缓冲与手感 | 跳跃缓冲、土狼时间、输入延迟补偿(手感数值属 L4) |
| 7.1 | 7.1 反射驱动的属性暴露 | 类型描述符→属性视图、类型强制、只读与隐藏、最小变更集 |
| 7.2 | 7.2 组件脚本的生命周期 | onCreate / onUpdate / onDestroy、状态机、帧末延迟卸载 |
| 7.3 | 7.3 模块替换与状态迁移 | 数据与代码分离、版本戳、迁移链升版(真实刷新时序属 L4) |
第三部分:编辑器(第 8–11 章)
让内容「做得出来、改得回去」——场景文档模型、可撤销命令栈、反射驱动的属性面板、视口与资产浏览器。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 8.1 | 8.1 编辑器与运行时的关系 | 共享文档、编辑态 / 运行态两实例、Play / Pause / Stop 与快照 |
| 8.2 | 8.2 文档模型与脏标记 | 扁平节点表、id 与稳定 uid、脏标记沿父链传播、变更记录 |
| 8.3 | 8.3 序列化落盘与场景格式 | JSON 关卡格式、uid 引用、版本字段与迁移、拒绝更高版本 |
| 9.1 | 9.1 命令模式与可撤销操作 | Command 的 do / undo、撤销栈、redo 链断裂、合并判定 |
| 9.2 | 9.2 事务、合并与撤销栈 | 原子事务补偿回滚、时间窗合并、栈条数上限与字节预算 |
| 9.3 | 9.3 与数据模型的接缝 | 文档为唯一编辑目标、SetProperty / AddNode、命令→脏标记→重绘 |
| 10.1 | 10.1 反射驱动的属性面板 | 面板描述数据模型(纯数据,L1);DOM 渲染属 L3 |
| 10.2 | 10.2 编辑器控件与类型映射 | 类型→控件映射表、可覆写注册表(映射 L1);控件创建属 L3 |
| 10.3 | 10.3 多选编辑与批量修改 | 公共属性、全等 / 混合判定、批量命令(L1);面板交互属 L3 / L4 |
| 11.1 | 11.1 gizmo 与拾取射线 | 像素→NDC→世界射线、轴 / 面手柄、沿轴拖拽求解(拖拽属 L4) |
| 11.2 | 11.2 场景视口与渲染接入 | 渲染器接进视口、双相机、片元描边、网格叠加(回读像素核对) |
| 11.3 | 11.3 资产浏览器与关卡编辑 | 资产树、类型判定、稳定 GUID、拖放载荷、关卡加载(拖放属 L4) |
周计划表
按路径 A 的顺序通读,可以照下面这张表安排节奏(每天约一小时):
| 周次 | 章节 | 本周焦点 | 本周产物 |
|---|---|---|---|
| 第 1 周 | 第 1–2 章 | 资源模型、依赖图、异步加载、导入与增量、流式区块 | 一条能拓扑加载、能流式装岛的管线 |
| 第 2 周 | 第 3–5 章 | 动画与蒙皮、物理集成、音频 | 一个会动、会碰、会响的角色 |
| 第 3 周 | 第 6–7 章 | 输入与动作映射、脚本绑定与热重载 | 一套可重绑定的输入与可热替换的脚本 |
| 第 4 周 | 第 8–11 章 | 编辑器四章 | 一个能编辑、能撤销、能存盘的最小编辑器 |
如果只有零散时间,可以只挑第 1–2 章(资源与流式)或第 8–11 章(编辑器)这两段——它们各自成篇,是整卷最容易被单独拿走的两块。
常见问题速查
| 你想解决的问题 | 去看 |
|---|---|
| 资源加载顺序不对、循环依赖报错 | 1.1 |
| 加载慢、进度条不动或停在 0.97 | 1.2 |
| 资源删了还在用 / 内存泄漏 | 1.3 |
| 美术资源进不来、格式不统一 | 2.1 |
| 改一个资源要重导全部 | 2.2 |
| 世界太大内存装不下、区块抖动 | 2.3 |
| 动画时间轴不对、循环跳变 | 3.1 |
| 两段动画过渡生硬、状态切错 | 3.2 |
| 角色动起来掉帧 | 3.3 |
| 不知道物理库该怎么接进引擎 | 4.1 |
| 物理抖动、高速物体穿透 | 4.2 |
| 射线打不中 / 接触事件重复触发 | 4.3 |
| 音效求和爆音、总线增益不对 | 5.1 |
| 同时响太多音效卡顿 | 5.2 |
| 第一次没有声音 | 5.3 |
| 每种设备各写一遍输入分支 | 6.1 |
| 想让玩家自定义按键 | 6.2 |
| 跳跃输入被「吃掉」 | 6.3 |
| 编辑器要读写组件字段 | 7.1 |
| 脚本回调顺序乱、报错就崩 | 7.2 |
| 改脚本要重启整个游戏 | 7.3 |
| 编辑器 Play 后编辑态被污染 | 8.1 |
| 改一个节点不知道要重算谁 | 8.2 |
| 关卡存盘后打不开 / 版本冲突 | 8.3 |
| 编辑器没有 Ctrl+Z | 9.1 |
| 一次多选要撤销十几步 | 9.2 |
| 命令改了数据但界面不刷新 | 9.3 |
| 每个组件都要手写属性面板 | 10.1 |
| 字段该配什么控件 | 10.2 |
| 多选编辑显示不对 | 10.3 |
| 鼠标点不中物体、拖拽漂移 | 11.1 |
| 视口不显示 / 描边不对 | 11.2 |
| 资产浏览器不会建树、关卡装不进来 | 11.3 |
怎么用这份索引
- 想系统学:走路径 A,从第 1 章 1.1 开始。
- 想让世界动起来:走路径 B,从第 3 章 3.1 开始,把运行时子系统 15 节吃透。
- 想搭编辑器:走路径 C,从第 8 章 8.1 开始。
- 想查问题:直接用上面的「常见问题速查」,或跳到对应小节。
- 想配代码:每一节的代码块都标了层级与来源文件,L1 的可以直接复制出来在 Node 26 上跑,L2 的按正文给的 headless Chromium 跑法复现。
- 每篇正文末行都有「上一节 / 下一节」导航,也可以随时回到本页或卷索引 。
相关阅读
- 卷一《引擎内核》目录 :本卷的地基,从零写引擎内核
- 卷二《渲染引擎》目录 :本卷的画面来源,从零接出渲染引擎
- 游戏引擎专题导航 :Unity、Unreal 与 Defold 的选型与入门
- 图形学专题 :渲染管线、着色器与 GPU 的深度文章
- 书稿书架 :全部电子书与创作手记
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。