Appearance
输入系统代码导读 (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 | 轮询手柄状态并更新按键 |