《游戏引擎实现原理》14.1 为什么 JS 也需要 Job System
前 13 章的所有更新都挤在一个核上跑。本节先讲清楚为什么 JS 也需要一套 Job System:Node 的 worker_threads 与浏览器 Worker 差在哪、SharedArrayBuffer 与 Atomics 在 Node 里怎么用、在浏览器里为什么必须加 COOP/COEP 跨源隔离头,并用一段 CPU 密集任务实测主线程串行与 4 worker 并行的差距。
tag
前 13 章的所有更新都挤在一个核上跑。本节先讲清楚为什么 JS 也需要一套 Job System:Node 的 worker_threads 与浏览器 Worker 差在哪、SharedArrayBuffer 与 Atomics 在 Node 里怎么用、在浏览器里为什么必须加 COOP/COEP 跨源隔离头,并用一段 CPU 密集任务实测主线程串行与 4 worker 并行的差距。
每个系统声明读哪些、写哪些数据槽,两个系统存在写-读或写-写重叠即冲突。用最长路径分层把系统排进互不冲突的组:组内可并行、组间同步。本节给出 helio 内核可编译的调度器,跑出《微光岛》十个系统的真实分组结果,并对照粗粒度与细粒度数据槽对并行度的影响。
改动一个节点后只把它的子树标脏,updateDirty 只重算被标脏的节点,updateAll 则无条件全量重算。本节给出 helio 内核可编译的 TransformHierarchy(扁平 TypedArray + 拓扑序),并实测 4 叉树上改叶子时「全量重算 vs 脏标记」的耗时差与被重算的节点数。
实体用位掩码表示拥有哪些组件,查询用 all / none 两个掩码描述条件;结果按查询键缓存,任一组件增删都把全局 revision 加一使缓存整体失效。本节给出 helio 内核可编译的 QueryEngine,并实测有缓存与无缓存跑同一批查询的耗时差、命中与失效次数。
按领域与学习阶段整理的在线电子书与创作手记。覆盖 Rust、Lua、TypeScript、Python、Java(Spring Boot)、Go、AI Agent、MMO 服务器开发(Go 主线 + Java 21 双栈对照,四卷本 180 篇)、游戏开发(含 Cocos 四卷本与《游戏引擎实现原理》四卷本)、SLG 游戏产品设计(产品架构与经济系统 + 数值平衡与商业化运营,两卷本 100 篇)、小游戏开发实战(市场品类与产品设计 + 引擎实现与体验打磨 + 上线变现与长线运营 + 商业运营实战 + 跨平台发行与出海,五卷本 250 篇)、游戏美术与技术美术(美术制作与视觉规范 + 技术美术与管线工程 + 风格与品类美术实战,三卷本 150 篇)、游戏设计(玩法机制与可执行规格 + 策略、涌现与体验度量 + 原型、可玩性与品类,三卷本 150 篇)、计算机图形学(几何、变换与光栅化 + 着色、光照与纹理 + 光线追踪与全局光照,三卷本 150 篇)、叙事策划与叙事工艺(叙事结构与可执行规格 + 叙事引擎与分支系统 + 叙事体验与路径度量,三卷本 150 篇)与文学创作,从阅读指南进入,或按全书目录查找章节。
物理与渲染是两条频率、精度、目标都不同的时间线,把它们正确缝合是游戏引擎的核心难题。本文讲解刚体动力学与碰撞检测的两阶段流程、固定步长累加器与渲染插值/外推、角色控制器的扫掠与滑动处理、布料与绳索的 PBD/XPBD 约束求解、确定性与网络回滚同步,以及物理与渲染解耦的 ECS 架构设计,给出避免抖动、穿透与回滚错位的完整工程方案。
实现 helio 的曲线模块:二次/三次贝塞尔、Catmull-Rom 样条与常用缓动函数。用采样点表说清贝塞尔不穿过中间控制点、Catmull-Rom 穿过却会过冲(实测会冲到 -0.0625),并给出四种缓动函数在同一组 t 上的取值对照。
实现 helio 的四元数模块:乘法、共轭、归一化、向量旋转与 slerp。重点把插值里那个反直觉的分支跑出来——当两个四元数的点积为负时若不取反,t=0.5 会转 175° 的远弧;取反后只转 5°,两条轨迹相差整整 180°。两段轨迹的角度扫描一并给出真实数字。
描述一个旋转最直觉的方式是三个角度——欧拉角。本节用可编译的 TypeScript 把它的失效点跑出来:pitch 转到 90° 时,两组完全不同的欧拉角给出逐元素相同的旋转矩阵,误差只有 6.12e-17。我们把死锁现场、丢掉的自由度与四元数的引入动机一次讲清。
把平移、旋转、缩放按固定顺序复合,就是每个物体的世界变换。本节给出 helio 的 TRS 复合与分量逆变换实现,用 vitest 断言 M 乘 M⁻¹ 约等于单位阵并写出实测误差,并说明非均匀缩放下法线为什么要用 R 乘 S⁻¹ 来变换。
同一组 16 个数字,按行主序读和按列主序读会得到两个不同的变换。本节讲清主序、手性与转置陷阱,说明 WebGPU / WGSL 的列主序约定与 CPU 到 GPU 的统一口径做法,给出 helio 可编译的列主序 Mat4,并用实测数字展示同一矩阵两种解释下的结果差异。
向量在引擎里怎么存,决定了批量运算能跑多快。本节把三维向量放进 Float32Array,对照 AoS 交错布局与 SoA 分离布局的字节分布,给出零分配的 addInto 约定,以及可编译的 Vec3Store 与 Vec3SoA 实现和实测求和结果。
把引擎里到处流动的名字压成整数:字符串驻留表让同名字符串只存一份、比较变成整数比较,资源 GUID 让文件路径确定性地映射成 64 位标识,名字表把组件类型、属性、系统登记成句柄。给出按字符串查 Map 与按句柄索引数组的 6.6 倍实测差距。
实现 helio 的三个零分配容器:用 Uint32Array 把 32 个布尔压进一个字的位集合、固定容量首尾相接的环形缓冲,以及把 next/prev 长在对象自己身上的侵入式链表。给出位集合与布尔数组的堆增量、遍历耗时实测,以及环形缓冲 1000 万次 push 的吞吐。
从零实现 helio 的两个基础容器:增长因子可配的动态数组,以及用 FNV-1a 加线性探测、Robin Hood 抢占与墓碑删除的开放寻址哈希表。给出 1.25×/1.5×/2× 三档因子的扩容与搬运实测,以及探测步数分布和墓碑退化数据,全部本机跑出。
把实体的创建、销毁、句柄复用收进一个实体注册表,复用第 6 章的世代句柄:删除实体后旧句柄立即失效,新实体沿用同一槽位却拿到新世代。给出可编译实现、断言用例、复用与不复用的实测对比,以及世代回绕撞上 NULL_HANDLE 这个真实边界与应对办法。
把「每种组件一个稀疏集」和「按组件集合分组的 archetype」两种组件存储都从零实现出来,用同一组操作对比:建 20 万个实体的耗时与堆净增量、以及查询并原地更新的耗时,把本机实测的真实数字写进正文,并给出各自的取舍边界与可操作的选择判据。
把「让物体动起来」用继承、组合、数据三种方式各写一遍,对照它们各自的代价;再在 6.2 的世代句柄之上从零搭出一套最小 ECS,用可编译的 MiniEcs 落地实体、组件与查询三件套,并用 vitest 断言「销毁实体后旧句柄立即失效」这一关键行为。
把 for (const e of entities) e.update(dt) 这样一段面向对象的更新循环,一步步改造成按组件列分组的批处理。本节给出改造前后两份可编译的 helio 源码与一份基准,在本机实测出耗时约 1.5 倍、堆增量 16.6 MB 对 0 MB 的差距,并说明哪些场景根本不该改。
同样 1M 个元素,AoS(对象数组)与 SoA(平行 TypedArray)在本机实测出 22.94 ms 对 14.47 ms、约 1.59 倍的差距,但堆增量是 62.6 MB 对 0 MB——TypedArray 的底层 ArrayBuffer 属外部内存,不进 JS 堆。本节讲清三种布局的真实差异,并给出「什么时候不该用数据导向」的判据。