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

8.9 KiB
Raw Permalink Blame History

手机端虚拟按键方案

目标:手机浏览器打开 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 行)

在文件末尾追加:

// ─── 移动端虚拟按键 ───

// 检测触摸设备
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>' 之前插入虚拟按键样式:

'#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 之前加判断:

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。