《游戏引擎实现原理》附录 A helio 内核 API 与模块速查

《游戏引擎实现原理》卷一附录 A:把全书出现的 helio 内核类型与关键 API 按 core / ecs / jobs / reflect / platform 五个模块整理成速查表,每个条目给出签名、一句话说明与所在章节,并附模块依赖方向、典型组合用法与按章反查索引,方便边读边查、随时回跳。

这份速查怎么用

本卷 18 章会一路往 helio 内核里加零件。零件多了以后,最常见的困惑不是「这个算法怎么实现」,而是「这个类型叫什么、在哪一章、签名长什么样」。这份附录就是为这种时刻准备的:把全书出现过的类型与关键 API 按模块铺开,每一行给签名、一句话说明、出自哪一章三样东西,读到哪一节想不起来,回头扫一眼即可。

用法上,它是查阅表不是教程:不要从第一行读到最后一行,而是带着问题跳——想找句柄就翻到「句柄与容器」,想找任务系统就翻到 src/jobs/。真正讲原理的内容在正文各节,本表只做索引与备忘。

模块与章节的对应关系如下:

模块负责什么主要章节
src/core/内存、句柄、容器、数学、主循环第 2、4–10 章
src/ecs/实体、组件存储、查询、系统调度第 11–13 章
src/jobs/工作者池、任务图、依赖 DAG第 14–15 章
src/reflect/类型注册表、属性描述符、序列化第 16–17 章
src/platform/平台抽象层、日志、CVar、子系统编排第 3 章

表中的「出处」列写的是章节号(如 6.2 指 6.2 节),而不是文件路径——因为同一份 API 可能被多节复用,章节号才是它「第一次被讲清楚」的地方。

src/core/ 内存、容器与数学

内核最底层的一层,也是全书代码量最大的一层。第 4 章讲分配器,第 5 章讲池化,第 6 章讲句柄,第 7 章讲自研容器,第 8–10 章讲数学与数值。

内存分配

名称签名 / 成员说明出处
Arenanew Arena(capacityBytes) · alloc(bytes, align?) → offset · reset() · highWaterMark · usedBytes只前进不回收的线性分配器;帧末整体 reset 一次释放4.2
ScratchArenamark() → mark · release(mark?) · alloc(bytes, align?)帧内临时内存的栈式分配;mark / release 必须成对4.2
ObjectPool<T>new ObjectPool<T>({ create, reset, initial, policy? }) · acquire() → T · release(item) · stats → PoolStats定长对象池 + 空闲链表;stats.created 是收益指标5.1
ObjectPoolOptions<T>{ create: () => T; reset: (item: T) => void; initial: number; policy?: 'throw' 或 'grow' }池的构造参数;create / reset 由调用方注入5.1
PoolStats{ capacity, free, inUse, created, recycled, exhausted }池的运行统计5.1
BlockAllocatoralloc(bytes) → ptr · free(ptr) · sizeClassOf(bytes)按 2 的幂分大小类,在固定 buffer 上切块5.2
MemoryTagtag(name) · bytesOf(name) · snapshot()按标签统计占用与分配计数,用于泄漏检测5.3
alignUpalignUp(n, align) → number向上对齐到 align 的整数倍4.3

句柄与容器

名称签名 / 成员说明出处
Handletype Handle = number(低 20 位索引 + 高 12 位世代)可序列化的引用;失效可在访问瞬间检测6.2
makeHandlemakeHandle(index, generation) → Handle把索引与世代拼成 32 位句柄6.2
handleIndexhandleIndex(h) → number掩出低 20 位的槽位索引6.2
handleGenerationhandleGeneration(h) → number右移掩出高 12 位的世代6.2
NULL_HANDLENULL_HANDLE = 0保留空句柄,永远取不到值6.2
SlotMap<T>insert(v) → Handle · get(h) → T 或 undefined · has(h) · expect(h) → T · remove(h) · entries()世代句柄容器;remove 时该槽位世代自增6.2
SparseSet<T>add(id, v) · get(id) · has(id) · remove(id) · values() · ids() · denseView()稀疏 ID → dense 下标;swap-remove 保持紧凑6.3
DynamicArray<T>push(v) · pop() · get(i) · set(i, v) · reserve(n) · length自增长数组;容量按 2 倍增长7.1
OpenHashMap<K, V>set(k, v) · get(k) · has(k) · delete(k) · loadFactor开放寻址哈希;线性探测与墓碑删除7.1
BitSetset(i) · clear(i) · test(i) · popcount()位集合,用位运算做成员测试7.2
RingBuffer<T>push(v) · pop() · peek() · full定长、零额外分配的环形缓冲7.2
IntrusiveListpushFront(node) · remove(node) · forEach(fn)侵入式链表,节点自带前驱后继指针7.2
StringInternintern(s) → NameId · resolve(id) → string字符串驻留,把名字句柄化7.3
Guidparse(str) → Guid · toString()资源 GUID,稳定的资源标识7.3

