Files
q1k3/knowledge/tech-stack-analysis.md

12 KiB
Raw Permalink Blame History

Q1K3 技术栈分析文档

作者:phoboslabDominic Szablewski 竞赛:2021 年 JS13K13KB 游戏编程大赛) 仓库:https://github.com/js13kGames/q1k3


一、项目概述

Q1K3 是 JS13K 2021 的参赛作品,一款致敬《Quake》的第一人称射击游戏。包含完整的 3D 渲染引擎、敌人 AI 系统、武器系统、动态光照、音效音乐,全部压缩在 13KB 以内。项目由多人格大神 phoboslab 独立完成,他也是 IMP(另一款 JS13K 3D 游戏)的作者。


二、核心技术栈总览

层面 技术 用途
渲染 WebGL 1.0(原生) 3D 硬件加速渲染
语言 Vanilla JavaScriptES5/ES6 所有游戏逻辑
音频 Sonant-X(定制版) 程序化生成音效 + 音乐
纹理 Tiny Texture TumblerTTT Canvas 程序化生成纹理
地图 TrenchBroom + 自定义 C 编译器 Quake 风格地图编辑与打包
模型 Wavefront OBJ + 自定义 PHP 打包器 3D 模型处理
压缩 UglifyJS3 + Roadroller + advzip JS 代码 + ZIP 极致压缩
构建 Bash + PHP + GCC 全自动构建管线

三、渲染引擎(renderer.js

3.1 核心架构

纯 WebGL 1.0 实现,无任何第三方库Three.js、PixiJS 等)。作者手写了 vertex 和 fragment shader,实现了一套精简的光栅化管线。

3.2 渲染策略

  • 延迟提交渲染:所有 draw call 先收集到 r_draw_calls 数组中,r_end_frame() 时统一提交。这样灯光 buffer 只需上传一次,所有几何体共享。
  • 动态光照:支持最多 32 个动态光源(R_MAX_LIGHT_V3 = 64,每个光源占 2 个 vec3:位置 + 颜色/强度)。在 fragment shader 中逐像素计算光照,光照衰减按距离平方反比(1/distance²)。
  • 视口投影:使用固定 90° FOV,投影矩阵简化为一元矩阵(atan(90/2) = 1,矩阵 [0][5] 恰为 1)。
  • 颜色量化:最终输出颜色压缩到 16 级(floor(val * 16 + 0.5) / 16),模拟 Quake 的低色深视觉风格。Gamma 校正通过 pow(vl, 0.75) 实现。

3.3 Shader 代码

Vertex Shader(内嵌在 JS 字符串中):

  • 支持两个顶点缓冲区混合(mix(p, p2, f)),实现模型动画的关键帧插值。
  • 模型旋转矩阵使用 yaw/pitch 分离(mat4 ry(r) / mat4 rz(r))。
  • 投影矩阵、相机旋转、模型位移全部在 VS 中完成,一行 gl_Position 搞定。

Fragment Shader

  • 从纹理采样获取基础颜色。
  • 循环计算所有光源:法线方向点积 + 距离衰减 + 颜色强度。
  • gamma 校正 + 颜色量化。

3.4 几何体构建

代码中直接构建 3D 几何体:

  • r_push_quad():用 4 个顶点 + UV坐标 + 法线构建三角形对(6 个顶点 = 2 个三角形)。
  • r_push_block():构建完整 6 面体(12 个三角形),按面设置纹理映射。
  • 顶点格式紧凑:每顶点 8 个 float = [x, y, z, u, v, nx, ny, nz](位置 3 + UV 2 + 法线 3)。
  • 所有顶点存入一个 Float32Array(固定 64K 顶点上限),一次性提交到 GPU。

3.5 WebGL 编码技巧

通过 pack_js.php 将所有 gl.xxx 方法名和枚举常量替换为最短缩写(如 gl.buD 代替 gl.bufferData34962 代替 gl.ARRAY_BUFFER)。这是 13KB 极限压缩下极致的尺寸优化手段。


