《游戏引擎实现原理》附录 B 环境与复现速查

《游戏引擎实现原理》卷一附录 B:跟读本卷所需的环境准备与代码复现方法。列出实测的 Node / TypeScript / vitest 版本,给出从零搭建 playground 的完整步骤、推荐的 tsconfig 与 package.json 并逐个解释编译选项、各层代码(L1 运行、L2 无头浏览器、L3 类型检查、B 基准)的跑通命令,以及常见环境问题的排查清单与目录结构约定。

实测环境

本卷全部代码在下列环境中实测通过:

组件版本
Node.js26.5.0
npm11.17.0
TypeScript7.0.2
vitest5.0.3
操作系统macOS(Darwin 25.3.0,Apple Silicon)

Node 26 是本卷的硬性要求,因为它原生支持直接运行 .ts 文件。更低版本需要额外的加载器(tsx / ts-node),与本卷的「零构建步骤」口径不符。

为什么是 Node 26

Node 26 的 type-stripping 允许 node src/core/arena.ts 直接跑,靠的是只做类型剥离、不做语法转换:它把类型标注、interface、type 这些「可擦除」的东西删掉,剩下的就是普通 JS。代价是有些语法不能出现——凡是编译后需要生成运行时代码的,都不可擦除:

  • enum(会生成一个运行时对象)
  • namespace(会生成一个运行时对象)
  • 构造函数参数属性(constructor(private x: number),会生成赋值语句)

这三样在 Node 26 下会抛 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX。本卷的替代写法是 const 对象 + as const 与显式字段声明,详见附录 D 。

从零搭建 playground

mkdir helio-playground && cd helio-playground
npm init -y
npm i -D typescript vitest

package.json

npm init -y 生成的 package.json 默认是 CommonJS,需要改成 ESM,否则 verbatimModuleSyntax 会报 TS1295:

{
  "name": "helio-playground",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^7.0.2",
    "vitest": "^5.0.3"
  }
}

关键的一行是 "type": "module"。 漏了它,tsc 会把 .ts 当成 CommonJS 文件,报出一堆 TS1295 和 TS1287。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2023",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"]
}

tsconfig 选项逐个解释

上面那份配置不是抄来的模板,每个选项都有理由。逐条说明:

选项取值作用
targetES2023语法下限。Node 26 支持到 ES2023+,没必要往下压
lib["ES2023","DOM","DOM.Iterable"]可用的全局类型。带上 DOM 是为了卷二用 WebGPU 时无缝衔接
moduleNodeNext按 Node 的 ESM 规则解析模块,与 "type": "module" 配套
moduleResolutionNodeNext与 module 成对出现,缺一个解析就会错
stricttrue打开全部严格检查(strictNullChecks 等),内核代码必须严
noEmittrue只做类型检查,不产出文件——Node 自己跑 .ts,不需要产物
erasableSyntaxOnlytrue禁止 enum / namespace / 参数属性,保证 Node 26 能直接跑
verbatimModuleSyntaxtrue要求 import type 显式标注,避免类型导入被当值导入
allowImportingTsExtensionstrue允许 import './arena.ts' 这种带扩展名的写法(Node 直跑必需)
isolatedModulestrue保证每个文件都能独立转译,与 type-stripping 的前提一致
skipLibChecktrue跳过 .d.ts 的内部一致性检查,显著加快 tsc
forceConsistentCasingInFileNamestrue大小写不一致直接报错,避免 macOS 大小写不敏感带来的假通过

四个「必开」的是 erasableSyntaxOnly、verbatimModuleSyntax、allowImportingTsExtensions、noEmit——它们直接对应「Node 26 直跑 .ts」这条工作方式。其余是为了工程严谨与编译速度。

版本核对

装完先核对版本,避免「版本对不上却怪代码」:

node --version   # 应输出 v26.x 或更高
npx tsc --version
npx vitest --version

如果 node --version 低于 26,后面的 node src/xxx.ts 会报 ERR_UNKNOWN_FILE_EXTENSION。

各层代码怎么跑

本卷每节都标了可运行性层级(L1–L4 / B),跑法各不相同。

L1:纯 TypeScript 内核

Node 26 不需要任何 flag 就能跑 .ts:

node src/core/arena.ts

如果文件里有单元测试,用 vitest:

npx vitest run src/core/arena.test.ts

一次跑全部单测:

npx vitest run

L2:需要浏览器渲染(本卷用不到,卷二要用)

L2 的代码要真实浏览器环境才能跑。本卷是内核,没有任何 L2 内容;但卷二《渲染引擎》会大量使用,所以这里把准备步骤提前交代,免得读到卷二再回头折腾。

准备 headless Chromium 的两种常见方式:方式一用 Playwright(推荐,自带浏览器管理),方式二用 Puppeteer(会下载一份自带 Chromium)。

npm i -D playwright && npx playwright install chromium
npm i -D puppeteer

装好后用一段最小脚本验证浏览器能起来、WebGPU 能拿到适配器:

// snippet: 最小 L2 冒烟测试(卷二用,本卷不需要)
import { chromium } from 'playwright';
const browser = await chromium.launch({
  args: ['--enable-unsafe-webgpu', '--use-angle=metal'], // 开启 WebGPU
});
const page = await browser.newPage();
const hasGpu = await page.evaluate(() => 'gpu' in navigator);
console.log('WebGPU available:', hasGpu);
await browser.close();