时间与主循环

名称签名 / 成员说明出处
ClocknowMs() → number单调毫秒时钟抽象;所有时间都从这里取2.1
FakeClocknew FakeClock(startMs?) · advance(deltaMs)假时钟,用于测试与确定性回放2.1
FrameTimerbegin() · end() → ms · avgMs() · percentile(p) · maxMs() · fps() · stats() · histogram(bucketMs)环形缓冲记录最近 N 帧耗时与长尾2.1
FramePhases{ logic, submit, gpu, present }一帧四个阶段的耗时账本2.1
frameTotalMsframeTotalMs(phases) → number四段求和2.1
withinBudgetwithinBudget(phases, budgetMs = 1000/60) → boolean是否落在帧预算内2.1
FixedStepLoopnew FixedStepLoop({ stepSeconds, maxStepsPerFrame }) · advance(dt, onStep?) → StepOutcome · reset()固定步长累加器;maxStepsPerFrame 是死亡螺旋的刹车2.2
StepOutcome{ steps, alpha, droppedSeconds, accumulatorSeconds }一次 advance 的结果;alpha 恒在 [0, 1)2.2
interpolateinterpolate(prev, curr, alpha) → number在上一物理态与当前物理态之间补间2.2
TimeScalesetScale(s) · scaled · unscaled时间缩放与未缩放两条时间轴2.3

数学与数值

名称签名 / 成员说明出处
Vec3add · sub · scale · dot · cross · length · normalize三维向量;遵循零分配约定(写进 out 参数)8.1
Mat4identity() · multiply(a, b) · transpose(m) · invert(m) · transformPoint(m, v)4×4 矩阵;列主序8.2
Transformcompose(t, r, s) → Mat4 · invert(m) → Mat4平移 / 旋转 / 缩放的复合顺序与逆变换8.3
EulertoQuat(e) → Quat · fromQuat(q) → Euler欧拉角与四元数互转9.1
QuatfromAxisAngle · mul · conjugate · normalize · slerp四元数运算与最短弧插值9.1 / 9.2
bezierbezier(p0, p1, p2, p3, t) → number三次贝塞尔曲线9.3
catmullRomcatmullRom(p0, p1, p2, p3, t) → numberCatmull-Rom 样条9.3
easeInOuteaseInOut(t) → number常用缓动函数9.3
FixedFixed.fromFloat(x) · .toFloat() · .mul(o) · .div(o) · .add(o)Q16.16 定点数,为确定性模拟准备10.2
approxEqualapproxEqual(a, b, eps) → boolean带阈值的浮点比较10.3
saturatesaturate(x) → number把值夹到 [0, 1]10.3
safeDividesafeDivide(a, b, fallback) → number除零防护10.3

src/ecs/ 实体、组件与系统

这一层把 src/core/ 的句柄与稀疏集组装成 ECS。实体是句柄,组件存进稀疏集,系统声明读写依赖后被调度器并行分组。

名称签名 / 成员说明出处
WorldcreateEntity() → Entity · destroyEntity(e) · add<T>(e, c) · get<T>(e) · has<T>(e) · remove<T>(e)ECS 世界;实体与组件的唯一入口12.3
Entitytype Entity = Handle实体就是一个世代句柄12.3
ComponentStorage<T>add(e, c) · get(e) · remove(e) · view()单个组件类型的存储,底层是 SparseSet<T>12.2
ArchetypeStoragearchetypeOf(components) → Archetype · chunks()按组件集合分桶的存储方案12.2
Querynew Query(world, [A, B]) · forEach(fn) · matching() → Entity[]组件集合求交;结果带缓存13.1
QueryCacheinvalidate(tag) · hit · miss查询结果缓存与失效13.1
DirtyFlagmark(e) · isDirty(e) · clear(e)变更检测与脏标记传播13.2
SystemScheduleraddSystem({ reads, writes, run }) · groups() · tick(dt)声明读写依赖,推导并行分组13.3

