Files
q1k3/docs/plans/mobile-touch-controls.md
mai dfb738f406 feat: add mobile virtual touch controls
- document.js: add is_touch detection and virtual button CSS
- input.js: create virtual D-pad + action buttons + touch drag camera
- main.js: skip Pointer Lock on touch devices
2026-07-04 08:23:39 +08:00

230 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 手机端虚拟按键方案
> 目标:手机浏览器打开 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。