Compare commits
13 Commits
babefbc083
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3376c3e333 | |||
| dfb738f406 | |||
| b8cfe6ec32 | |||
| 5beb3d2940 | |||
| 9735c0fdcf | |||
| 8ee934856c | |||
| c419b7d126 | |||
| d1759ae6ec | |||
| cfcff07c58 | |||
| 3112515834 | |||
| 4a24fcc2fa | |||
| feff06d83b | |||
| a522cb597d |
+14
-7
@@ -1,8 +1,15 @@
|
|||||||
node_modules
|
# Build artifacts
|
||||||
package.json
|
build/*
|
||||||
package-lock.json
|
!build/dummy.txt
|
||||||
autosave/
|
|
||||||
build/
|
# Compiled map packer
|
||||||
pack_map
|
pack_map
|
||||||
\#*
|
|
||||||
*~
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
package.json
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
post: https://phoboslab.org/log/2021/09/q1k3-making-of
|
||||||
|
# See github.com/js13kGames/hello-world for supported frontmatter
|
||||||
|
---
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **Movement**: <kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> or <kbd>↑</kbd> <kbd>←</kbd> <kbd>↓</kbd> <kbd>→</kbd>
|
||||||
|
- **Attack**: <kbd><svg><use href=#i-mouse></svg> LMB</kbd>
|
||||||
|
- **Jump**: <kbd>Space</kbd> or <kbd><svg><use href=#i-mouse></svg> RMB</kbd>
|
||||||
|
- **Switch weapon**: <kbd>Q</kbd>/<kbd>E</kbd> or <kbd><svg><use href=#i-mouse></svg> Mousewheel</kbd>
|
||||||
|
|
||||||
|
If you accidentally scroll the page down when you want to change weapons, either resize your browser window so there's
|
||||||
|
no scrollbar, or go to fullscreen using the button below the game!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 2 Levels
|
||||||
|
- 5 Types of enemies
|
||||||
|
- 3 Weapons
|
||||||
|
- 30 different textures
|
||||||
|
- Music from Andy Lösch - no-fate.net
|
||||||
|
- Dynamic lighting
|
||||||
|
- Doors(!)
|
||||||
|
- Somewhat robust collision detection, even for fast moving objects
|
||||||
|
- Enemy AI with line of sight checks. No pathfinding, but still does a reasonable job following the player
|
||||||
|
- "Spatial" Audio (stereo separation and falloff by distance)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -4,6 +4,8 @@ My entry for the 2021 [js13k](https://js13kgames.com/) competition.
|
|||||||
|
|
||||||
Play here: https://phoboslab.org/q1k3/
|
Play here: https://phoboslab.org/q1k3/
|
||||||
|
|
||||||
|
Making Of: https://phoboslab.org/log/2021/09/q1k3-making-of
|
||||||
|
|
||||||
### Controls
|
### Controls
|
||||||
- Movement: WASD or Arrow Keys
|
- Movement: WASD or Arrow Keys
|
||||||
- Attack: Left Mouse Button
|
- Attack: Left Mouse Button
|
||||||
|
|||||||
+1
-1
@@ -2415,7 +2415,7 @@
|
|||||||
// entity 60
|
// entity 60
|
||||||
{
|
{
|
||||||
"classname" "pickup_nails"
|
"classname" "pickup_nails"
|
||||||
"origin" "2660 1188 252"
|
"origin" "2660 1204 252"
|
||||||
}
|
}
|
||||||
// entity 61
|
// entity 61
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ npx uglify-js build/game.packed.js \
|
|||||||
--compress --mangle toplevel --mangle-props regex=/^_/ \
|
--compress --mangle toplevel --mangle-props regex=/^_/ \
|
||||||
-o build/game.min.js
|
-o build/game.min.js
|
||||||
|
|
||||||
npx roadroller -Zab14 -Zlr1113 -Zmd16 -Zpr14 -S0,1,2,3,6,7,13,21,25,50,235,333 build/game.min.js -o build/game.roadrolled.js
|
npx roadroller -Zab14 -Zlr930 -Zmd19 -Zpr14 -S0,1,2,3,7,13,14,19,58,97,305,422 build/game.min.js -o build/game.roadrolled.js
|
||||||
|
|
||||||
# Embed source into HTML
|
# Embed source into HTML
|
||||||
sed -e '/GAME_SOURCE/{r build/game.roadrolled.js' -e 'd}' source/html_template.html > build/index.html
|
sed -e '/GAME_SOURCE/{r build/game.roadrolled.js' -e 'd;}' source/html_template.html > build/index.html
|
||||||
|
|
||||||
# Build ZIP
|
# Build ZIP
|
||||||
rm -f -- build/game.zip
|
rm -f -- build/game.zip
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# 手机端虚拟按键方案
|
||||||
|
|
||||||
|
> 目标:手机浏览器打开 https://foxpaw.top/q1k3/ 能直接玩
|
||||||
|
> 方案:半透明悬浮按键,覆盖在游戏界面上
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、输入映射
|
||||||
|
|
||||||
|
游戏当前输入系统(`input.js`)是 `keys[]` 数组 + `mouse_x/mouse_y`,虚拟按键只需往同一个数组里写值:
|
||||||
|
|
||||||
|
| 游戏操作 | 当前触发方式 | keys 索引 | 虚拟按键 |
|
||||||
|
|---------|-------------|----------|---------|
|
||||||
|
| 前进 | W / ↑ | keys[1] | 左区 ↑ |
|
||||||
|
| 后退 | S / ↓ | keys[3] | 左区 ↓ |
|
||||||
|
| 左移 | A / ← | keys[2] | 左区 ← |
|
||||||
|
| 右移 | D / → | keys[4] | 左区 → |
|
||||||
|
| 射击 | 鼠标左键 | keys[7] | 右下大按钮 |
|
||||||
|
| 跳跃 | 空格 / 鼠标右键 | keys[9] | 右下小按钮 |
|
||||||
|
| 切武器上 | E / 滚轮下 | keys[6] | 右区小按钮 |
|
||||||
|
| 切武器下 | Q / 滚轮上 | keys[5] | 右区小按钮 |
|
||||||
|
| 视角旋转 | 鼠标移动 | mouse_x/mouse_y | 右半屏拖拽 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、界面布局
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ 游戏画面 (Canvas) │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ ┌────┐ ┌─────────┐ │
|
||||||
|
│ │ ↑ │ │ 视角 │ │
|
||||||
|
│ │← ─→│ │ 拖拽 │ │
|
||||||
|
│ │ ↓ │ │ 区域 │ │
|
||||||
|
│ └────┘ └─────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌────┐ ┌──────┐ ┌───────┐ │
|
||||||
|
│ │攻击│ │ Q │ │ E │ │
|
||||||
|
│ │ │ │上武器│ │下武器 │ │
|
||||||
|
│ └────┘ └──────┘ └───────┘ │
|
||||||
|
│ ┌─────────┐ │
|
||||||
|
│ │ 跳跃 │ │
|
||||||
|
│ └─────────┘ │
|
||||||
|
└──────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分区说明
|
||||||
|
|
||||||
|
| 分区 | 覆盖区域 | 操作 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **左区(运动)** | 屏幕左 30%,从底部到 55% 高度 | 四个方向按钮 + 中心空白 |
|
||||||
|
| **右区(视角)** | 屏幕右 70%,从顶部到底部 | 触摸拖拽控制视角 |
|
||||||
|
| **右下角(动作)** | 屏幕底部 15%,左侧攻击 | 攻击、武器切换、跳跃 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、改动文件清单
|
||||||
|
|
||||||
|
### 3.1 `input.js` — 新增触摸处理 (~40 行)
|
||||||
|
|
||||||
|
在文件末尾追加:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ─── 移动端虚拟按键 ───
|
||||||
|
|
||||||
|
// 检测触摸设备
|
||||||
|
let is_touch = 'ontouchstart' in window;
|
||||||
|
|
||||||
|
// 仅在触摸设备上创建虚拟按键
|
||||||
|
if (is_touch) {
|
||||||
|
// 创建控制层
|
||||||
|
let ctrl = document.createElement('div');
|
||||||
|
ctrl.id = 'ctrl';
|
||||||
|
ctrl.innerHTML =
|
||||||
|
'<div id="ctrl-left">' +
|
||||||
|
'<button class="ctrl-btn" id="btn-up">▲</button>' +
|
||||||
|
'<button class="ctrl-btn" id="btn-left">◀</button>' +
|
||||||
|
'<button class="ctrl-btn" id="btn-right">▶</button>' +
|
||||||
|
'<button class="ctrl-btn" id="btn-down">▼</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="ctrl-right">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div id="ctrl-actions">' +
|
||||||
|
'<button class="ctrl-btn ctrl-shoot" id="btn-shoot">⚡</button>' +
|
||||||
|
'<button class="ctrl-btn ctrl-prev" id="btn-prev">◀◀</button>' +
|
||||||
|
'<button class="ctrl-btn ctrl-next" id="btn-next">▶▶</button>' +
|
||||||
|
'<button class="ctrl-btn ctrl-jump" id="btn-jump">▲</button>' +
|
||||||
|
'</div>';
|
||||||
|
document.getElementById('g').appendChild(ctrl);
|
||||||
|
|
||||||
|
// 阻止 touch 事件穿透到 canvas
|
||||||
|
ctrl.addEventListener('touchstart', (e) => e.preventDefault(), {passive: false});
|
||||||
|
ctrl.addEventListener('touchmove', (e) => e.preventDefault(), {passive: false});
|
||||||
|
|
||||||
|
// 左区方向键 - touchstart 按下,touchend 松开
|
||||||
|
let bindBtn = (id, keyIndex) => {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
el.addEventListener('touchstart', () => { keys[keyIndex] = 1; });
|
||||||
|
el.addEventListener('touchend', () => { keys[keyIndex] = 0; });
|
||||||
|
el.addEventListener('touchcancel', () => { keys[keyIndex] = 0; });
|
||||||
|
};
|
||||||
|
bindBtn('btn-up', 1);
|
||||||
|
bindBtn('btn-down', 3);
|
||||||
|
bindBtn('btn-left', 2);
|
||||||
|
bindBtn('btn-right', 4);
|
||||||
|
bindBtn('btn-shoot', 7);
|
||||||
|
bindBtn('btn-jump', 9);
|
||||||
|
bindBtn('btn-prev', 5);
|
||||||
|
bindBtn('btn-next', 6);
|
||||||
|
|
||||||
|
// 右区触摸拖拽 → 视角控制
|
||||||
|
let rightZone = document.getElementById('ctrl-right');
|
||||||
|
let touch_id = null, last_tx = 0, last_ty = 0;
|
||||||
|
rightZone.addEventListener('touchstart', (e) => {
|
||||||
|
let t = e.changedTouches[0];
|
||||||
|
touch_id = t.identifier;
|
||||||
|
last_tx = t.clientX;
|
||||||
|
last_ty = t.clientY;
|
||||||
|
});
|
||||||
|
rightZone.addEventListener('touchmove', (e) => {
|
||||||
|
for (let t of e.changedTouches) {
|
||||||
|
if (t.identifier === touch_id) {
|
||||||
|
mouse_x += t.clientX - last_tx;
|
||||||
|
mouse_y += t.clientY - last_ty;
|
||||||
|
last_tx = t.clientX;
|
||||||
|
last_ty = t.clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rightZone.addEventListener('touchend', (e) => {
|
||||||
|
for (let t of e.changedTouches) {
|
||||||
|
if (t.identifier === touch_id) touch_id = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 `document.js` — 新增 CSS 样式 (~25 行)
|
||||||
|
|
||||||
|
在 `'</style>'` 之前插入虚拟按键样式:
|
||||||
|
|
||||||
|
```js
|
||||||
|
'#ctrl{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10}'+
|
||||||
|
'#ctrl-left{position:absolute;bottom:18%;left:2%;width:28%;height:40%;pointer-events:auto}'+
|
||||||
|
'#ctrl-right{position:absolute;top:0;right:0;width:70%;height:100%;pointer-events:auto}'+
|
||||||
|
'#ctrl-actions{position:absolute;bottom:3%;right:2%;display:flex;gap:8px;pointer-events:auto}'+
|
||||||
|
'.ctrl-btn{background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.25);'+
|
||||||
|
'border-radius:12px;color:rgba(255,255,255,0.5);font-size:18px;'+
|
||||||
|
'display:flex;align-items:center;justify-content:center;'+
|
||||||
|
'user-select:none;-webkit-user-select:none;touch-action:none}'+
|
||||||
|
'.ctrl-btn:active{background:rgba(255,255,255,0.35)}'+
|
||||||
|
'#btn-up{position:absolute;top:0;left:50%;transform:translateX(-50%);width:48px;height:48px}'+
|
||||||
|
'#btn-down{position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:48px;height:48px}'+
|
||||||
|
'#btn-left{position:absolute;left:0;top:50%;transform:translateY(-50%);width:48px;height:48px}'+
|
||||||
|
'#btn-right{position:absolute;right:0;top:50%;transform:translateY(-50%);width:48px;height:48px}'+
|
||||||
|
'.ctrl-shoot{width:64px;height:64px;border-radius:50%;font-size:24px}'+
|
||||||
|
'.ctrl-prev,.ctrl-next{width:40px;height:40px;font-size:14px}'+
|
||||||
|
'.ctrl-jump{width:52px;height:52px;border-radius:50%;font-size:20px}'+
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 `main.js` — 不请求 Pointer Lock(触摸设备跳过)
|
||||||
|
|
||||||
|
在 `g.onclick` 回调中,进入 Pointer Lock 之前加判断:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (!is_touch) {
|
||||||
|
g.onclick = () => c.requestPointerLock();
|
||||||
|
g.onclick();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、处理边界情况
|
||||||
|
|
||||||
|
### 4.1 触摸与 Pointer Lock 冲突
|
||||||
|
|
||||||
|
当前启动流程:点击标题画面 → 第一次点击全屏 → 第二次点击请求 Pointer Lock。
|
||||||
|
触摸设备上不请求 Pointer Lock,改为直接进游戏。触摸操作本身的焦点是虚拟按键层,与 canvas 无关。
|
||||||
|
|
||||||
|
### 4.2 鼠标速度滑块
|
||||||
|
|
||||||
|
桌面端有鼠标灵敏度滑块,触摸端不需要(默认灵敏度即可),仍然保留显示不影响。
|
||||||
|
|
||||||
|
### 4.3 多指触控
|
||||||
|
|
||||||
|
- 方向键:单指操作,每个指对应一个按键
|
||||||
|
- 视角拖拽只用第一根触摸点(touch_id 记录 identifier)
|
||||||
|
- 攻击等动作按钮可与其他操作同时进行(不同手指)
|
||||||
|
|
||||||
|
### 4.4 横竖屏
|
||||||
|
|
||||||
|
布局基于 vw/vh 百分比,横竖屏自动适配。左区 28% × 40%,右区 70% × 100%,底部动作栏固定在右下角。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、效果预期
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ┌──┐ ┌──────────────────────────┐ │
|
||||||
|
│ │▲ │ │ │ │
|
||||||
|
│ │◀┼▶│ │ 游戏画面(可拖拽视角) │ │
|
||||||
|
│ │▼ │ │ │ │
|
||||||
|
│ └──┘ │ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌──┐ │ ┌─┐┌─┐│ │
|
||||||
|
│ │攻│ │ │Q││E││ │
|
||||||
|
│ │击│ │ └─┘└─┘│ │
|
||||||
|
│ └──┘ │ ┌──┐ │ │
|
||||||
|
│ │ │跳│ │ │
|
||||||
|
│ │ └──┘ │ │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、改动汇总
|
||||||
|
|
||||||
|
| 文件 | 改动量 | 说明 |
|
||||||
|
|------|-------|------|
|
||||||
|
| `source/input.js` | ~40 行 | 新增虚拟按键 DOM 创建 + 触摸事件绑定 |
|
||||||
|
| `source/document.js` | ~15 行 | 新增 CSS 样式 |
|
||||||
|
| `source/main.js` | ~3 行 | 触摸设备跳过 Pointer Lock |
|
||||||
|
|
||||||
|
总增量:约 60 行纯 JS + 15 行 CSS → 预估 gzip 后增加 < 1KB。
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# Q1K3 技术栈分析文档
|
||||||
|
|
||||||
|
> 作者:phoboslab(Dominic Szablewski)
|
||||||
|
> 竞赛:2021 年 JS13K(13KB 游戏编程大赛)
|
||||||
|
> 仓库:https://github.com/js13kGames/q1k3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目概述
|
||||||
|
|
||||||
|
Q1K3 是 JS13K 2021 的参赛作品,一款致敬《Quake》的第一人称射击游戏。包含完整的 3D 渲染引擎、敌人 AI 系统、武器系统、动态光照、音效音乐,**全部压缩在 13KB 以内**。项目由多人格大神 [phoboslab](https://phoboslab.org) 独立完成,他也是 [IMP](https://phoboslab.org/log/2023/08/imp)(另一款 JS13K 3D 游戏)的作者。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、核心技术栈总览
|
||||||
|
|
||||||
|
| 层面 | 技术 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| **渲染** | WebGL 1.0(原生) | 3D 硬件加速渲染 |
|
||||||
|
| **语言** | Vanilla JavaScript(ES5/ES6) | 所有游戏逻辑 |
|
||||||
|
| **音频** | Sonant-X(定制版) | 程序化生成音效 + 音乐 |
|
||||||
|
| **纹理** | Tiny Texture Tumbler(TTT) | 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.bufferData`,`34962` 代替 `gl.ARRAY_BUFFER`)。这是 13KB 极限压缩下极致的尺寸优化手段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、音频系统(audio.js)
|
||||||
|
|
||||||
|
### 4.1 来源
|
||||||
|
|
||||||
|
基于 [Sonant-X](https://github.com/nicolas-van/sonant-x)(zlib 许可证)的深度定制版。作者在 2018、2019、2021 年三次重写,改为使用 Float32 buffer 直接合成音频,并改用查找表代替函数调用实现振荡器(~10x 性能提升)。
|
||||||
|
|
||||||
|
### 4.2 技术特点
|
||||||
|
|
||||||
|
- **纯程序化合成**:无任何音频文件,全部在运行时用数学函数生成波形。
|
||||||
|
- **自定义参数编码**:声音和歌曲定义从 JS 对象改为数字数组,大幅压缩代码体积。
|
||||||
|
- **3D 空间音频**:支持立体声分离和距离衰减(基于实体与相机的相对位置)。
|
||||||
|
- **流式播放**:音乐通过 `audio_create_song()` 创建,与音效共用同一个音频上下文。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、纹理系统(textures.js + ttt.js)
|
||||||
|
|
||||||
|
### 5.1 Tiny Texture Tumbler(TTT)
|
||||||
|
|
||||||
|
由 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](https://trenchbroom.github.io/)(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 运行时格式
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct {
|
||||||
|
u16 blocks_size; // 区块数据大小
|
||||||
|
block_t blocks[]; // 区块数组(纹理切换指令穿插其中)
|
||||||
|
u16 num_entities; // 实体数量
|
||||||
|
entity_t entities[]; // 实体数据
|
||||||
|
} map_data;
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 `block_t`:`u8 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 文件转换为自定义紧凑二进制格式 `.rmf`(Retarded Model Format)。
|
||||||
|
3. 运行时 `model_load_container` 用 `Uint8Array` 解析二进制数据。
|
||||||
|
|
||||||
|
### 7.2 RMF 格式
|
||||||
|
|
||||||
|
```c
|
||||||
|
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 和混合因子 `mix`,shader 中 `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` 替换为 `34962`,`gl.bufferData` 替换为 `gl.buD`,节省大量字符。
|
||||||
|
- **Roadroller**:JS13K 专用的概率模型压缩器,比常规 gzip/zip 压缩率更高。
|
||||||
|
- **advzip**:AdvanceCOMP 工具,在 ZIP 级别进一步优化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、代码风格与体积优化技巧
|
||||||
|
|
||||||
|
### 12.1 命名约定
|
||||||
|
|
||||||
|
- 所有变量名极短(1-3 字符),如 `r_num_verts`、`f`、`c`
|
||||||
|
- 私有方法用 `_` 前缀标记(mangle-props 时可安全缩短)
|
||||||
|
- 全局变量用 `let ` 声明,不加 `var`(ES6 更短)
|
||||||
|
|
||||||
|
### 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.0(3D) | Canvas 2D(Tile-based 2D) |
|
||||||
|
| 纹理 | 程序化生成(TTT) | 程序化生成(Canvas) |
|
||||||
|
| 地图 | TrenchBroom → 自定义二进制 | 程序化迷宫生成 |
|
||||||
|
| 模型 | OBJ → RMF 二进制格式 | 无 3D 模型 |
|
||||||
|
| 碰撞 | 128³ 位图 | Tile cell 碰撞 |
|
||||||
|
| 音频 | 程序化合成(Sonant-X) | 待定 |
|
||||||
|
| 压缩 | UglifyJS + Roadroller + advzip | 不适用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本:2024-07-04*
|
||||||
|
*分析对象:q1k3 commit at clone*
|
||||||
+26
-1
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
|
// Touch detection
|
||||||
|
is_touch = 'ontouchstart' in window;
|
||||||
|
|
||||||
document.body.innerHTML +=
|
document.body.innerHTML +=
|
||||||
'<style>'+
|
'<style>'+
|
||||||
'*{font-family:sans-serif;}'+
|
'*{font-family:sans-serif;}'+
|
||||||
@@ -10,6 +13,28 @@ document.body.innerHTML +=
|
|||||||
'#a,#h{position:absolute;bottom:3%;left:20%;right:0;font-size:3.2vw;}'+
|
'#a,#h{position:absolute;bottom:3%;left:20%;right:0;font-size:3.2vw;}'+
|
||||||
'#h{left:-20%;}'+
|
'#h{left:-20%;}'+
|
||||||
'h1{font-size:16vw;margin:0;}'+
|
'h1{font-size:16vw;margin:0;}'+
|
||||||
|
(is_touch ?
|
||||||
|
'#ctrl{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10}'+
|
||||||
|
'#ctrl-left{position:absolute;bottom:18%;left:2%;width:28%;height:40%;pointer-events:auto}'+
|
||||||
|
'#ctrl-right{position:absolute;top:0;right:0;width:70%;height:100%;pointer-events:auto}'+
|
||||||
|
'#ctrl-actions{position:absolute;bottom:3%;right:2%;display:flex;gap:8px;pointer-events:auto}'+
|
||||||
|
'.ctrl-btn{background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.25);'+
|
||||||
|
'border-radius:12px;color:rgba(255,255,255,0.5);font-size:18px;'+
|
||||||
|
'display:flex;align-items:center;justify-content:center;'+
|
||||||
|
'user-select:none;-webkit-user-select:none;touch-action:none}'+
|
||||||
|
'.ctrl-btn:active{background:rgba(255,255,255,0.35)}'+
|
||||||
|
'#btn-up{position:absolute;top:0;left:50%;transform:translateX(-50%);width:48px;height:48px}'+
|
||||||
|
'#btn-down{position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:48px;height:48px}'+
|
||||||
|
'#btn-left{position:absolute;left:0;top:50%;transform:translateY(-50%);width:48px;height:48px}'+
|
||||||
|
'#btn-right{position:absolute;right:0;top:50%;transform:translateY(-50%);width:48px;height:48px}'+
|
||||||
|
'.ctrl-shoot{width:64px;height:64px;border-radius:50%;font-size:24px}'+
|
||||||
|
'.ctrl-prev,.ctrl-next{width:40px;height:40px;font-size:14px}'+
|
||||||
|
'.ctrl-jump{width:52px;height:52px;border-radius:50%;font-size:20px}'+
|
||||||
|
'#btn-fs{position:absolute;top:8px;right:8px;width:40px;height:40px;'+
|
||||||
|
'background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.3);'+
|
||||||
|
'border-radius:8px;color:#fff;font-size:20px;z-index:20;'+
|
||||||
|
'display:flex;align-items:center;justify-content:center}'
|
||||||
|
: '')+
|
||||||
'</style>'+
|
'</style>'+
|
||||||
'<div id="g">'+
|
'<div id="g">'+
|
||||||
'<canvas id=c width=320 height=180></canvas>'+
|
'<canvas id=c width=320 height=180></canvas>'+
|
||||||
@@ -17,7 +42,7 @@ document.body.innerHTML +=
|
|||||||
'<div id="h"></div><div id="a"></div>'+
|
'<div id="h"></div><div id="a"></div>'+
|
||||||
'<div id="msg"></div>'+
|
'<div id="msg"></div>'+
|
||||||
'</div>'+
|
'</div>'+
|
||||||
'<p>MOUSE SPEED: <input id="m" type="range" value=10 min=1 max=50></p>'+
|
'<p>MOUSE SPEED: <input id="m" type="range" value=10 min=1 max=50> INVERT: <input type="checkbox" id="mi"></p>'+
|
||||||
'<p><input id="f" type="button" value="FULLSCREEN"></p>'+
|
'<p><input id="f" type="button" value="FULLSCREEN"></p>'+
|
||||||
'<p>'+
|
'<p>'+
|
||||||
'code: <a href="https://phoboslab.org">phoboslab.org</a> / music: <a href="http://no-fate.net">no-fate.net</a>'+
|
'code: <a href="https://phoboslab.org">phoboslab.org</a> / music: <a href="http://no-fate.net">no-fate.net</a>'+
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class entity_player_t extends entity_t {
|
|||||||
|
|
||||||
_update() {
|
_update() {
|
||||||
// Mouse look
|
// Mouse look
|
||||||
this._pitch = clamp(this._pitch + mouse_y * m.value * 0.00015, -1.5, 1.5);
|
this._pitch = clamp(this._pitch + mouse_y * m.value * (mi.checked ? -0.00015 : 0.00015), -1.5, 1.5);
|
||||||
this._yaw = (this._yaw + mouse_x * m.value * 0.00015) % (Math.PI*2);
|
this._yaw = (this._yaw + mouse_x * m.value * 0.00015) % (Math.PI*2);
|
||||||
|
|
||||||
// Acceleration in movement direction
|
// Acceleration in movement direction
|
||||||
|
|||||||
@@ -81,3 +81,73 @@ c.onmouseup = (ev) => {
|
|||||||
keys[key_action + ev.button] = 0;
|
keys[key_action + ev.button] = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── Mobile virtual controls ───
|
||||||
|
|
||||||
|
// Called from main.js after game start
|
||||||
|
create_touch_controls = () => {
|
||||||
|
let ctrl = document.createElement('div');
|
||||||
|
ctrl.id = 'ctrl';
|
||||||
|
ctrl.innerHTML =
|
||||||
|
'<div id="ctrl-left">'+
|
||||||
|
'<button class="ctrl-btn" id="btn-up">▲</button>'+
|
||||||
|
'<button class="ctrl-btn" id="btn-left">◀</button>'+
|
||||||
|
'<button class="ctrl-btn" id="btn-right">▶</button>'+
|
||||||
|
'<button class="ctrl-btn" id="btn-down">▼</button>'+
|
||||||
|
'</div>'+
|
||||||
|
'<div id="ctrl-right"></div>'+
|
||||||
|
'<div id="ctrl-actions">'+
|
||||||
|
'<button class="ctrl-btn ctrl-shoot" id="btn-shoot">⚡</button>'+
|
||||||
|
'<button class="ctrl-btn ctrl-prev" id="btn-prev">◀◀</button>'+
|
||||||
|
'<button class="ctrl-btn ctrl-next" id="btn-next">▶▶</button>'+
|
||||||
|
'<button class="ctrl-btn ctrl-jump" id="btn-jump">▲</button>'+
|
||||||
|
'</div>';
|
||||||
|
document.getElementById('g').appendChild(ctrl);
|
||||||
|
|
||||||
|
// Prevent touches from reaching the canvas
|
||||||
|
ctrl.addEventListener('touchstart', (e) => e.preventDefault(), {passive: false});
|
||||||
|
ctrl.addEventListener('touchmove', (e) => e.preventDefault(), {passive: false});
|
||||||
|
|
||||||
|
// Bind button press/release
|
||||||
|
let bindBtn = (id, keyIndex) => {
|
||||||
|
let el = document.getElementById(id);
|
||||||
|
el.addEventListener('touchstart', () => { keys[keyIndex] = 1; });
|
||||||
|
el.addEventListener('touchend', () => { keys[keyIndex] = 0; });
|
||||||
|
el.addEventListener('touchcancel', () => { keys[keyIndex] = 0; });
|
||||||
|
};
|
||||||
|
bindBtn('btn-up', 1);
|
||||||
|
bindBtn('btn-down', 3);
|
||||||
|
bindBtn('btn-left', 2);
|
||||||
|
bindBtn('btn-right', 4);
|
||||||
|
bindBtn('btn-shoot', 7);
|
||||||
|
bindBtn('btn-jump', 9);
|
||||||
|
bindBtn('btn-prev', 5);
|
||||||
|
bindBtn('btn-next', 6);
|
||||||
|
|
||||||
|
// Right zone touch drag → camera
|
||||||
|
let rightZone = document.getElementById('ctrl-right'),
|
||||||
|
touch_id = null,
|
||||||
|
last_tx = 0,
|
||||||
|
last_ty = 0;
|
||||||
|
rightZone.addEventListener('touchstart', (e) => {
|
||||||
|
let t = e.changedTouches[0];
|
||||||
|
touch_id = t.identifier;
|
||||||
|
last_tx = t.clientX;
|
||||||
|
last_ty = t.clientY;
|
||||||
|
});
|
||||||
|
rightZone.addEventListener('touchmove', (e) => {
|
||||||
|
for (let t of e.changedTouches) {
|
||||||
|
if (t.identifier === touch_id) {
|
||||||
|
mouse_x += t.clientX - last_tx;
|
||||||
|
mouse_y += t.clientY - last_ty;
|
||||||
|
last_tx = t.clientX;
|
||||||
|
last_ty = t.clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rightZone.addEventListener('touchend', (e) => {
|
||||||
|
for (let t of e.changedTouches) {
|
||||||
|
if (t.identifier === touch_id) touch_id = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
+31
-6
@@ -44,6 +44,7 @@ sfx_enemy_hound_attack,
|
|||||||
|
|
||||||
sfx_no_ammo,
|
sfx_no_ammo,
|
||||||
sfx_hurt,
|
sfx_hurt,
|
||||||
|
sfx_pickup,
|
||||||
|
|
||||||
sfx_plasma_shoot,
|
sfx_plasma_shoot,
|
||||||
|
|
||||||
@@ -122,14 +123,34 @@ game_load = async () => {
|
|||||||
requestAnimationFrame(run_frame);
|
requestAnimationFrame(run_frame);
|
||||||
|
|
||||||
f.onclick = () => g.requestFullscreen();
|
f.onclick = () => g.requestFullscreen();
|
||||||
g.onclick = () => {
|
|
||||||
|
// Mobile: fullscreen button overlay in top-right
|
||||||
|
if (is_touch) {
|
||||||
|
let fs_btn = document.createElement('button');
|
||||||
|
fs_btn.id = 'btn-fs';
|
||||||
|
fs_btn.textContent = '⛶';
|
||||||
|
fs_btn.onclick = () => g.requestFullscreen();
|
||||||
|
document.getElementById('g').appendChild(fs_btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game start handler
|
||||||
|
let start_game = () => {
|
||||||
|
// Remove the entry handler so it only fires once
|
||||||
|
g.onclick = null;
|
||||||
|
if (g._touch_entry) {
|
||||||
|
g.removeEventListener('touchstart', g._touch_entry);
|
||||||
|
g._touch_entry = null;
|
||||||
|
}
|
||||||
|
if (is_touch) {
|
||||||
|
// Create virtual controls now that game is starting
|
||||||
|
create_touch_controls();
|
||||||
|
} else {
|
||||||
g.onclick = () => c.requestPointerLock();
|
g.onclick = () => c.requestPointerLock();
|
||||||
g.onclick();
|
g.onclick();
|
||||||
// title_show_message('...');
|
}
|
||||||
|
|
||||||
audio_init();
|
audio_init();
|
||||||
|
|
||||||
setTimeout(()=> {
|
|
||||||
// Generate sounds
|
// Generate sounds
|
||||||
sfx_enemy_hit = audio_create_sound(135, [8,0,0,1,148,1,3,5,0,0,139,1,0,2653,0,2193,255,2,639,119,2,23,0,0,0,0,0,0,0]);
|
sfx_enemy_hit = audio_create_sound(135, [8,0,0,1,148,1,3,5,0,0,139,1,0,2653,0,2193,255,2,639,119,2,23,0,0,0,0,0,0,0]);
|
||||||
sfx_enemy_gib = audio_create_sound(140, [7,0,0,1,148,1,7,5,0,1,139,1,0,4611,789,15986,195,2,849,119,3,60,0,0,0,1,10,176,1]);
|
sfx_enemy_gib = audio_create_sound(140, [7,0,0,1,148,1,7,5,0,1,139,1,0,4611,789,15986,195,2,849,119,3,60,0,0,0,1,10,176,1]);
|
||||||
@@ -155,10 +176,14 @@ game_load = async () => {
|
|||||||
audio_play(audio_create_song(...music_data), 1, 1);
|
audio_play(audio_create_song(...music_data), 1, 1);
|
||||||
game_init(0);
|
game_init(0);
|
||||||
run_frame = game_run;
|
run_frame = game_run;
|
||||||
},100); // Not sure why such a long delay is needed. Ideally we would
|
|
||||||
// want to use requestAnimationFrame, but the previous DOM
|
|
||||||
// update (...) never makes it to the screen then ;/
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wire up game start: click for desktop, touch for mobile
|
||||||
|
g.onclick = start_game;
|
||||||
|
if (is_touch) {
|
||||||
|
g._touch_entry = (e) => { e.preventDefault(); start_game(); };
|
||||||
|
g.addEventListener('touchstart', g._touch_entry, {passive: false});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
run_frame = (time_now) => {
|
run_frame = (time_now) => {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ map_load_container = async (path) => {
|
|||||||
for (let i = 0; i < data.length;) {
|
for (let i = 0; i < data.length;) {
|
||||||
let blocks_size = data[i++] | (data[i++] << 8),
|
let blocks_size = data[i++] | (data[i++] << 8),
|
||||||
cm = new Uint8Array(map_size * map_size * map_size >> 3), // collision map
|
cm = new Uint8Array(map_size * map_size * map_size >> 3), // collision map
|
||||||
nm = new Uint8Array(map_size * map_size * map_size >> 3), // nav map
|
|
||||||
b = data.subarray(i, i += blocks_size),
|
b = data.subarray(i, i += blocks_size),
|
||||||
r = [],
|
r = [],
|
||||||
t;
|
t;
|
||||||
|
|||||||
Reference in New Issue
Block a user