目录
欢迎来到《游戏引擎实现原理》。这一卷要陪你做一件很实在的事:用 TypeScript 从零写一个 3D 游戏引擎的内核 helio,写到它能让一个小方块在场景里走动。
为什么是「内核」而不是「整个引擎」?因为游戏引擎里最贵、最难、最不容易被讲清楚的部分,恰恰不是那些看得见的画面,而是看不见的底座——内存怎么分配、对象引用怎么不失效、实体和组件怎么组织、帧内怎么并发、数据怎么序列化。这些东西决定了引擎能跑多快、能撑多大、能不能热重载。渲染是把它们的结果画出来,编辑器是把它们的数据编辑出来。先有内核,才谈得上别的。
市面上讲引擎的书大多从「怎么用 Unity」或「图形学管线」切入。本卷走第三条路:每一节都写代码,每一节的代码都在本机真跑过,跑出来的结果就是这一节的结论。
这一卷写给谁
| 读者 | 现状 | 读完能拿到什么 |
|---|---|---|
| 有编程基础、想理解引擎原理 | 会用某个引擎,但不知道里面怎么转 | 一套从零写出的引擎内核,和对照它的工程判断力 |
| 想做自研引擎 / 独立开发 | 有想法,但被「从哪下手」卡住 | 一条可复制的实现路径:先最小可跑,再逐章长大 |
| 前端 / Web 开发者 | 熟 TypeScript,缺系统编程经验 | 内存布局、句柄、并发、反射这些平时被 GC 藏起来的东西 |
| 已读过渲染原理的读者 | 懂管线,但不知道引擎怎么把它组织起来 | 内核与渲染的接缝:句柄怎么传给 GPU、数据怎么上传 |
前置知识:会写 TypeScript(或 JavaScript),了解基本类型与异步;不需要图形学基础,不需要装任何游戏引擎。本卷全部代码在 Node 26 下直接运行,不需要浏览器(渲染要等卷二)。
四类读者,四种收获
下面把这四类人分别展开,你可以对照自己的处境挑一段读——它决定了你该走哪条阅读路径。
如果你有编程基础、想理解引擎原理。 你可能已经用 Unity 或 Godot 做过东西,但每当性能出问题、行为不符合预期时,你只能靠猜和试。这一卷给你的是「里面到底怎么转」的完整链条:从一帧怎么开始,到内存怎么摆、对象引用怎么保证不失效、系统之间怎么并行。读完之后,你手里不只是一个能跑的引擎,还有一套用来解释引擎行为的模型——看到任何引擎的架构图,你能说出每一层大概在做什么。
如果你打算自研引擎或做独立开发。 你缺的往往不是想法,而是「先写哪一块」的次序感。这一卷的 18 章本身就是一份施工顺序表:先立主循环,再补内存与句柄,然后才是容器、数学、ECS、并发、反射,最后装配。每一章都能独立跑通,你可以在任何一步停下,把它接到自己的项目里。第 18 章会把全部零件拼起来,给你一条「从空文件到一个能跑的引擎」的完整参照。
如果你是前端 / Web 开发者。 你的 TypeScript 很熟,但 GC 帮你把内存这件事藏了太久——你很少需要考虑对象在内存里怎么摆、一个引用为什么会失效、一段代码会不会因为缓存不友好而慢十倍。这一卷恰好把这些「被 GC 藏起来的东西」摊开:arena 分配器、对象池、世代句柄、SoA 布局、SharedArrayBuffer 与 Atomics。这些知识迁移到任何语言都成立,也是你从「写页面」迈向「写系统」的那道坎。
如果你已经读过渲染原理。 你懂管线、懂着色器、懂 GPU 在做什么,但你不清楚引擎是怎么把场景数据组织好、再交给渲染的:实体和组件怎么存、句柄怎么传到 GPU、顶点数据怎么上传。本卷讲的正是这层「内核与渲染的接缝」。卷二《渲染引擎》会在这套内核上接出真正的画面,你先在这里把地基打好,卷二才不会踩空。
三条阅读路径
本卷 18 章 54 节,一次读完大约 8 周(每天一小时),但不是每个人都该按同一种顺序读。下面三条路径覆盖了绝大多数人的需求。
路径 A:顺序通读(约 8 周)
最完整的一条。按顺序走完 18 章,每章的代码都亲手敲一遍。
第 1–3 章(地基)→ 第 4–6 章(内存与句柄)→ 第 7–10 章(容器与数学)
→ 第 11–13 章(数据导向与 ECS)→ 第 14–18 章(并发、反射与综合)
适合谁:想真正吃透引擎机制、或打算自研引擎的读者。第 18 章会把前 17 章的零件拼成一个能跑的引擎,不要跳过。这条路径最慢,但拿到的是一套完整的、能自己复现的模型。
路径 B:直奔 ECS 与内存(约 4 周)
已经懂容器与数学、只想看引擎最核心的两块的话:
4.1 → 4.2 → 5.1 → 6.1 → 6.2 → 6.3 → 11.1 → 11.2
→ 12.1 → 12.2 → 13.1 → 13.3 → 18.1 → 18.2
适合谁:时间有限、目标明确是「理解引擎最核心的内存与 ECS」的读者。这条路径读得快,但会牺牲一部分「为什么」。建议至少把第 4 章(内存)完整读——它是后面所有性能结论的地基;跳过它,11、12、13 章的实测数字会变成一堆没有根据的结论。
路径 C:按问题查(随时)
不打算通读,只在遇到具体问题时翻对应章节。
帧率/暂停问题 → 第 2 章;引用失效 → 第 6 章;性能上不去 → 第 11、13 章
存档读不出 → 第 17 章;多核跑不满 → 第 14、15 章;编辑器拿不到类型 → 第 16 章
适合谁:已经有一个能跑的项目,正被某个具体问题卡住的读者。下节「常见问题速查」就是为这条路准备的——它把「你想解决的问题」直接映射到「去看哪一节」。
贯穿项目:《微光岛》
全书只有一个项目:《微光岛》——一款低多边形 3D 收集冒险小游戏。玩家操控一个角色在一座程序化生成的小岛上走动,收集散落的「微光」,昼夜循环随时间推移。
选它,是因为它体量小到能从零写完,又覆盖了 3D 游戏的全部关键面:
| 玩法要素 | 牵出的技术点 | 对应章节 |
|---|---|---|
| 角色在岛上移动 | 帧循环、固定步长、时间缩放 | 2 |
| 场景里成百上千个物体 | 内存布局、对象池、句柄 | 4、5、6 |
| 物体挂在父子层级下 | 变换、矩阵、四元数 | 8、9 |
| 大量同类物体批量更新 | 数据导向、ECS、查询调度 | 11、12、13 |
| 程序化生成岛屿 | 确定性、定点数、可复现随机 | 10 |
| 加载与保存进度 | 反射、序列化、版本迁移 | 16、17 |
| 多核并行更新 | Job System、依赖 DAG | 14、15 |
范围红线:本卷只做到「一个小方块在场景里走动」为止。真正的画面要等卷二《渲染引擎》。这不是偷懒,而是先把地基夯到能承重。
本卷的可运行性口径
本套书的每一节都标注它属于哪一层,你据此知道「这一节的东西能不能在你机器上跑起来」:
| 层级 | 含义 | 你怎么验证 |
|---|---|---|
| L1 | 纯 TypeScript 内核,零运行时依赖 | Node 26 直接跑 .ts,或 vitest 跑单测 |
| L2 | 需要浏览器渲染 | 用 headless Chromium 跑(本卷用不到,卷二大量使用) |
| L3 | 只能做类型检查 | tsc --noEmit 通过 |
| L4 | 命令行不可复现 | 正文会明确标注「需手动验证」 |
| B | 性能基准 | 跑出 JSON,数字写进附录 C |
这五层各自意味着什么,值得展开说清楚:
- L1——能真跑。 这是本卷的主战场。内存分配器、句柄系统、容器、数学、ECS、Job System,全部是纯 TypeScript、零运行时依赖,
node src/xxx.ts就能跑,配vitest就能断言。你看到的每一段 L1 代码,都是在本机跑过、被单测盯过的。 - L2——需要浏览器。 本卷是内核,一节都用不到 L2。它存在的意义是给卷二《渲染引擎》留位置:等到 GPU 相关的代码登场,就得靠 headless Chromium 跑起来验证。提前写在这里,是为了让你知道「本卷为什么没有它」。
- L3——只能类型检查。 少数代码依赖 Node 26 跑不出来的类型特性(比如平台相关的声明),能通过
tsc --noEmit但无法直接执行。这类代码会被明确标注,正文会说明「为什么它跑不了」。 - L4——命令行不可复现。 涉及真实硬件、真实时间或平台差异的部分(比如跨线程的真实调度抖动),命令行里复现不了。正文会标「需手动验证」,并给出你该手动观察什么。
- B——性能基准。 凡是给出「快了 N 倍」的结论,背后都有一份 B 级基准:它跑出 JSON,原始数字汇总在附录 C。看到 B 级结论时,请连着附录 C 的原始数据一起看,而不是只记那个倍数。
关于基准数字的诚实声明
本卷会出现不少性能数字——内存占用、遍历耗时、加速比。它们都来自作者本机实测,并附带原始数据(附录 C)。但它们有一个必须说清楚的边界:
这些数字是同机相对量级,绝对数值不可移植。 V8 的 JIT 会在运行时把热点代码编译成机器码,GC 会在不确定的时刻介入,微基准对循环写法极其敏感——同一段代码,换个循环顺序、换个预热次数,结果可能差 50%。所以当你看到「SoA 比 AoS 快 1.6 倍」时,请把重点放在方向而不是倍数上。
真正稳定的结论是那些量级差异,比如「对象数组的堆增量是 60 MB,TypedArray 是 0 MB」——这种结论不会因为写法微调而翻转。本卷会刻意把「方向性结论」和「具体倍数」分开表述:前者你可以直接采信,后者请当作「作者机器上的参考值」。附录 C 会给出测量方法,鼓励你在自己的机器上复现一遍。
全书目录
第 1 部分 地基
引擎是什么、一帧怎么转、子系统怎么活。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 1.1 | 引擎的分层与模块边界 | 平台层到游戏层的五层划分,对照 Unity / Unreal / Godot / Bevy |
| 1.2 | 本书范围与站内分工 | 本卷讲什么、不讲什么,与站内既有渲染与引擎文章的分工 |
| 1.3 | 一个引擎最少要有哪些零件 | 最小可运行引擎的零件清单与骨架代码 |
| 2.1 | 帧、时间与帧预算 | 帧预算的构成,滑窗统计帧耗时 |
| 2.2 | 固定步长、累加器与渲染插值 | 固定步长循环、插值系数、死亡螺旋与钳制 |
| 2.3 | 时间缩放、暂停与慢动作 | 缩放时间轴与未缩放时间轴,暂停时 UI 仍推进 |
| 3.1 | 子系统注册、启动顺序与逆序关闭 | 依赖拓扑排序启动、逆序关闭、循环依赖检测 |
| 3.2 | PAL 平台抽象层 | 时间、文件、线程、日志的平台抽象与 Node / Web 双实现 |
| 3.3 | 断言、日志分级与配置系统 CVar | 断言语义、五级日志与分类开关、类型化配置项 |
第 2 部分 内存与句柄
引擎里最贵的东西,是内存。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 4.1 | 为什么要自己管内存 | 分配节奏与布局可预测性,实测堆增量差异 |
| 4.2 | 线性 arena 与栈式分配器 | bump 分配、对齐、峰值水位、帧内临时内存 |
| 4.3 | 对齐、padding 与 TypedArray 上的真实布局 | 手工布局顶点结构时的对齐陷阱与大小计算 |
| 5.1 | 对象池与 freelist | 预分配与空闲链表,对照 new/GC 的实测 |
| 5.2 | 块分配器与大小类 | 按 2 的幂分大小类,固定 buffer 上切块与碎片统计 |
| 5.3 | 内存标签、预算与泄漏检测 | 按标签统计占用、超预算告警、分配计数泄漏检测 |
| 6.1 | 为什么指针会失效 | 悬垂引用在 TS 里的表现:旧引用读到新对象 |
| 6.2 | slot map 与世代句柄 | 索引 + 世代的句柄编码,槽位复用与失效检测 |
| 6.3 | 稀疏集、句柄回收与 ECS 的接缝 | 稀疏集 O(1) 增删查与紧凑遍历,接上 ECS |
第 3 部分 容器与数学
自己写的容器,和游戏里真正用得上的数学。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 7.1 | 动态数组与开放寻址哈希 | 增长策略与开放寻址的探测、删除与装载因子 |
| 7.2 | 位集合、环形缓冲与侵入式链表 | 位运算集合、无分配环形缓冲、零额外内存的链表 |
| 7.3 | 字符串 intern、资源 GUID 与名字表 | 字符串驻留、句柄化的名字与资源标识 |
| 8.1 | 向量代数与 TypedArray 布局 | 向量运算与连续内存布局,零分配约定 |
| 8.2 | 矩阵、行/列主序与左/右手坐标系 | 主序与手性的约定,以及它们带来的转置陷阱 |
| 8.3 | 变换复合与逆变换 | 平移旋转缩放的复合顺序与逆变换求法 |
| 9.1 | 欧拉角、万向节死锁与四元数 | 万向节死锁的复现与四元数的引入动机 |
| 9.2 | 四元数运算与最短弧插值 | 乘法、共轭、归一化与 slerp 的最短弧处理 |
| 9.3 | 曲线、样条与缓动 | 贝塞尔、Catmull-Rom 与常用缓动函数 |
| 10.1 | 浮点确定性与跨端一致性 | 浮点在不同平台上的不可复现性从何而来 |
| 10.2 | 定点数:为确定性模拟准备 | 定点数的表示、运算与它换来的确定性 |
| 10.3 | 数值稳健:epsilon、饱和与除零 | 比较阈值、饱和运算与除零防护的工程做法 |
第 4 部分 数据导向与 ECS
把数据摆对,比把算法写巧更重要。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 11.1 | 缓存层级与访存代价 | 缓存层级与连续 / 随机访存的实测差距 |
| 11.2 | SoA / AoS / AoSoA 的真实差异 | 三种布局的速度与内存实测,以及什么时候不该用 SoA |
| 11.3 | 从 OOP 到批处理的重构方法 | 一步步把一个面向对象的更新循环改造成批处理 |
| 12.1 | 从继承到组合到数据 | 三种组织方式的对照与它们各自的代价 |
| 12.2 | 组件存储:稀疏集与 archetype | 两种组件存储方案的实现与取舍 |
| 12.3 | 实体生命周期与句柄复用 | 实体的创建、销毁与句柄安全复用 |
| 13.1 | 查询匹配与缓存命中 | 组件集合的匹配算法与查询结果缓存 |
| 13.2 | 迭代顺序、变更检测与脏标记 | 稳定的迭代顺序、变更检测与脏标记传播 |
| 13.3 | 系统依赖声明与并行调度 | 声明系统间的读写依赖并推导并行分组 |
第 5 部分 并发、反射与综合
让它跑满多核,让它能被编辑,最后把它拼起来。
| 节 | 文章 | 核心内容 |
|---|---|---|
| 14.1 | 为什么 JS 也需要 Job System | worker_threads、SharedArrayBuffer 与主线程之外的计算 |
| 14.2 | 线程池、任务队列与 Atomics 同步 | 工作线程池、任务队列与原子同步原语 |
| 14.3 | work stealing 队列的真实实现 | 每个工作者一个双端队列与窃取策略 |
| 15.1 | 依赖 DAG 与拓扑执行 | 任务依赖图的构建与拓扑执行 |
| 15.2 | 任务图构建与帧内调度 | 每帧构建任务图并调度到工作线程 |
| 15.3 | 与主循环、渲染线程的同步 | 跨线程同步点与帧边界的确立 |
| 16.1 | 引擎为什么需要反射 | 编辑器、序列化与脚本绑定对类型信息的需求 |
| 16.2 | 类型注册与属性描述符 | 类型与属性的描述符设计与注册表 |
| 16.3 | 显式注册表 + 代码生成扫描 | 不用装饰器元数据的反射方案与代码生成 |
| 17.1 | 反射驱动的序列化:JSON 与二进制 | 由类型信息驱动的两种序列化格式 |
| 17.2 | 引用、句柄与循环的处理 | 对象引用、句柄与循环引用的序列化策略 |
| 17.3 | 版本化与迁移 | schema 版本号与旧存档的迁移路径 |
| 18.1 | 内核装配:把前 17 章拼成引擎 | 把前面所有零件组装成一个可运行的引擎 |
| 18.2 | 《微光岛》第一个能走动的方块 | 用拼好的内核跑出第一个确定性 demo |
| 18.3 | 回顾与路线:进入渲染卷 | 本卷小结与卷二《渲染引擎》的衔接 |
序、前言与附录
| 篇目 | 内容 |
|---|---|
| 序 | 为什么在 2026 年还要从零写一个引擎 |
| 前言 | 本套书的技术选型、可运行性口径与环境准备 |
| 附录 A helio 内核 API 与模块速查 | 全书出现的模块、类型与关键 API 一览 |
| 附录 B 环境与复现速查 | Node 版本、依赖、如何跑通每一层的代码 |
| 附录 C 实测数据汇总 | 全书基准的原始数字与测量方法 |
| 附录 D 常见错误与 FAQ | 易错点、报错含义与排查清单 |
常见问题速查
| 你想解决的问题 | 去看 |
|---|---|
| 帧率不稳、逻辑和渲染对不上 | 2.2 |
| 暂停游戏时 UI 却停住了 | 2.3 |
| 子系统启动顺序错了导致崩溃 | 3.1 |
| 每帧都在分配、GC 频繁触发 | 4.1 、5.1 |
| 内存越跑越碎、分配越来越慢 | 5.2 、5.3 |
| 对象删了但旧引用还能读到东西 | 6.1 、6.2 |
| 遍历实体时性能上不去 | 11.2 、13.1 |
| 模型旋转后出现奇怪的翻转 | 9.1 、8.2 |
| 旋转插值走了「远路」、转半圈才到位 | 9.2 |
| 跨设备 / 跨平台模拟结果对不上 | 10.1 、10.2 |
| 数值比较飘忽、除法偶发炸出 Inf | 10.3 |
| 存档在新版本读不出来 | 17.3 |
| 存档里的对象引用变成了空 | 17.2 |
| 单核跑满、其他核闲着 | 14.1 、15.2 |
| 想让编辑器编辑属性却拿不到类型 | 16.2 、16.3 |
| 反射元数据到处散落、难以维护 | 16.3 |
| 更多报错原文与排查清单 | 附录 D |
怎么用这份索引
- 想系统学:走路径 A,从第 1 章 1.1 开始。
- 想抓重点:走路径 B,从第 4 章 4.1 开始。
- 想查问题:直接用上面的「常见问题速查」,或跳到对应小节。
- 想配代码:每一节的代码块都标了层级与来源文件,L1 的可以直接复制出来在 Node 26 上跑,跑不通再回附录 B 对版本。
- 每篇正文末行都有「上一节 / 下一节」导航,也可以随时回到本页。
相关阅读
- 游戏引擎专题导航 :Unity、Unreal 与 Defold 的选型与入门
- 图形学专题 :渲染管线、着色器与 GPU 的深度文章
- Defold 专题 :轻量引擎的子系统视角
- 书稿书架 :全部电子书与创作手记
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。