《游戏引擎实现原理》10.2 下一步:读源码、做贡献、做自己的引擎

全书的最后一节:给出三条能真正走下去的路——去读 three.js、Bevy、Godot、Filament 这些真实引擎的源码该从哪个文件入手,怎么给开源项目提第一个 PR,以及如何按本书四卷的顺序做自己的引擎并用一个自检脚本量进度。同时明确本书不覆盖的边界:地形流式、网络同步、光追与全局光照、体积云与水体,都指向站内既有文章。

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。开源引擎的贡献门槛没有想象中高——维护者最缺的往往不是「天才算法」,而是愿意认真读代码、把一个小问题修干净的人。但有几个规律值得先知道:

  1. 挑对入口。文档修正、补一个边界测试、复现一个已知 bug,都是很好的第一个 PR。多数项目会用 good first issue 之类的标签专门标出这类任务。
  2. 先读贡献指南。每个项目根目录都有贡献说明,规定了代码风格、提交信息格式、测试要求。先读它,再动手。
  3. 小步走。一个 PR 只做一件事。把「修 bug」和「顺手重构」混在一起,评审会很难通过。
  4. 先讨论再动手。对较大的改动,先在 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 引擎实现原理全景回顾 · 下一节:回到目录 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「engine」更多文章

  1. 《游戏引擎实现原理》工程与实战 目录
  2. 《游戏引擎实现原理》9.3 选型与自研的判断标准
  3. 《游戏引擎实现原理》9.2 同一个特性在不同引擎里的实现路径