Skip to content

输入系统代码导读 (Input System Code Walkthrough)

本节深入 app.js 中的输入处理代码,从键盘映射到 REG_KEYINPUT 的内存写入,逐步解析每个环节。

按键映射表

掩码定义

javascript
// GBA 按键位掩码(与 REG_KEYINPUT 位定义一致)
const KEY_A      = 0x0001;  // Bit 0
const KEY_B      = 0x0002;  // Bit 1
const KEY_SELECT = 0x0004;  // Bit 2
const KEY_START  = 0x0008;  // Bit 3
const KEY_RIGHT  = 0x0010;  // Bit 4
const KEY_LEFT   = 0x0020;  // Bit 5
const KEY_UP     = 0x0040;  // Bit 6
const KEY_DOWN   = 0x0080;  // Bit 7
const KEY_R      = 0x0100;  // Bit 8
const KEY_L      = 0x0200;  // Bit 9

键盘到 GBA 的映射

javascript
// 键盘 code → GBA 按键掩码的映射表
const KEY_MAP = {
  // 主要按钮
  'KeyZ':       KEY_A,
  'KeyX':       KEY_B,
  'Enter':      KEY_START,
  'ShiftRight': KEY_SELECT,

  // 方向键
  'ArrowUp':    KEY_UP,
  'ArrowDown':  KEY_DOWN,
  'ArrowLeft':  KEY_LEFT,
  'ArrowRight': KEY_RIGHT,

  // 肩键
  'KeyA':       KEY_L,
  'KeyS':       KEY_R,
};

按键状态管理

核心状态变量

javascript
let keyState = 0x03FF;  // 初始值:所有按键未按下(低电平有效)
const pressedKeys = new Set();  // 追踪当前物理按下的键,解决多键同掩码问题

键盘事件处理

javascript
function handleKeyDown(e) {
  const mask = KEY_MAP[e.code];
  if (!mask) return;

  e.preventDefault();  // 阻止浏览器默认行为(如方向键滚动页面)
  e.stopPropagation();

  if (pressedKeys.has(e.code)) return;  // 防止按键重复触发
  pressedKeys.add(e.code);

  // 清除对应位:按键按下
  keyState &= ~mask;

  // 立即写入 WASM 内存
  updateKeyInput();
}

function handleKeyUp(e) {
  const mask = KEY_MAP[e.code];
  if (!mask) return;

  e.preventDefault();
  pressedKeys.delete(e.code);

  // 检查是否还有其他键映射到同一掩码
  const stillPressed = [...pressedKeys].some(
    code => KEY_MAP[code] === mask
  );
  if (!stillPressed) {
    // 设置对应位:按键释放
    keyState |= mask;
  }

  updateKeyInput();
}

// 注册事件监听
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);

失焦处理

javascript
// 页面失去焦点时释放所有按键,防止"粘键"
window.addEventListener('blur', () => {
  keyState = 0x03FF;
  pressedKeys.clear();
  updateKeyInput();
});

写入 WASM 内存

javascript
// GBA 按键输入寄存器地址
const REG_KEYINPUT = 0x04000130;

function updateKeyInput() {
  // 直接写入 WASM 线性内存
  // 0x04000130 / 2 = Uint16Array 中的索引
  mem16[REG_KEYINPUT / 2] = keyState;
}

触摸控制实现

虚拟按钮 HTML 结构

html
<!-- 触摸控制的虚拟按钮 -->
<div id="touch-controls">
  <!-- 方向键 -->
  <button class="dpad-btn" data-key="UP">↑</button>
  <button class="dpad-btn" data-key="DOWN">↓</button>
  <button class="dpad-btn" data-key="LEFT">←</button>
  <button class="dpad-btn" data-key="RIGHT">→</button>

  <!-- 功能键 -->
  <button class="action-btn" data-key="A">A</button>
  <button class="action-btn" data-key="B">B</button>
  <button class="menu-btn" data-key="START">START</button>
  <button class="menu-btn" data-key="SELECT">SELECT</button>
</div>

触摸事件处理

javascript
const TOUCH_KEY_MAP = {
  'A':      KEY_A,
  'B':      KEY_B,
  'START':  KEY_START,
  'SELECT': KEY_SELECT,
  'UP':     KEY_UP,
  'DOWN':   KEY_DOWN,
  'LEFT':   KEY_LEFT,
  'RIGHT':  KEY_RIGHT,
};

function setupTouchControls() {
  const buttons = document.querySelectorAll('[data-key]');

  buttons.forEach(btn => {
    const keyName = btn.dataset.key;
    const mask = TOUCH_KEY_MAP[keyName];

    // 触摸开始
    btn.addEventListener('touchstart', (e) => {
      e.preventDefault();
      keyState &= ~mask;
      btn.classList.add('pressed');  // 视觉反馈
      updateKeyInput();
    });

    // 触摸结束
    btn.addEventListener('touchend', (e) => {
      e.preventDefault();
      keyState |= mask;
      btn.classList.remove('pressed');
      updateKeyInput();
    });

    // 触摸离开按钮区域
    btn.addEventListener('touchcancel', (e) => {
      e.preventDefault();
      keyState |= mask;
      btn.classList.remove('pressed');
      updateKeyInput();
    });
  });
}

Gamepad API 集成

javascript
// 手柄按钮到 GBA 按键的映射
const GAMEPAD_MAP = {
  0: KEY_A,        // A / Cross
  1: KEY_B,        // B / Circle
  2: KEY_SELECT,   // X / Square (用于 Select)
  3: KEY_START,    // Y / Triangle (用于 Start)
  4: KEY_L,        // L1
  5: KEY_R,        // R1
  8: KEY_SELECT,   // Select
  9: KEY_START,    // Start
};

function pollGamepad() {
  const gamepads = navigator.getGamepads();
  if (!gamepads) return;

  for (const gp of gamepads) {
    if (!gp) continue;

    // 从按键状态开始(保留键盘/触摸的状态)
    let gpState = 0x03FF;

    // 处理按钮
    for (const [index, mask] of Object.entries(GAMEPAD_MAP)) {
      if (gp.buttons[index]?.pressed) {
        gpState &= ~mask;
      }
    }

    // 处理摇杆(带死区)
    const deadzone = 0.2;
    if (gp.axes[0] < -deadzone) gpState &= ~KEY_LEFT;
    if (gp.axes[0] > deadzone)  gpState &= ~KEY_RIGHT;
    if (gp.axes[1] < -deadzone) gpState &= ~KEY_UP;
    if (gp.axes[1] > deadzone)  gpState &= ~KEY_DOWN;

    // 合并手柄状态与键盘/触摸状态
    keyState = keyState & gpState;  // 取交集:任一源按下即为按下
    break;  // 只处理第一个连接的手柄
  }

  updateKeyInput();
}

关键函数索引

函数名所在文件说明
handleKeyDown(e)app.js键盘按下事件处理
handleKeyUp(e)app.js键盘释放事件处理
updateKeyInput()app.js将 keyState 写入 WASM 内存的 REG_KEYINPUT
setupTouchControls()app.js初始化触摸虚拟按钮
pollGamepad()app.js轮询手柄状态并更新按键

相关阅读