四、音频系统(audio.js

4.1 来源

基于 Sonant-X(zlib 许可证)的深度定制版。作者在 2018、2019、2021 年三次重写,改为使用 Float32 buffer 直接合成音频,并改用查找表代替函数调用实现振荡器(~10x 性能提升)。

4.2 技术特点

  • 纯程序化合成:无任何音频文件,全部在运行时用数学函数生成波形。
  • 自定义参数编码:声音和歌曲定义从 JS 对象改为数字数组,大幅压缩代码体积。
  • 3D 空间音频:支持立体声分离和距离衰减(基于实体与相机的相对位置)。
  • 流式播放:音乐通过 audio_create_song() 创建,与音效共用同一个音频上下文。

五、纹理系统(textures.js + ttt.js

5.1 Tiny Texture TumblerTTT

由 phoboslab 开发的程序化纹理生成库。所有纹理没有 PNG/JPG 资产文件,全部通过 Canvas 2D API 在运行时生成。

5.2 编码方式

纹理定义编码为紧凑数字数组,例如:

[64,64,0,2,3,1.4,2,17176,1.3]

代表:宽 64x64,分形噪声种子、颜色混合模式、缩放因子等参数。

5.3 纹理种类

30 种不同纹理,涵盖:

  • 砖墙纹理
  • 金属面板
  • 血迹/污渍
  • 地板石纹
  • 特殊图案(logo、符号等)
  • 粒子效果用纹理

六、地图系统(map.js + pack_map.c

6.1 设计工具

地图使用 TrenchBroom(Quake 引擎地图编辑器)编辑,导出为 .map 格式(Quake 标准 Brush 格式)。

6.2 编译管线

  1. 原始格式.map 文件包含用平面方程定义的 Brush 体(每 6 个平面 = 1 个立方体 Brush)。
  2. C 编译器pack_map.c 用标准 C99 编译,将 .map 文件解析为紧凑二进制 .plb 格式。
  3. 合并:所有地图数据拼接到一个二进制文件 build/l

6.3 运行时格式

struct {
    u16 blocks_size;        // 区块数据大小
    block_t blocks[];       // 区块数组(纹理切换指令穿插其中)
    u16 num_entities;       // 实体数量
    entity_t entities[];    // 实体数据
} map_data;

每个 block_tu8 x, y, z, sx, sy, sz(位置 + 尺寸,共 6 字节)。

6.4 碰撞检测

使用 128³ Bitmap 作为碰撞贴图:

  • u8 cm[128*128*128 >> 3] = 256KB 的位图内存。
  • 每 bit 表示一个 voxel 是否被占据。
  • 支持快速遍历射线/球体碰撞检测。

七、模型系统(model.js + pack_model.php

7.1 设计流程

  1. 在 Blender 或其他 3D 软件中建 OBJ 模型。
  2. pack_model.php 将 OBJ 文件转换为自定义紧凑二进制格式 .rmfRetarded Model Format)。
  3. 运行时 model_load_containerUint8Array 解析二进制数据。

7.2 RMF 格式

struct {
    u8 num_frames;          // 动画帧数
    u8 num_verts;           // 每帧顶点数
    u8 num_indices;         // 索引数
    vert_t verts[num_frames * num_verts];  // 顶点数据
    index_t indices[num_indices];          // 三角索引
} rmf_data;

7.3 关键实现细节

  • 顶点只含位置3 bytes: x, y, z),UV 和法线通过 OBJ 的纹理坐标/法线计算。
  • 动画通过帧间线性插值r_draw() 传入两个帧的 offset 和混合因子 mixshader 中 mix(p, p2, f) 完成插值。
  • 共享几何体:多种模型共用相同几何体模板,仅缩放和纹理不同。
  • 模型容器:所有模型拼进一个文件 build/m,按顺序读取。

八、实体系统(entity.js + 各类派生)

8.1 架构

基于类的继承体系:

entity_t(基类)
├── entity_player_t      ← 玩家
├── entity_enemy_t(基类)
│   ├── entity_enemy_grunt_t
│   ├── entity_enemy_enforcer_t
│   ├── entity_enemy_ogre_t
│   ├── entity_enemy_zombie_t
│   └── entity_enemy_hound_t
├── entity_projectile_*(弹丸)
├── entity_pickup_*(拾取物)
├── entity_door_t
├── entity_barrel_t
├── entity_light_t
├── entity_torch_t
├── entity_particle_t(粒子)
└── entity_trigger_level_t(关卡触发)

8.2 物理系统

内置在 entity_t._update_physics() 中:

  • 重力加速度(a.y = -1200 * gravity
  • 速度积分与摩擦
  • AABB 碰撞检测(基于碰撞贴图)
  • 台阶高度处理(_step_height
  • 弹跳系数(_bounciness

8.3 敌人 AI

有限状态机实现,每种敌人共用 entity_enemy_t 基类的状态机:

  • IDLE → PATROL → FOLLOW → ATTACK → EVADE
  • 视线检测(Raycasting 检查障碍物)
  • 无寻路算法,但能通过视线检测合理跟随玩家
  • 5 种敌人类型各有不同参数(速度、攻击距离、攻击方式、血量等)

九、武器系统(weapons.js

基于类的武器系统,共 3 种武器:

武器 弹药 特点
霰弹枪 ∞(无限) 基础武器,发射多发弹丸
钉枪 有消耗 高速连续射击
榴弹发射器 有消耗 弹道弧线 + 爆炸范围伤害

weapon_t 基类处理发射逻辑,子类重写 _init() 设置不同参数。


十、输入系统(input.js

  • 键盘:使用 ev.code(WASD / 方向键),键盘布局无关。
  • 鼠标Pointer Lock API,通过 movementX / movementY 实现视角控制。
  • 键位映射:通过字符串第 N 个字符的映射技巧减少代码体积(W 对应 KeyW 的 [1]p 对应 ArrowUp 的 [6])。

十一、构建管线(build.sh

全自动构建流程,从源文件到最终 13KB ZIP:

1. gcc → pack_map.c → 编译地图
2. php → pack_model.php → 打包模型
3. cat → 所有 JS 源拼接为一个文件
4. php → pack_js.php → WebGL 调用压缩 + DEBUG 去除
5. npx uglify-js → 代码混淆压缩(toplevel + mangle-props
6. npx roadroller → 进一步压缩(基于概率模型的压缩)
7. sed → 嵌入 HTML 模板
8. zip → 创建最终 ZIP 包
9. advzip → 再压缩优化

关键压缩步骤

  • pack_js.php:将 gl.ARRAY_BUFFER 替换为 34962gl.bufferData 替换为 gl.buD,节省大量字符。
  • Roadroller:JS13K 专用的概率模型压缩器,比常规 gzip/zip 压缩率更高。
  • advzipAdvanceCOMP 工具,在 ZIP 级别进一步优化。

十二、代码风格与体积优化技巧

12.1 命名约定

  • 所有变量名极短(1-3 字符),如 r_num_vertsfc
  • 私有方法用 _ 前缀标记(mangle-props 时可安全缩短)
  • 全局变量用 let 声明,不加 varES6 更短)

12.2 编码技巧

  • 矩阵和向量函数用一行写在高阶函数链中(vec3_normalize(vec3_cross(...))
  • 条件编译/*DEBUG[*/ 'build/' + /*]*/ — 注释去掉后自动切换路径
  • WebGL 方法名自动缩短:运行时通过 name.match(/(^..|[A-Z]|\d.|v$)/g).join('') 提取缩写
  • 纹理定义数字编码:把 30 个纹理的参数编码为紧凑数组,无需单独命名

12.3 内存分配策略

  • 固定大小预分配:Float32Array(R_MAX_VERTS * 8)64K 顶点上限)
  • 无垃圾回收敏感操作:所有内存一次性分配,运行时复用 buffer
  • 碰撞贴图:128³ 位图(262,144 bits = 32KB),空间换时间

十三、值得注意的架构决策

决策 原因
纯原生 WebGL,不用 Three.js 13KB 限制下无法容忍任何库的开销
程序化纹理 + 音效 没有空间放外部资产文件
碰撞用 128³ 位图(非 BVH/八叉树) 地图规模小(128³),位图查找 O(1)
延迟绘制提交 减少 uniform 上传次数,优化光照计算
模型动画用顶点插值而非骨骼 省去骨骼矩阵计算,shader 中一行 mix 搞定
敌人在基础类中共享 AI 状态机 5 种敌人共用一套 AI 逻辑,参数差异化

十四、与 fox-maze 的对比(参考)

维度 Q1K3 fox-maze
渲染 WebGL 1.03D Canvas 2DTile-based 2D
纹理 程序化生成(TTT 程序化生成(Canvas
地图 TrenchBroom → 自定义二进制 程序化迷宫生成
模型 OBJ → RMF 二进制格式 无 3D 模型
碰撞 128³ 位图 Tile cell 碰撞
音频 程序化合成(Sonant-X 待定
压缩 UglifyJS + Roadroller + advzip 不适用

文档版本:2024-07-04 分析对象:q1k3 commit at clone