# 手机端虚拟按键方案 > 目标:手机浏览器打开 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 = '
' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '' + '' + '' + '' + '
'; 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 行) 在 `''` 之前插入虚拟按键样式: ```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。