Skip to content

输入系统练习 (Input System Exercises)

练习 1:手动计算 REG_KEYINPUT 的值

题目: GBA 的 REG_KEYINPUT 使用低电平有效逻辑。请计算以下场景中 0x04000130 地址的 16 位值:

  1. 没有任何按键被按下
  2. 同时按下 A 和 B
  3. 同时按下 方向右 和 Start
  4. 同时按下 L、R 和 方向下

按键掩码参考:A=0x0001, B=0x0002, Select=0x0004, Start=0x0008, Right=0x0010, Left=0x0020, Up=0x0040, Down=0x0080, R=0x0100, L=0x0200

参考答案

REG_KEYINPUT 的初始值为 0x03FF(所有按键未按下)。按下某键时,对应位清零(即与掩码取反后做 AND 操作)。

  1. 无按键按下0x03FF — 所有位为 1
  2. A + B0x03FF & ~0x0001 & ~0x0002 = 0x03FF & 0x03FF & 0x03FF = 0x03FF & 0x03FC = 0x03FC
  3. 右 + Start0x03FF & ~0x0010 & ~0x0008 = 0x03FF & 0xFFEF & 0xFFF7 = 0x03E7
  4. L + R + 下0x03FF & ~0x0200 & ~0x0100 & ~0x0080 = 0x03FF & 0x03FF & 0x03FF & 0x03FF0x03FF & 0xFDFF & 0xFEFF & 0xFF7F = 0xFC7F

验证方法:将值的二进制与掩码对比,0 的位即为被按下的按键。

练习 2:设计自定义键盘映射方案

题目: 当前默认映射为 Z=A, X=B, 方向键=方向。现在需要支持 WASD 方向 + JK 功能键 的替代映射方案:

  1. 设计新的映射表,使用 W/A/S/D 控制方向,J/K 控制 A/B
  2. 写出按键状态更新的伪代码,要求支持同时按下多个键
  3. 考虑一个问题:如果用户同时按住 ArrowUpKeyW,会发生什么?应该如何处理?
参考答案
  1. 新映射表:

    KeyW → Up     (0x0040)    KeyA → Left   (0x0020)
    KeyS → Down   (0x0080)    KeyD → Right  (0x0010)
    KeyJ → A      (0x0001)    KeyK → B      (0x0002)
    KeyU → L      (0x0200)    KeyI → R      (0x0100)
    Enter → Start (0x0008)    Backspace → Select (0x0004)
  2. 伪代码:

    javascript
    let keyState = 0x03FF;  // 初始:无按键按下
    
    const KEY_MAP = {
      'KeyW': 0x0040, 'KeyA': 0x0020, 'KeyS': 0x0080, 'KeyD': 0x0010,
      'KeyJ': 0x0001, 'KeyK': 0x0002, 'KeyU': 0x0200, 'KeyI': 0x0100,
      'Enter': 0x0008, 'Backspace': 0x0004,
      // 保留原始方向键映射
      'ArrowUp': 0x0040, 'ArrowDown': 0x0080,
      'ArrowLeft': 0x0020, 'ArrowRight': 0x0010
    };
    
    document.addEventListener('keydown', (e) => {
      const mask = KEY_MAP[e.code];
      if (mask) {
        keyState &= ~mask;   // 清除对应位 = 按键按下
        e.preventDefault();
      }
    });
    
    document.addEventListener('keyup', (e) => {
      const mask = KEY_MAP[e.code];
      if (mask) {
        keyState |= mask;    // 设置对应位 = 按键释放
        e.preventDefault();
      }
    });
  3. ArrowUpKeyW 映射到同一个掩码 0x0040。由于使用位运算:

    • 按下 ArrowUpkeyState &= ~0x0040 → 位清零
    • 再按下 KeyWkeyState &= ~0x0040 → 位仍然为零(无变化)
    • 释放 ArrowUpkeyState |= 0x0040 → 位变为一(但 KeyW 还按着!)

    问题:一个键释放会取消另一个同类键的状态。解决方案是使用引用计数按 keycode 独立追踪

    javascript
    const pressedKeys = new Set();  // 追踪当前物理按下的键
    
    document.addEventListener('keydown', (e) => {
      const mask = KEY_MAP[e.code];
      if (mask && !pressedKeys.has(e.code)) {
        pressedKeys.add(e.code);
        keyState &= ~mask;
      }
    });
    
    document.addEventListener('keyup', (e) => {
      const mask = KEY_MAP[e.code];
      if (mask) {
        pressedKeys.delete(e.code);
        // 检查是否还有其他键映射到同一掩码
        const stillPressed = [...pressedKeys].some(k => KEY_MAP[k] === mask);
        if (!stillPressed) {
          keyState |= mask;
        }
      }
    });