两点提醒:headless Chromium 默认不开 WebGPU,要显式加 --enable-unsafe-webgpu 等 flag;macOS 上走 Metal 后端,--use-angle=metal 更稳。这些细节本卷用不上,记在心里即可。

L3:只能做类型检查

npx tsc --noEmit

本卷的 L3 内容极少(主要是配置文件与类型垫片),大部分代码都是 L1。

B:性能基准

基准脚本产出 JSON,不参与断言:

node bench/soa-vs-aos.mjs
node --expose-gc bench/soa-vs-aos.mjs

基准跑得比单测慢,且对机器负载敏感,所以基准门禁是软告警——数字波动不会让流程失败,但会提示你复核。方法论与原始数字见附录 C 。

目录结构约定

本卷代码按模块组织,你可以照此搭建:

helio/
  src/
    core/       内存、句柄、容器、数学(第 2、4–10 章)
    ecs/        实体、组件、系统(第 11–13 章)
    jobs/       任务系统(第 14–15 章)
    reflect/    反射与序列化(第 16–17 章)
    platform/   平台抽象层(第 3 章)
  bench/        性能基准(B 层)

各目录里你会遇到的典型文件:

路径内容章节
src/core/arena.ts线性 arena 与 scratch arena4.2
src/core/object-pool.ts对象池与 freelist5.1
src/core/slotmap.ts世代句柄与 slot map6.2
src/core/sparse-set.ts稀疏集6.3
src/core/frame-timer.ts帧计时器与滑窗统计2.1
src/core/fixed-step-loop.ts固定步长主循环2.2
src/core/subsystem.ts子系统注册与生命周期编排3.1
src/core/platform.tsIPlatform 与平台无关辅助3.2
src/ecs/world.tsECS 世界12.3
src/jobs/job-system.ts任务系统入口14.1
src/reflect/type-registry.ts类型注册表16.2
bench/soa-vs-aos.mjsSoA / AoS 基准11.2

每个代码块顶部的 // file: 注释就是它在真实工程里的路径,照抄即可。

常见环境问题

跟读时最容易卡住的地方都在这张表里。按「现象 → 原因 → 解决」查:

现象原因解决
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX用了 enum / namespace / 参数属性改用 const X = {...} as const 与显式字段
TS1295 / TS1287package.json 缺 "type": "module"加上它
TS1294: not allowed when 'erasableSyntaxOnly' is enabled写了不可擦除的语法改写法,别关这个选项
TS5097: An import path can only end with a '.ts' extension ...缺 allowImportingTsExtensions打开该选项
Cannot find module './arena.ts'同上,或文件名大小写不符打开选项并核对文件名
ERR_MODULE_NOT_FOUNDESM 下 import 路径漏了 .ts 扩展名补上 .ts
Cannot find name 'process' / readFileSync缺 @types/nodenpm i -D @types/node 或在 types 里加
Cannot find name 'GPUBufferUsage'缺 @webgpu/types(卷二才需要)npm i -D @webgpu/types 并在 types 里加
node: command not foundNode 未安装或版本过低装 Node ≥ 26
ERR_UNKNOWN_FILE_EXTENSION: Unknown file extension ".ts"Node 版本低于 26,不认识 .ts升级到 Node ≥ 26
SyntaxError: Cannot use import statement outside a module同 TS1295,运行时表现加 "type": "module"
__dirname is not defined in ES module scopeESM 下没有 __dirname用 import.meta.url 换算
vitest 找不到测试文件文件名不以 .test.ts 结尾改名为 *.test.ts
vitest 无输出卡住watch 模式在等交互用 vitest run 而非 vitest
Playwright 浏览器下载失败代理/网络设 HTTPS_PROXY,或改用系统已装的 Chromium
基准数字与书里差很多JIT / GC / 负载看方向不看倍数,见附录 C

每章结束时的复现清单

跟着本卷做时,建议每章结束时确认下面几项。它们是「这一章我真的跑通了」的最小证据:

  • npx tsc --noEmit 通过,零类型错误
  • npx vitest run 全绿,没有 skipped 的用例
  • 本章的 L1 代码能在 node 下直接跑,不需要任何 flag
  • 新增文件都在正确的模块目录下(core/ / ecs/ / jobs/ / reflect/ / platform/)
  • 如果本章有基准,跑一次 node bench/*.mjs,数字与附录 C 的方向一致(不必完全相等)
  • 本章用到的新类型/API,在附录 A 里能找到对应条目

复现的常见误区

最后提醒三个跟读时反复出现的误区,避免把时间花在错的地方。

误区一:急着装一堆工具。 本卷不需要 webpack、vite、ts-node、esbuild。Node 26 直跑 .ts,vitest 跑单测,仅此而已。多装的构建工具只会引入新的报错源。

误区二:把 noEmit 关掉想「编译出 JS」。 不需要。Node 直接吃 .ts,编译产物反而会让「我跑的是哪一份」变得含糊。本卷全程 noEmit。

误区三:把基准数字当验收标准。 基准是软告警。你的机器跑出来 1.4× 而书里写 1.59×,不是你没跑对,是微基准本就不可移植。真正要核对的是方向和结构,不是小数位。

环境没问题了,从第 1 章 1.1 引擎的分层与模块边界 开始。

阅读上一节:附录 A helio 内核 API 与模块速查 · 阅读下一节:附录 C 实测数据汇总

继续阅读

探索更多技术文章

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

全部文章 返回首页

「engine」更多文章

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