src/jobs/ 任务系统

第 14 章把计算搬出主线程,第 15 章把任务排成有依赖的图。整层都建立在 worker_threads(Node)或 Web Worker(浏览器)之上,通过 IWorkerPool 抽象掉差异。

名称签名 / 成员说明出处
JobSystemnew JobSystem(workers) · submit(job) → Promise · waitAll()任务系统入口14.1
IWorkerPoolspawn(task) · size · terminate()工作者池接口;Node / Web 双实现14.2
NodeWorkerPoolnew NodeWorkerPool(n)基于 worker_threads 的后端14.2
WebWorkerPoolnew WebWorkerPool(n)基于 Worker + SharedArrayBuffer 的后端14.2
TaskQueuepush(t) · pop() · steal()任务队列与 Atomics 同步14.2
WorkStealingQueuepush(t) · pop() · steal()每个工作者一个双端队列的窃取策略14.3
DependencyDagaddNode(id) · addEdge(from, to) · topoOrder()依赖 DAG 的构建与拓扑执行15.1
TaskGraphaddTask({ deps, run }) · build() · run()每帧构建任务图并调度到工作线程15.2

src/reflect/ 反射与序列化

第 16 章用显式注册表(不用装饰器元数据)描述类型,第 17 章让序列化由这些类型信息驱动。生成器脚本在构建期扫描源码,把注册表写成一个普通 .ts 文件。

名称签名 / 成员说明出处
TypeRegistryregister(type) · get(name) → TypeDescriptor · create(name) → unknown类型注册表16.2
PropertyDescriptor{ name, type, offset, flags }属性描述符;编辑器与序列化的元数据16.2
codegenScanscan(srcDir) → registry.ts构建期扫描源码,生成类型注册表16.3
Serializerserialize(obj) → bytes · deserialize(bytes) → obj反射驱动的序列化接口17.1
JsonSerializernew JsonSerializer(registry)文本格式,便于调试17.1
BinarySerializernew BinarySerializer(registry)二进制格式,紧凑、快17.1
migratemigrate(save, fromVersion, toVersion) → saveschema 版本迁移17.3

src/platform/ 平台抽象与基础设施

内核其余部分只依赖这里的接口,不直接 import node:fs 或 window。子系统编排、日志、配置也都收敛在这一层,作为启动期的公共设施注入。

名称签名 / 成员说明出处
IPlatform{ name, time, file, thread, log }内核与宿主环境的唯一契约面3.2
PlatformTimenowSeconds() · nowMillis()单调时钟,不受系统时间改动影响3.2
PlatformFilereadText(path) · writeText(path, data) · exists(path)同步文本读写,够启动期加载配置3.2
PlatformThreadhardwareConcurrency · spawn(task)并发度与跨线程执行3.2
PlatformLogwrite(level, category, message)日志输出面3.2
NodePlatformNodePlatform: IPlatformnode:fs / perf_hooks / os 实现3.2
WebPlatformWebPlatform: IPlatform浏览器实现(fetch / Worker,L4 需手动验证)3.2
measureMillismeasureMillis(p, fn) → number平台无关的计时辅助,只吃 IPlatform3.2
ISubsystem{ name, dependencies, onInit, onStart, onStop, onShutdown }子系统四钩子接口3.1
SubsystemRegistryregister(sub) · resolveOrder() · boot(ctx) · shutdown() · startOrder拓扑排序启动、逆序关闭、成环报错3.1
Loggerdebug/info/warn/error(category, msg) · setLevel(level)五级日志与分类开关3.3
LogLevelconst LogLevel = { Debug: 0, Info: 1, Warn: 2, Error: 3 } as const日志级别(用 const 对象,不用 enum)3.3
CVarSystemdefine(spec) · get(name) · set(name, value) · onChange(cb)类型化配置项 CVar3.3
assertassert(cond, msg) · assertNever(x)断言语义与穷尽性检查3.3

模块依赖方向

