技术选型
这一篇是给全书做「使用说明」的:这本书用什么写、为什么这么选、每一节的可运行性怎么划分、代码块上的标注怎么读、环境怎么准备,以及怎么配合目录篇与附录使用。如果你只有五分钟,读完这一篇就能判断该从哪一页开始。
语言:TypeScript
本套书用 TypeScript,运行在 Node 26 上(浏览器端则直接用原生 ESM)。
选它不是因为 TypeScript 最适合写引擎——恰恰相反,真正的引擎几乎都用 C++ 或 Rust。选它是因为一个更朴素的标准:读者必须能把代码跑起来。
| 方案 | 问题 |
|---|---|
| C++ | 需要编译器、CMake、平台 SDK、图形 API 环境;跨平台配置差异大,读者极易卡在环境上 |
| Rust | 需要 cargo 与完整工具链;编译慢,迭代体验差 |
| TypeScript | Node 26 直接跑 .ts,零构建步骤;浏览器里也直接跑 |
另一个理由是:在一个有 GC 的语言里讲手动内存管理,反而更诚实。你不会产生「写了 arena 就没有 GC 开销了」的错觉——你会亲眼看到 process.memoryUsage() 的数字,看到 TypedArray 的 buffer 内容确实不计入 JS 堆,也会看到访问它的代码依然受 JIT 影响。这种带着约束做判断的经验,比在 C++ 里照抄一遍更接近真实的工程权衡。
需要强调:本卷讲的机制与语言无关。内存布局、句柄、ECS、Job System 在 C++ 引擎里一模一样,TypeScript 只是让它们能被所有人跑起来的手段。
渲染目标:为什么是 WebGPU 而不是 WebGL
本卷是内核,用不到渲染。但整套书的渲染目标是 WebGPU,WebGL2 只作为降级路径,这个选择值得先说清楚——因为它直接影响卷二的内核抽象长什么样。
| 维度 | WebGL2 | WebGPU |
|---|---|---|
| 绑定方式 | 全局状态机,靠 bindBuffer 之类切换 | 显式的 GPURenderPipeline,一次绑定一整套状态 |
| 命令提交 | 直接画进默认帧缓冲 | 编码成 GPUCommandBuffer,再统一提交 |
| 计算着色器 | 没有(只能借渲染管线绕) | 原生 compute 阶段,可跑通用计算 |
| 多线程录制 | 不支持 | 支持在 worker 里录制命令缓冲 |
| 资源生命周期 | 隐式,靠引擎猜 | 显式,绑定组与缓冲区都由你管理 |
| 心智模型 | 贴近 OpenGL 的老模型 | 贴近 Vulkan / D3D12 的现代模型 |
选 WebGPU,是因为它的显式风格和本卷要讲的内核思路一致:资源由你分配、生命周期由你管理、命令由你录制。WebGL2 的全局状态机把很多东西藏了起来,而「不藏东西」正是本套书的立场。提前说明,是为了让你知道:本卷的代码与卷二共享同一套内存与句柄抽象,不会白写。
一个必须遵守的约束:erasable syntax
Node 26 直接运行 .ts 时,只做类型剥离,不做语法转换。这意味着有些 TypeScript 语法不能用——它们需要生成运行时代码,而类型剥离不负责这件事。下面是最完整的禁用清单与替代写法:
| 禁用语法 | 为什么不行 | 替代写法 |
|---|---|---|
enum | 需要生成枚举对象与反向映射 | const 对象 + as const,再取同名类型 |
namespace | 需要生成包裹对象与合并逻辑 | 用模块(文件)的 import / export |
构造函数参数属性 constructor(private x: number) | 需要生成「声明字段 + 赋值」代码 | 显式声明字段,在构造函数里赋值 |
import x = require('y') | 需要转换成 CommonJS 调用 | 用 ESM 的 import / export |
装饰器元数据 emitDecoratorMetadata | 需要运行时类型元数据 | 显式注册表 + 代码生成(第 16 章) |
enum 的替代写法值得给一个具体例子:
// ✗ 不行:enum 在 Node 26 下会抛 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
enum LogLevel { Debug, Info, Warn, Error }
// ✓ 可以:用 const 对象 + as const
const LogLevel = { Debug: 0, Info: 1, Warn: 2, Error: 3 } as const;
type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
本卷的 tsconfig.json 里开了 erasableSyntaxOnly: true,写错会被编译器直接拒绝(TS1294),不会留到运行时才炸。这看起来是限制,其实是一种更贴近现代 TS 的写法——用显式的值代替隐式的语法糖,代码反而更清楚。
可运行性口径
本套书的每一节都标注它属于哪一层。这个标注不是装饰,它告诉你这一节的东西能不能在你机器上跑起来。
| 层级 | 含义 | 怎么验证 |
|---|---|---|
| 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 就能断言。L2 本卷一节都用不到——它是给卷二留的位置,写在这里是为了让你知道「本卷为什么没有它」。L3 指少数依赖 Node 26 跑不出来的类型特性的代码,能过 tsc 但无法直接执行。L4 指涉及真实硬件、真实时间或平台差异、命令行里复现不了的部分,正文会标「需手动验证」。B 是所有性能结论背后的基准,原始数字都在附录 C。
关于基准数字的诚实声明
本卷会出现不少性能数字——内存占用、遍历耗时、加速比。它们都满足两个条件:
- 来自作者本机实测,不是估算、不是从别处抄的;
- 附带原始数据,汇总在附录 C 。
但请务必记住:这些数字是同机相对量级,绝对数值不可移植。V8 的 JIT 会在运行时把热点代码编译成机器码,GC 会在不确定的时刻介入,微基准对循环写法极其敏感。同一段代码,换个循环顺序、换个预热次数,结果可能差 50%。
所以看到「SoA 比 AoS 快 1.6 倍」时,请把重点放在方向而不是倍数上。真正稳定的结论是那些量级差异——比如「对象数组的堆增量是 60 MB,TypedArray 是 0 MB」这种,它不会因为写法微调而翻转。
本卷还会专门用一节讲「什么时候不该用数据导向」(11.2 ),因为诚实地承认一种技术的代价,比一味推销它更有价值。
代码块标注约定
本卷的每个代码块,都可能在首行带几个注释标记。它们不是装饰,而是告诉你「这段代码是什么、属于哪一层、怎么验证」。四个标记的完整含义如下:
| 标记 | 含义 | 你要做什么 |
|---|---|---|
// file: | 这是一个完整可编译的文件,路径就是它的归属 | 可以整个复制出来,存成同名文件直接跑 |
// snippet: | 只用于说明的代码片段,不是完整文件 | 理解意图即可,不必单独运行 |
// tier: | 这段代码属于哪一层(L1 / L2 / L3 / L4 / B) | 据此判断它能不能在你机器上跑 |
// verify: | 验证这段代码的具体命令 | 想亲手确认结论时,跑它 |
一个典型的多行示例:
// file: src/game/components.ts
// tier: L1
// verify: node tools/gen-registry.mjs
// @reflect
export interface Vec3 {
x: number;
y: number;
z: number;
}
// @reflect
export interface Transform {
position: Vec3;
rotation: Vec3;
scale: Vec3;
}
看到 // file: src/game/components.ts 与 // verify: node tools/gen-registry.mjs,你就知道:这是一个完整文件,把它存到 src/game/components.ts,再跑那行 verify 命令,就能看到代码生成器为这两个类型产出的注册表。本卷鼓励你这么做——把「读到的结论」变成「自己跑出的结果」,正是这套书想传递的习惯。
环境准备
最低要求
- Node.js ≥ 26.0.0(本卷实测 26.5.0)
node --version # 应输出 v26.x 或更高
Node 26 原生支持直接运行 .ts 文件,不需要 ts-node、tsx 或任何加载器:
node src/core/arena.ts
可选:类型检查与单测
如果你想跟着跑类型检查和单元测试:
mkdir helio-playground && cd helio-playground
npm init -y
npm i -D typescript vitest
package.json 需要设置 "type": "module"。tsconfig.json 建议至少包含:
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"allowImportingTsExtensions": true
}
}
然后:
npx tsc --noEmit # 类型检查
npx vitest run # 跑单测
代码组织约定
本卷的 helio 内核按模块分目录:
helio/
src/
core/ 内存、句柄、容器、数学
ecs/ 实体、组件、系统
jobs/ 任务系统
reflect/ 反射与序列化
platform/ 平台抽象层
每一节会标明代码属于哪个文件,你可以按这个结构逐步搭起来。
读这本书之前,常见的问题
把读者最常问的几个问题集中回答一下,省得你在读的过程中反复分心:
| 问题 | 回答 |
|---|---|
| 一定要装游戏引擎吗? | 不需要。本卷是内核,全部 L1 代码在 Node 26 上直接跑,与任何引擎无关。 |
| 需要显卡吗? | 不需要。本卷不碰渲染,卷二才会用到 GPU 与浏览器。 |
| 需要联网吗? | 不需要。零第三方运行时依赖,单测与基准全部离线可跑。 |
| 数学不好能学吗? | 能。用到的是向量、矩阵、四元数的基础,正文会随用随讲,不预设高等数学。 |
| 学过 C++ 有帮助吗? | 有。内存布局、对齐、句柄这些概念可以直接迁移,你会读得很快。 |
| 会不会和站内引擎专题重复? | 不会。专题是单点深挖,本卷是「从零搭一台能跑的引擎」的递进路径,两者互补。 |
| 读完能直接写商业引擎吗? | 不能,也不该这么期待。这一卷给你的是理解引擎的判断力,不是工业级引擎。 |
| 为什么不用现成的 ECS 库? | 因为本书要讲的正是那些库内部做了什么。理解了机制,用不用库都能做判断。 |
| 需要什么数学基础? | 只要线性代数的基础概念:向量、矩阵、四元数。本卷只讲「在引擎里怎么存、怎么算」,不推公式。 |
| 需要先学 WebGPU 吗? | 不需要。卷一一行渲染代码都没有,WebGPU 从卷二才开始。 |
| 没有游戏开发经验能读吗? | 能。本卷从「内存怎么摆」讲起,不预设任何引擎使用经验。 |
上面三条最容易被人想复杂,单独展开一下。
数学基础。 你真正要动手的运算只有向量加减、矩阵乘法、四元数插值这几样,知道它们能表示位置和旋转就够了;三角函数、复数、微分方程都不是前置条件。
本卷的重点不在公式,而在「这些量在引擎里怎么存、怎么算」——布局、对齐、缓存友好度才是正文反复权衡的东西。需要补推导时,外链到站内图形学专题 ,那里才是讲数学的地方。
要不要先学 WebGPU。 不需要,而且建议不要。卷一是纯 CPU 内核,从内存分配器一路讲到 Job System,一行渲染代码都没有。
把 WebGPU 推迟到卷二是有意的:你先弄懂内存、句柄、ECS 这些与图形 API 无关的东西,等卷二讲管线与命令缓冲时再学 GPU,两段知识不会互相打岔。提前啃 WebGPU,只会在你还不需要它的地方消耗精力。
游戏开发经验。 完全不是前置条件。有经验的人会更快把概念对应到实际场景;没经验的人反而少一些既有习惯的干扰——数据导向的写法本就和「面向对象地组织游戏逻辑」的直觉相反,先读这本书再上手引擎,未必是坏事。
这些问题有一个共同点:它们关心的都是「值不值得投入」。我的回答是——如果你想要的是一份能长期迁移的底层理解,而不是一个速成的框架用法,那这条路值得走。
怎么验证你搭的环境是对的
环境准备看着只是装个 Node,真正卡人的却往往是「版本对了、命令却跑不通」。下面几步从零到一,每步都告诉你期望看到什么和出错先查哪里。全部通过再开始读第 1 章。
第一、二步,确认工具版本。
node --version # 期望 v26.x 或更高(本卷实测 v26.5.0)
npx tsc --version # 期望 Version 7.x(本卷实测 7.0.2)
npx vitest --version # 期望 5.x(本卷实测 5.0.3)
三行命令一次跑完。node 低于 26 会让后面每一步都失败,用 nvm install 26 && nvm use 26 升级;tsc 或 vitest 提示找不到包,多半是没在项目目录里执行,或没跑过 npm i -D typescript vitest。
第三步,跑一个最小冒烟测试。 在项目根目录建一个 smoke.ts:
// file: smoke.ts
// tier: L1
// verify: node smoke.ts
const buf = new Uint8Array(16);
buf[0] = 42;
console.log('ok', buf.length, buf[0]);
然后直接跑它:
node smoke.ts
期望输出 ok 16 42,且不报任何错。若报 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,说明你写了需要生成运行时代码的语法(enum、构造函数参数属性等),换成前面「一个必须遵守的约束:erasable syntax」里给的替代写法即可。
第四步,确认 tsconfig.json 没漏关键项。
npx tsc --noEmit
期望没有任何输出(退出码 0)。报 erasable 相关错误,检查是否漏了 "erasableSyntaxOnly": true;报模块解析错误,检查是否漏了 "module": "NodeNext" 与 "moduleResolution": "NodeNext"。
四步全过,你的环境就和本卷正文一致了。任何一步卡住,先翻附录 B 环境与复现速查 与附录 D 常见错误与 FAQ ,那里的排查顺序和这里一致。
阅读约定
- 正文中的代码都是可运行的,标 L1 的可以直接跑,标 L3/L4 的会说明为什么不能。
- 每章第 1 节有「本章与站内既有内容的分工」小节,说明这一章与站内已有文章(如渲染原理、引擎选型、Defold 子系统)的区别,避免重复阅读。
- 跨专题链接用绝对路径(如
/posts/graphics/),书内链接用relref,点击即可跳转。 - 每篇正文末行有「上一节 / 下一节」导航,也可以随时回到目录 。
怎么配合目录篇与附录阅读
本卷除了 18 章正文,还有一篇目录 和四篇附录。它们不是「可读可不读」的附件,而是正文的两个配套工具。
目录篇回答「从哪开始、按什么顺序读」:它给出三条阅读路径(顺序通读 / 直奔 ECS 与内存 / 按问题查)、一份八周计划,以及一张把「你想解决的问题」映射到「去看哪一节」的常见问题速查表。动手之前先扫一遍目录篇,你会知道自己的处境对应哪条路径。
四篇附录回答「卡住了怎么办」,它们按查阅顺序而非教学顺序组织:
| 附录 | 回答什么问题 | 什么时候翻 |
|---|---|---|
| 附录 A 内核 API 与模块速查 | 某个模块、类型或 API 的签名是什么 | 忘了某个函数的参数,或想对照全书的模块划分 |
| 附录 B 环境与复现速查 | 怎么把某层的代码跑通 | 环境版本对不上、verify 命令跑不起来 |
| 附录 C 实测数据汇总 | 某个基准的原始数字是多少 | 想核对正文里的倍数,或在自己的机器上复现 |
| 附录 D 常见错误与 FAQ | 这个报错是什么意思、怎么修 | 报错信息看不懂,或行为与预期不符 |
一个实用建议:把目录篇的「常见问题速查」和附录 D 当作两条索引——前者按「现象」索引(我想解决什么问题),后者按「报错原文」索引(我看到了什么错误)。两者交叉使用,绝大多数「卡住」的时刻都能快速定位。
准备好了就从第 1 章 1.1 引擎的分层与模块边界 开始。
阅读上一节:序
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。