10.2 下一步:读源码、做贡献、做自己的引擎
读到这里,你已经跟着 helio 把一个引擎从内存分配器一路搭到了发布清单。但这本书能给你的,不是「一套可以照抄的代码」——helio 是一个为了教学而刻意做小的引擎,它甚至跑不出一款能上架的游戏。它真正能给你的,是三样东西:读懂真实引擎源码的能力、参与开源项目的基础、以及按正确顺序做自己引擎的路线图。
这一节不讲新原理,只回答一个很实际的问题:合上这本书之后,接下来该做什么。答案不是「多读几本书」,而是三条具体的路——去读源码、去做贡献、去做自己的引擎。下面分别给出可执行的入口、一个能跑的自检工具,再诚实地划出本书够不到的边界。
先给一个总的判断:这三条路不是三选一,而是一个自然的递进。读源码让你见识真实工程怎么写,做贡献让你在别人的工程里改出第一个被合并的提交,做自己的引擎让你把学到的东西从头组装一遍。很多人卡在第一步出不来——读了很久,却从没动过手。所以如果只能记住一句话,那就是:边读边改边做,别停在读。
还有一个心态上的提醒:不要等「准备好了」再开始。引擎不是先学完所有原理再动手,而是动手过程中发现缺什么、再去补什么。本书的每一章其实都是这个循环的产物——先写出一个能跑的版本,遇到问题,才回头把那一章的原理讲透。你现在拥有了这个循环的另一半:知道问题大概会在哪里出现。
这三条路还有一层时间上的关系:读源码和做贡献可以并行,做自己的引擎最好在做过贡献之后再开始。因为一旦你亲手在别人的工程里改过代码、被评审过、合并过,你对「一个引擎的代码该长什么样」就有了参照,自己做的时候会少走很多弯路。反过来,如果一上来就闭门造车,很容易花几个月造出一个只有自己看得懂、也说不清为什么这么写的项目。
读源码:从哪几个真实引擎入手
读源码最容易犯的错是「从第一个文件读到最后一个文件」。正确的做法是带着一个问题进去:比如「它的实体句柄怎么防止失效」「它的帧图怎么决定 Pass 顺序」。有了问题,源码就从一个无边的迷宫变成一张有目标的图。你在这本书里已经亲手写过 arena、slot map、ECS、帧图,所以看到别人实现时,能很快认出「这就是那类问题的另一种解法」。
具体的方法可以拆成四步。第一步,先读接口再看实现:一个模块的头文件或类型定义往往就把设计意图说清了,实现只是细节。第二步,边读边画调用链:从一次入口调用出发,记下它依次碰了哪些模块,画完你就有了那个系统的心智模型。第三步,用自己的话复述一遍「它是怎么解决那个问题的」,讲不出来就说明还没读懂。第四步,对照本书的实现:同一个问题,helio 怎么解、它怎么解,差别在哪里,为什么。第四步是收益最大的,因为它把「读」变成了「比较」。
按与 helio 的距离,建议从近到远挑:
- three.js(JavaScript / TypeScript):语言最接近,是理解「场景图 + 渲染列表 + 材质」这套 Web 引擎结构的入口。先看它的渲染器与场景对象两个核心文件,看它怎么把场景遍历成一次绘制调用。
- Bevy(Rust):一个以 ECS 为骨架的引擎,它的 ECS crate 是独立、可单独阅读的,适合拿来对照本书内核卷第 12–13 章的组件存储与查询设计。
- Godot(C++):功能完整但结构清晰,
core与渲染服务两端分开,适合理解「引擎内核 + 渲染服务」的分层,对照本书内核卷第 1 章的分层图。 - Filament(Google,C++):纯渲染引擎,帧图与资源生命周期做得很干净,适合对照本书渲染卷第 14 章。
Unreal 和 Unity 这类超大引擎不建议通读,它们更适合按系统查:你遇到某个具体机制,去官方文档和源码里定位那一个子系统即可。这几款引擎的源码都在各自官网或代码托管平台上公开,从官方文档的「架构概览」页进入仓库是最稳的路径,不要靠二手博客里的片段。
下面这张小表把「带着的问题」映射到「建议去读哪个引擎的哪一块」,可以当作读源码的起点清单:
| 你带着的问题 | 建议从哪读起 |
|---|---|
| 场景图怎么变成绘制调用 | three.js 的渲染器与场景对象 |
| 实体句柄怎么防止失效 | Bevy 的 ECS crate、本书内核第 6 章 |
| 组件怎么按查询连续摆放 | Bevy 的 ECS crate、本书内核第 12 章 |
| 引擎内核与渲染服务怎么分层 | Godot 的 core 与渲染服务 |
| 帧图怎么决定 Pass 顺序 | Filament 的帧图、本书渲染第 14 章 |
| 着色器变体怎么组织 | 各引擎的材质系统、本书渲染第 10–11 章 |
| 资源怎么异步加载与释放 | three.js 的资源管理、本书子系统第 1 章 |
| 编辑器怎么撤销重做 | Godot 的编辑器模块、本书子系统第 9 章 |
读源码时还要留意它没有做什么。一个成熟引擎为了兼容无数种项目,往往背着一堆历史包袱;你看不懂某段代码,很多时候不是因为你水平不够,而是因为那段代码在为一个你永远不会遇到的历史场景兜底。helio 没有这些包袱,这正是它的价值:它把「核心原理」和「历史包袱」分开了,你在这里学到的是前者。
另一个常见误区是把源码当文档读。源码只告诉你「它是怎么做的」,不告诉你「它为什么这么做」——后者通常在 commit message、设计文档或 issue 讨论里。看到一个奇怪的设计,先去翻它的历史讨论,往往比盯着代码猜半天更有效率。这也是开源项目一个被低估的宝藏:每一次有争议的设计决策,都在 issue 里留下了完整的辩论。
读源码的节奏也很重要。不要贪多,一周只啃一个系统,但每个系统都按上面四步走完。三个月下来,你会发现自己已经能独立看懂一个中型引擎的核心部分——这个速度比「一年读十本书」快得多,因为它把被动接收换成了主动拆解。
也不要只读一个引擎。同一个问题,看两个不同引擎怎么解,理解会立刻上一个台阶:比如「实体怎么表示」,一个引擎用句柄、另一个用指针加引用计数,把两种方案摆在一起,你才真正理解「世代句柄」解决了什么别人解决不了的问题。本书内核卷反复做这种对照,读源码时也该保持这个习惯。
无论读哪个引擎,读完一个系统就写一段自己的复述,能写出来才算真读进去。这个习惯会直接决定你在下一步做贡献时的效率。
做贡献:从第一个 PR 开始
读源码读顺手之后,最自然的下一步是给项目提一个 PR。开源引擎的贡献门槛没有想象中高——维护者最缺的往往不是「天才算法」,而是愿意认真读代码、把一个小问题修干净的人。但有几个规律值得先知道:
- 挑对入口。文档修正、补一个边界测试、复现一个已知 bug,都是很好的第一个 PR。多数项目会用
good first issue之类的标签专门标出这类任务。 - 先读贡献指南。每个项目根目录都有贡献说明,规定了代码风格、提交信息格式、测试要求。先读它,再动手。
- 小步走。一个 PR 只做一件事。把「修 bug」和「顺手重构」混在一起,评审会很难通过。
- 先讨论再动手。对较大的改动,先在 issue 里说明思路,得到维护者认可再写代码,能避免白做。
贡献类型按难度大致分三档,第一次从最低档开始:
| 贡献类型 | 典型难度 | 从哪找 |
|---|---|---|
| 文档修正、注释澄清 | 低 | 仓库文档、good first issue 标签 |
| 补测试、复现 bug | 低到中 | issue 区、未覆盖的分支 |
| 修小 bug、改进报错信息 | 中 | bug 标签、help wanted 标签 |
| 实现一个小特性 | 中到高 | 路线图、help wanted 标签 |
| 性能优化、重构 | 高 | 维护者指派的 issue |
提交之前,可以用下面这份清单自检一遍。它不是规定,但能挡掉大多数「被一句话打回」的情况:
| 自检项 | 为什么重要 |
|---|---|
| 问题真的存在吗 | 避免「凭直觉改」被否掉 |
| 复现步骤写清楚了吗 | 评审者要能自己复现 |
| 一个 PR 只做一件事吗 | 混杂改动难以评审 |
| 测试补了吗 | 没有测试的修复很容易回归 |
| 遵循了项目的代码风格吗 | 风格不符会增加评审成本 |
| 提交信息说清「为什么」了吗 | 「改了什么」看 diff 就知道 |
为什么本书能帮你做贡献?因为贡献的门槛往往不在「会不会写代码」,而在「能不能读懂别人的代码、能不能判断一个改动是否合理」。你在本书里建立的那套判断——内存该不该在这里分配、这个句柄该不该复用、这个抽象是不是过度——正是评审和维护代码时最需要的东西。一个只会在自己代码里加功能的开发者,和一个能在陌生代码库里定位问题并最小化修改的贡献者,差别就在这里。
还有一点常被忽略:提 PR 之前先确认问题真的存在。很多新手会凭直觉「觉得这里该改」,结果维护者一句「这是有意为之」就否掉了。花十分钟复现问题、写清「什么输入 → 什么现象 → 期望什么」,比多写五十行代码更值钱。这也是本书工程卷反复强调的「先度量、再动手」在协作场景里的翻版。
第一次提 PR 大概率会被要求改几轮,这很正常,不是被否定。把评审意见当成一次免费的代码审查——维护者在教你他所在项目的判断标准,这种反馈是闭门造车永远得不到的。改完之后,你对「什么样的代码算好代码」的理解会比读十篇文章更具体。
还有一条容易被忽视的贡献方式:把你在本书里踩过的坑写成文档或示例。如果你在用某个引擎时发现「这个机制没写清楚」,那多半别人也没搞懂。把「什么输入、什么现象、正确做法」写成一段能跑的最小示例提交上去,价值往往比修一个 bug 还大,因为它一次帮到了所有后来者。本书工程卷里那些「可运行产物」小节,本质上就是这种贡献思路的产物。
做贡献还有一个隐性的收获:你会被迫把「我觉得」变成「我验证过」。在自己项目里,一个含糊的判断可能永远没人挑战;但在开源评审里,「为什么这么改」必须给出证据。这种「让判断站得住脚」的习惯,恰恰是本书从第 1 章到第 10 章一直在训练的东西。
如果第一个 PR 被拒了,别灰心。被拒的原因通常有三种:问题不存在、方向不对、或者时机不对(维护者正在重构那一块)。前两种让你学到项目真正的判断标准,第三种只需要等一等。真正该在意的不是「这次有没有合并」,而是「我有没有比上一次更懂这个项目」。
做自己的引擎:路线与自检
如果你想做自己的引擎,最稳的路线就是本书四卷的顺序,但每一卷都只做最小可用版本,先让它跑起来,再加下一块。一个务实的里程碑序列:
- M1 内核:arena 分配器 + slot map 句柄 + 固定步长主循环 + 一个子系统,目标是把一个方块动起来。
- M2 渲染:一层 RHI 抽象 + 一个后端(WebGPU 或 WebGL2)+ 画出一个三角形,目标是看到画面。
- M3 场景:变换层级 + 视锥剔除 + 排序批次,目标是能画多个物体且不卡。
- M4 子系统:资源、动画、物理、音频、输入按游戏需要按需接入,不要一次全上。
- M5 工程:测试、打包、性能预算,目标是从「能跑」变成「能交付」。
每个里程碑都可以在本书里找到对应的章节作为参考,也有一个明确的「完成标志」——不要在没有完成标志的情况下往下走,否则很容易同时开五个坑:
| 里程碑 | 目标 | 本书参考 | 完成标志 |
|---|---|---|---|
| M1 内核 | 方块能动 | 内核第 2、3、6、18 章 | 主循环稳定跑、句柄不失效 |
| M2 渲染 | 看到画面 | 渲染第 2、3、5 章 | 屏幕上出现一个三角形 |
| M3 场景 | 画多物体不卡 | 渲染第 7–9 章 | 上千物体仍达标帧率 |
| M4 子系统 | 游戏能玩 | 子系统第 1、3、6 章 | 有资源、输入、动画 |
| M5 工程 | 能交付 | 工程第 1–5 章 | 有测试、有首包体积、能构建 |
如果把它铺成一个可执行的节奏,大致是这样一张周计划。它不是死规定,只是给你一个「什么时候该干什么」的参照,避免前松后紧或者反复推倒重来:
| 阶段 | 主要任务 | 产出 |
|---|---|---|
| 第 1–2 周 | 读内核卷前 6 章,写 arena 与 slot map | 能分配、能安全引用 |
| 第 3–4 周 | 主循环、子系统、ECS 最小版 | 方块能被系统驱动 |
| 第 5–6 周 | RHI 抽象与一个后端 | 屏幕出现三角形 |
| 第 7–8 周 | 场景图、剔除、排序 | 多物体稳定绘制 |
| 第 9–10 周 | 资源、输入、动画按需接入 | 一个能操作的小场景 |
| 第 11 周 | 测试与打包 | 可构建、有回归 |
| 第 12 周 | 性能预算与收尾 | 达标帧率、体积可控 |
做自己的引擎最常见的几个反模式:一上来就做编辑器(编辑器依赖反射和文档模型,底座没稳之前做它等于给自己挖坑)、一上来就做多后端(第一个后端都没跑通就抽象多后端,抽象必然是错的)、过早抽象(没有第二个用例之前不要抽公共层)、以及追求「完整」(引擎永远做不完,先做出能玩的东西)。还有一个更隐蔽的:照抄别人的架构但不懂为什么——看到某个引擎用了某种存储布局就跟着用,却说不清它换来了什么。本书每一章都在讲「为什么」,就是为了让你不犯这个错。
选语言和平台时,别被「引擎一定要用 C++」的说法绑住。对学习而言,选你最熟的语言,把精力放在原理上,比用不熟的语言硬啃内存管理划算得多。helio 用 TypeScript 写,一样把内存布局、句柄安全、任务调度这些「系统级」问题讲到了实处。等你对原理有把握了,再换语言重写一遍,那时候你会发现难的从来不是语法。
起步阶段最常被问的几个问题,答案其实都很朴素:
| 常见问题 | 朴素答案 |
|---|---|
| 用什么语言写 | 选你最熟的,原理和语言无关 |
| 要不要用现成的库 | 物理、音频、数学用成熟库,引擎骨架自己写 |
| 从哪里开始 | 从主循环和一个方块开始,别从编辑器开始 |
| 什么时候算做完 | 永远做不完,以「能玩一个小 demo」为界 |
| 卡住了怎么办 | 回本书对应章节,或去读同类引擎的源码 |
| 要不要一开始就多平台 | 不要,先在一个平台上跑通 |
还有一点值得说:做引擎最耗人的不是写代码,而是调试看不见的东西。内存布局错了、句柄失效了、帧图排错了,症状往往是「画面闪一下」「偶尔崩一次」,没有报错信息。所以本书把工程卷的测试与调试放在很重的位置——一套能复现问题的测试,比十个「我觉得应该没问题」有用得多。你自己做引擎时,也要尽早建立「能复现」的能力,哪怕只是一个每帧打印关键状态的小面板。
下面这个 L1 脚本 tools/engine-checklist.mjs 可以给你自己的引擎量进度:它对照本书归纳出的 12 项核心能力,检查一个工程目录里是否已经落地。
// file: tools/engine-checklist.mjs
// tier: L1
// verify: node tools/engine-checklist.mjs
//
// 引擎能力自检:对照本书四卷归纳出的 12 项核心能力,检查一个引擎工程
// (默认 src/)里是否已经落地。做自己的引擎时用它量进度。
import { readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
const ROOT = process.argv[2] ?? 'src';
function allFiles(dir, out = []) {
for (const name of readdirSync(dir).sort()) {
const full = join(dir, name);
if (statSync(full).isDirectory()) allFiles(full, out);
else out.push(full);
}
return out;
}
// 每一项能力:名字 + 能证明它已落地的文件名关键词
const CAPABILITIES = [
['内存分配器', ['arena', 'object-pool', 'pool-allocator']],
['世代句柄', ['slotmap', 'sparse-set']],
['容器', ['dyn-array', 'hash-map', 'ring-buffer', 'bitset']],
['数学', ['vec3', 'mat4', 'quaternion', 'transform']],
['ECS', ['archetype-world', 'sparse-world', 'mini-ecs']],
['任务系统', ['job-system', 'task-graph', 'work-stealing']],
['反射与序列化', ['type-registry', 'binary-serde', 'serializer']],
['RHI 抽象', ['/rhi/']],
['帧图', ['framegraph']],
['资源系统', ['/resource/', '/res/']],
['调试与遥测', ['diagnostics', 'frame-timer', 'memory-tags']],
['单元测试', ['.test.ts']],
];
const files = allFiles(ROOT);
const impl = files.filter((f) => !f.endsWith('.test.ts'));
let done = 0;
console.log(`== 引擎能力自检(${ROOT},共 ${files.length} 个源文件)==`);
for (const [name, keys] of CAPABILITIES) {
const hit =
impl.find((f) => keys.some((k) => f.includes(k))) ??
files.find((f) => keys.some((k) => f.includes(k)));
const ok = hit !== undefined;
if (ok) done += 1;
console.log(`${ok ? '[x]' : '[ ]'} ${name.padEnd(14, ' ')} ${hit ?? '(缺)'}`);
}
console.log(`进度:${done} / ${CAPABILITIES.length} 项能力已落地`);
拿它扫一遍本书的 helio 引擎,本机输出:
== 引擎能力自检(src,共 289 个源文件)==
[x] 内存分配器 src/core/arena.ts
[x] 世代句柄 src/core/slotmap.ts
[x] 容器 src/core/bitset.ts
[x] 数学 src/core/mat4-demo.ts
[x] ECS src/ecs/archetype-world.ts
[x] 任务系统 src/core/job-system.ts
[x] 反射与序列化 src/core/binary-serde.ts
[x] RHI 抽象 src/render/rhi/capability.ts
[x] 帧图 src/render/framegraph/aliasing.ts
[x] 资源系统 src/render/res/buffer-roundtrip.ts
[x] 调试与遥测 src/core/diagnostics.ts
[x] 单元测试 src/core/arena.test.ts
进度:12 / 12 项能力已落地
12 项全中,说明 helio 虽然小,但一个引擎该有的骨架它都搭齐了——这正是这本书想证明的:一个引擎的核心不在于行数,而在于那十几项能力有没有被正确实现。把同一个脚本指向你自己的工程,缺哪一项一目了然;那份「缺」的清单,就是你下一步的待办列表。这个检查只看文件是否出现,是粗糙的启发式,但对「我做到哪了」这个问题,它比凭感觉靠谱得多。
也可以把这个脚本接到你的 CI 里:每当新增一块能力,就在清单里补一条关键词,让「能力覆盖」变成可回归的指标。这本身就是工程卷第 2 章「让测试守住底线」在个人项目里的最小实践。
需要提醒的是,这个清单只衡量「有没有」,不衡量「好不好」。12 项全绿不代表你的引擎就没问题——它只说明骨架齐了,接下来要面对的才是真正难的部分:这些模块之间的接缝对不对、性能够不够、边界情况处理没有。清单是起点,不是终点。
本书不覆盖的边界
最后必须说清这本书没有覆盖什么。下面这几个方向每一个都够写一整本书,本书只到「引擎能跑」为止,再往下的深度留给站内更专门的专题:
| 本书不覆盖的方向 | 为什么单列 | 站内去哪读 |
|---|---|---|
| 地形与开放世界流式 | 需要专门的空间分区、LOD 与流式调度 | /graphics-terrain-large-world/
、/game-level-streaming-loading/ |
| 网络同步 | 涉及状态同步、回滚与一致性,是独立课题 | /game-networking-rollback/ |
| 光线追踪与全局光照 | 需要专门的降噪与混合渲染管线 | /graphics-raytracing-hybrid/
、/graphics-real-time-gi/ |
| 体积云与水体 | 需要体积渲染与大气散射的专门模型 | /graphics-volumetric-clouds-atmosphere/
、/graphics-water-ocean-rendering/ |
这些不是本书的疏漏,而是刻意的边界:一本讲「引擎实现原理」的书,如果把每一个渲染专题都展开,就永远讲不到「一个引擎如何被交付出去」。本书选择把交付这条线走完,把专题的深度交给站内 /posts/graphics/
与 /posts/game/
两个专题。想继续深入,从这两个入口往下走即可。
如果你确实要做带开放世界或联机的项目,正确的姿势不是「在 helio 上加」,而是先学那条专题的原理,再决定它怎么接进你的引擎。地形流式会反过来改变你的资源系统和空间分区设计,网络同步会反过来改变你的实体与时间模型——这些是架构级的输入,不能等引擎定型了再补。本书没把它们塞进来,正是因为它们不是「加一章」的量级。
也要承认,即使是本书覆盖到的部分,helio 走的也是「够用就好」的路线:它的物理是简化版、音频只覆盖 Web Audio 能做的、脚本系统没有接真实的脚本语言。这些取舍在书里都明说过,它们的目的是让每一行代码都能被讲清,而不是造一个能和商业引擎竞争的产物。你如果真的做商业项目,这些地方该换成熟库就换——本书教的是「怎么把库接进引擎」,不是「怎么自己造轮子」。
这条边界其实也回答了一个常被问的问题:「学完这本书能做出商业引擎吗?」不能,也不该指望能。商业引擎的复杂度有相当一部分来自「要同时满足成千上万个项目」这个约束,而不是来自原理本身。本书给你的是原理和一套参考实现,是「读得懂、改得动」的起点,不是终点。
反过来说,正因为划清了边界,这本书才敢把覆盖到的部分讲透。一本什么都想讲的书,最后往往什么都讲不深。与其把 60 章摊薄到每一个渲染专题上,不如把「一个引擎从零到交付」这条主线走完整——主线走通了,你再去看任何一个专题文章,都知道它该插在整条链的哪个位置。
所以最后对这本书的定位再说一次:它不是一份可以直接拿来发布的引擎,而是一份可运行的说明书——把「一个引擎由哪些原理构成」这件事,从抽象的概念落成了 187 个模块、16349 行可以逐行读懂的代码。读懂它、改坏它、再修好它,你就有了自己动手做引擎的底气。
合上这本书之后,最该带走的不是某个具体的 API,而是一套拆解问题的习惯:遇到一个陌生的系统,先问「它的数据怎么摆」「它的生命周期谁负责」「它的边界在哪里」。这三个问题,能帮你读懂任何一个引擎;它们也是这本书四卷 60 章反复在示范的东西。
四卷 60 章到这里就全部结束了。接下来轮到你:去读别人的源码,去给别人提 PR,或者去做一个属于你自己的引擎。开始动手,比读完任何一本书都更重要。
阅读导航:上一节:10.1 引擎实现原理全景回顾 · 下一节:回到目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。