练习 3:实现简单的手柄死区 (Deadzone)

题目: Gamepad API 的摇杆返回浮点数值(axes[0]axes[1]),范围 -1.0 到 1.0。由于模拟摇杆的物理特性,静止时值可能不为精确的 0(例如 0.03)。

  1. 解释什么是"死区",为什么需要它
  2. 实现一个死区函数:当摇杆偏移量小于阈值时视为零
  3. 扩展:实现"方圆映射"(square-to-circle mapping),使对角线移动速度与直线一致
参考答案
  1. 死区是摇杆偏移的一个范围,在此范围内的值被忽略(视为零)。需要死区的原因:

    • 消除摇杆漂移(drift):手柄老化或制造误差导致静止时读数偏离零点
    • 防止意外输入:微小触碰不应触发角色移动
    • 提升操控感:用户需要有明确的"推动"动作才触发响应
  2. 简单死区实现:

    javascript
    function applyDeadzone(value, deadzone = 0.2) {
      if (Math.abs(value) < deadzone) {
        return 0;
      }
      // 将死区外的值重新映射到 0~1 范围
      return Math.sign(value) * (Math.abs(value) - deadzone) / (1 - deadzone);
    }
    
    // 使用
    const xAxis = applyDeadzone(gamepad.axes[0], 0.2);
    const yAxis = applyDeadzone(gamepad.axes[1], 0.2);
    
    if (xAxis < -0.5) keyState &= ~0x0020; // 左
    else keyState |= 0x0020;
    if (xAxis > 0.5) keyState &= ~0x0010;  // 右
    else keyState |= 0x0010;
    // ... 同理处理 Y 轴
  3. 方圆映射:摇杆物理移动范围是正方形(X 和 Y 独立的 -1~1),但理想情况应为圆形(所有方向等距)。

    javascript
    function applyCircleMapping(x, y) {
      // 检查是否在圆外
      const magnitude = Math.sqrt(x * x + y * y);
      if (magnitude > 1.0) {
        // 缩放到单位圆上
        x /= magnitude;
        y /= magnitude;
      }
      return { x, y };
    }
    
    // 组合使用
    let x = applyDeadzone(gamepad.axes[0]);
    let y = applyDeadzone(gamepad.axes[1]);
    const mapped = applyCircleMapping(x, y);

    这确保对角线方向(如 0.707, 0.707)的移动速度与正方向(1.0, 0.0)一致。

拓展挑战

  • 挑战 1:实现一个可自定义的按键映射系统,允许用户在运行时通过 UI 界面重新绑定按键。将配置保存到 localStorage
  • 挑战 2:为触摸控制添加长按检测——当方向键被持续按住超过 200ms 时,触发快速重复输入(模拟 GBA 的按键重复行为)。
  • 挑战 3:研究 KeyboardEvent.codeKeyboardEvent.key 的区别。为什么映射时应该使用 code 而非 key?在不同键盘布局(QWERTY、AZERTY、DVORAK)下会有什么差异?
  • 挑战 4:使用 Web Haptics API(navigator.vibrate())在移动设备上为虚拟按键提供触觉反馈。研究哪些浏览器支持此 API。