helio 只有一条铁律:依赖只能向下。任何「上层被下层 import」都是错误,1.1 节的 findUpwardDeps() 会在构建期把它抓出来。

platform   ← 宿主边界:IPlatform / Logger / CVar / SubsystemRegistry
    ↑
  core     ← 内存、句柄、容器、数学、主循环(不依赖任何上层)
    ↑
   ecs     ← 依赖 core 的句柄与稀疏集
    ↑
  jobs     ← 依赖 core 的容器;ecs 借 jobs 并行跑系统
    ↑
 reflect   ← 依赖 core 与 ecs 暴露的类型信息

分层与依赖方向的完整讨论在1.1 引擎的分层与模块边界 ;这里只需记住:core 永远不知道 ecs 的存在,platform 永远不知道上面有谁。

典型组合用法

查表不如看一次真实的串联。下面三段是全书最常见的组合,展示这些 API 怎么拼在一起。

创建世界、生成实体、挂组件、查询——ECS 的日常:

// snippet: ECS 的典型四步
const world = new World();
const e = world.createEntity();
world.add(e, { x: 0, y: 0, vx: 1, vy: 0 }); // Transform 组件
new Query(world, [Transform, Velocity]).forEach((entity, t, v) => {
  t.x += v.x * dt;
  t.y += v.y * dt;
});

用 arena 兜住一帧内的临时分配,帧末整体回收:

// snippet: 帧内临时内存走 ScratchArena
const scratch = new ScratchArena(64 * 1024);
const mark = scratch.mark();
const tmp = scratch.alloc(1024, 16); // 帧内临时数据
// ... 本帧用完即弃 ...
scratch.release(mark); // 偏移回到标记点,下一帧继续用

把一批互相独立的任务交给 JobSystem,最后等齐:

// snippet: 提交任务并等待
const jobs = new JobSystem(4);
const handles = entities.map((e) => jobs.submit(() => updateEntity(e, dt)));
await Promise.all(handles);
jobs.waitAll();

命名约定速记

  • 句柄一律是 32 位 number,用 NULL_HANDLE(0)表示「空」,绝不用 -1 或 undefined 混着来。
  • 分配器一律「只前进不回收」;回收的语义是「标记为空闲、允许复用」(reset / free / swap-remove),不是把内存还给系统。
  • 时间一律从 Clock 或 IPlatform.time 取,正文代码里不出现直接的 performance.now()。
  • 类型用 interface / type;级别枚举用 const 对象 + as const,因为 Node 26 的 erasable syntax 禁止 enum。
  • 容器把「值」和「身份」分开:SlotMap 管身份(带世代),SparseSet 管存储(dense 紧凑)。

按章反查

只想按章节找对应零件时,看这张表:

章节关键类型 / API
2.1Clock FakeClock FrameTimer FramePhases
2.2FixedStepLoop StepOutcome interpolate
2.3TimeScale
3.1ISubsystem SubsystemRegistry
3.2IPlatform NodePlatform measureMillis
3.3Logger LogLevel CVarSystem assert
4.2Arena ScratchArena
4.3alignUp
5.1ObjectPool PoolStats
5.2BlockAllocator
5.3MemoryTag
6.2Handle makeHandle SlotMap NULL_HANDLE
6.3SparseSet
7.1DynamicArray OpenHashMap
7.2BitSet RingBuffer IntrusiveList
7.3StringIntern Guid
8.xVec3 Mat4 Transform
9.xEuler Quat bezier catmullRom
10.xFixed approxEqual saturate safeDivide
12.xWorld Entity ComponentStorage ArchetypeStorage
13.xQuery DirtyFlag SystemScheduler
14.xJobSystem IWorkerPool TaskQueue WorkStealingQueue
15.xDependencyDag TaskGraph
16.xTypeRegistry PropertyDescriptor codegenScan
17.xSerializer JsonSerializer BinarySerializer migrate

一处提醒:本表列的是接口与签名,不是实现。同一个名字在正文里可能有更细的字段与更严的边界检查(例如 SlotMap.remove 会先判 has、ObjectPool.release 会拦截重复归还),以正文的可编译代码为准。

阅读下一节:附录 B 环境与复现速查

继续阅读

探索更多技术文章

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

全部文章 返回首页

「engine」更多文章

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