上篇文章 中,我们已经构建了 GameBoy 模拟器的基础架构,包括 CPU 指令处理、内存管理、GPU 时序和图形渲染系统。虽然我们的模拟器能够显示图像,但还无法与用户进行交互。这篇文章将继续完善该模拟器。

6. 输入系统:让玩家与游戏交互

与现代游戏手柄复杂的按键布局不同,GameBoy 的输入系统相对简单但非常巧妙。它只有 8 个按键,但通过矩阵式的硬件设计,能够有效地检测任意组合的按键状态。

6.1. GameBoy 输入硬件原理

GameBoy 的 8 个按键被组织为一个 2 列 × 4 行的矩阵:

列 1(功能键) 列 2(方向键)
A 按键 右方向键
B 按键 左方向键
Select 上方向键
Start 下方向键

这种矩阵设计的工作原理类似于键盘扫描:

  1. 列选择:CPU 通过写入 0xFF00 寄存器的第 4、5 位来选择要扫描的列

    • 写入 0x10 选择列 1(功能键)
    • 写入 0x20 选择列 2(方向键)
  2. 行读取:选择列后,CPU 读取 0xFF00 寄存器的低 4 位,获取该列中各行的按键状态

  3. 状态反转:硬件使用反转逻辑,即 未按下 = 1,按下 = 0

为什么用矩阵?

如果直接为每个按键分配一位,需要 8 位。但用 2×4 矩阵只需要 6 根线(2 列 + 4 行),节省了硬件成本。

这在 1989 年是很重要的优化,每减少一根线都能降低成本。

6.2. 输入控制器实现

在实现输入控制器之前,我们需要先设计完整的常量体系和架构。GameBoy 的输入系统虽然简单,但需要精确的位操作和状态管理。

输入系统的所有魔法数字都需要明确的常量定义:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 输入常量定义 - 每个数值都有其硬件意义
*/
const INPUT_CONSTANTS = {
// 寄存器地址
JOYPAD_REGISTER: 0xFF00, // GameBoy输入寄存器的内存地址

// 列选择位掩码 - 控制扫描哪一列按键
COLUMN_SELECT_MASK: 0x30, // 二进制: 00110000 - 提取第4、5位
COLUMN_1_SELECT: 0x10, // 二进制: 00010000 - 选择功能键列
COLUMN_2_SELECT: 0x20, // 二进制: 00100000 - 选择方向键列

// 行状态位掩码 - 各行按键的状态
ROW_MASK: 0x0F, // 二进制: 00001111 - 提取低4位行状态
ROW_0: 0x01, ROW_1: 0x02, // 第0、1行
ROW_2: 0x04, ROW_3: 0x08, // 第2、3行

// 默认状态:所有按键未按下
DEFAULT_ROW_STATE: 0x0F // 所有行为高电平(未按下状态)
};

首先创建输入控制器的核心类:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* GameBoy 输入控制器
* 管理键盘输入矩阵和寄存器交互
*/
class GameBoyInputController {
constructor() {
this.initializeInputMatrix();
this.initializeKeyMapping();
this.initializeEventListeners();
}

/**
* 初始化输入矩阵
* 2 列 × 4 行的按键矩阵,每列用一个字节表示
*/
initializeInputMatrix() {
// 按键状态矩阵:[列1, 列2]
// 每列 4 位,每位代表一行的状态
// 0 = 按键按下,1 = 按键未按下
this.keyMatrix = [
INPUT_CONSTANTS.DEFAULT_ROW_STATE, // 列1:功能键列
INPUT_CONSTANTS.DEFAULT_ROW_STATE // 列2:方向键列
];

// 当前选择的列
this.selectedColumn = 0;

// 按键状态缓存(用于调试)
this.pressedKeys = new Set();
}
}

按键映射定义了 JavaScript 键码到 GameBoy 按键的对应关系:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 按键映射常量
*/
const KEY_MAPPING = {
// 功能键(列1)
A_BUTTON: { column: 0, row: 0, keyCode: 90, name: 'A', key: 'Z' }, // Z键
B_BUTTON: { column: 0, row: 1, keyCode: 88, name: 'B', key: 'X' }, // X键
SELECT: { column: 0, row: 2, keyCode: 32, name: 'Select', key: 'Space' }, // 空格键
START: { column: 0, row: 3, keyCode: 13, name: 'Start', key: 'Enter' }, // 回车键

// 方向键(列2)
RIGHT: { column: 1, row: 0, keyCode: 39, name: 'Right', key: '→' }, // 右箭头
LEFT: { column: 1, row: 1, keyCode: 37, name: 'Left', key: '←' }, // 左箭头
UP: { column: 1, row: 2, keyCode: 38, name: 'Up', key: '↑' }, // 上箭头
DOWN: { column: 1, row: 3, keyCode: 40, name: 'Down', key: '↓' } // 下箭头
};

这个映射表的设计考虑了几个重要因素:

  • 人体工程学:A/B 键放在左手位置(Z/X),方向键用标准箭头
  • 避免冲突:避开常用的网页快捷键(如 Ctrl + C 等)
  • 易于记忆:功能键集中,方向键直观

输入控制器需要将浏览器的键盘事件转换为 GameBoy 格式:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 初始化键盘事件监听
* 建立浏览器事件与GameBoy输入系统的桥梁
*/
initializeKeyMapping() {
// 创建keyCode到按键信息的快速查找表
this.keyCodeMap = {};

Object.values(KEY_MAPPING).forEach(keyInfo => {
this.keyCodeMap[keyInfo.keyCode] = keyInfo;
});

console.log('🗝️ 按键映射已初始化');
}

initializeEventListeners() {
// 绑定全局键盘事件
document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this));

// 防止箭头键等按键的默认行为(如页面滚动)
document.addEventListener('keydown', this.preventDefaultKeys.bind(this));
}

键盘事件处理是输入系统的核心,需要正确实现状态反转逻辑:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 处理按键按下事件
*/
handleKeyDown(event) {
const keyInfo = this.keyCodeMap[event.keyCode];

if (!keyInfo || this.pressedKeys.has(event.keyCode)) {
return; // 不是我们关心的按键或重复按键
}

// 记录按键状态
this.pressedKeys.add(event.keyCode);

// 更新矩阵状态:按下时对应位设为 0
const rowMask = ~(1 << keyInfo.row); // 创建掩码,对应位为0
this.keyMatrix[keyInfo.column] &= rowMask;

if (window.DEBUG_MODE) {
console.log(`🎯 按键按下: ${keyInfo.name} - 列${keyInfo.column}${keyInfo.row}`);
}
}

/**
* 处理按键释放事件
*/
handleKeyUp(event) {
const keyInfo = this.keyCodeMap[event.keyCode];

if (!keyInfo || !this.pressedKeys.has(event.keyCode)) {
return;
}

// 移除按键状态
this.pressedKeys.delete(event.keyCode);

// 更新矩阵状态:释放时对应位设为 1
const rowMask = 1 << keyInfo.row;
this.keyMatrix[keyInfo.column] |= rowMask;
}

/**
* 防止按键默认行为
*/
preventDefaultKeys(event) {
const keyInfo = this.keyCodeMap[event.keyCode];
if (keyInfo) {
event.preventDefault(); // 阻止箭头键滚动页面等
}
}

/**
* 统计和调试支持
*/
initializeDebugInfo() {
this.stats = {
keyPressCount: 0,
keyReleaseCount: 0,
lastKeyPressed: null,
registerReads: 0,
registerWrites: 0
};
}

6.2.1. 位操作详解:状态反转的数值示例

GameBoy 的状态反转逻辑是整个输入系统的核心。让我们通过具体的数值示例来理解这个过程:

初始状态(所有按键未按下):

1
2
3
4
5
列1(功能键): keyMatrix[0] = 0x0F = 0b00001111
列2(方向键): keyMatrix[1] = 0x0F = 0b00001111

位含义:[bit3=Start, bit2=Select, bit1=B, bit0=A]
[bit3=Down, bit2=Up, bit1=Left, bit0=Right]

示例 1:按下 A 键(列 0 行 0)

1
2
3
4
5
6
1. 按键信息:A_BUTTON = { column: 0, row: 0, ... }
2. 创建行掩码:rowMask = ~(1 << 0) = ~0b00000001 = 0b11111110 = 0xFE
3. 应用掩码:keyMatrix[0] &= 0xFE
原值:0x0F = 0b00001111
掩码:0xFE = 0b11111110
结果:0x0E = 0b00001110 ← A 键现在为 0(按下状态)

示例 2:按下方向键 Up(列 1 行 2)

1
2
3
4
5
6
1. 按键信息:UP = { column: 1, row: 2, ... }
2. 创建行掩码:rowMask = ~(1 << 2) = ~0b00000100 = 0b11111011 = 0xFB
3. 应用掩码:keyMatrix[1] &= 0xFB
原值:0x0F = 0b00001111
掩码:0xFB = 0b11111011
结果:0x0B = 0b00001011 ← Up 键现在为 0(按下状态)

示例 3:同时按下 A 键和 B 键

1
2
3
4
5
6
7
8
9
10
11
初始:keyMatrix[0] = 0x0F = 0b00001111

按下A键:
rowMask = ~(1 << 0) = 0xFE = 0b11111110
keyMatrix[0] = 0x0F & 0xFE = 0x0E = 0b00001110

按下B键:
rowMask = ~(1 << 1) = 0xFD = 0b11111101
keyMatrix[0] = 0x0E & 0xFD = 0x0C = 0b00001100

最终状态:0b00001100 ← A 和 B 键都为 0(都被按下)

释放按键的过程(以释放 A 键为例):

1
2
3
4
5
6
1. 当前状态:keyMatrix[0] = 0x0C = 0b00001100 (A和B都按下)
2. 创建恢复掩码:rowMask = 1 << 0 = 0b00000001 = 0x01
3. 应用或操作:keyMatrix[0] |= 0x01
当前:0x0C = 0b00001100
掩码:0x01 = 0b00000001
结果:0x0D = 0b00001101 ← A 键恢复为 1(未按下),B 键保持 0(仍按下)

关键理解点:

  1. 按下操作:使用 &= 和反转掩码将指定位设为 0

    • ~(1 << row) 创建只有目标位为 0 的掩码
    • 与运算确保只有目标位被清零
  2. 释放操作:使用 |= 和正常掩码将指定位设为 1

    • (1 << row) 创建只有目标位为 1 的掩码
    • 或运算确保只有目标位被置位
  3. 组合按键:多个按键可以同时处于按下状态,每个按键独立管理其对应的位

这种设计让 GameBoy 能够准确检测任意按键组合,为复杂的游戏操作提供了基础。

为什么是反转的?

GameBoy 使用 "低电平有效" 的硬件设计:

  • 按键未按下时,该位为高电平(1)
  • 按键按下时,该位被拉低(0)

6.3. 寄存器接口实现

输入控制器的最终目的是为 CPU 提供符合 GameBoy 硬件规范的寄存器接口。CPU 通过读写 0xFF00 地址来获取按键状态和控制扫描模式。

输入控制器必须提供符合 GameBoy 硬件规范的寄存器接口:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 读取游戏手柄寄存器
* 根据当前选择的列返回对应的按键状态
*/
readJoypadRegister() {
let result = 0xC0; // 高2位始终为1(未使用)

// 添加列选择位
result |= this.selectedColumn;

// 根据选择的列返回对应的行状态
switch (this.selectedColumn) {
case INPUT_CONSTANTS.COLUMN_1_SELECT:
// 功能键列
result |= this.keyMatrix[0] & INPUT_CONSTANTS.ROW_MASK;
break;

case INPUT_CONSTANTS.COLUMN_2_SELECT:
// 方向键列
result |= this.keyMatrix[1] & INPUT_CONSTANTS.ROW_MASK;
break;

default:
// 没有选择列,返回所有行为高电平
result |= INPUT_CONSTANTS.ROW_MASK;
break;
}

return result;
}

/**
* 写入游戏手柄寄存器
* 主要用于选择要读取的列
*/
writeJoypadRegister(value) {
// 提取列选择位
const columnSelect = value & INPUT_CONSTANTS.COLUMN_SELECT_MASK;

if (columnSelect !== this.selectedColumn) {
this.selectedColumn = columnSelect;

if (window.DEBUG_MODE) {
const columnName = columnSelect === INPUT_CONSTANTS.COLUMN_1_SELECT ? '功能键' :
columnSelect === INPUT_CONSTANTS.COLUMN_2_SELECT ? '方向键' : '无';
console.log(`✏️ 选择${columnName}列`);
}
}
}

这些函数是整个输入系统的核心,实现了完整的矩阵扫描逻辑:

  1. 高位设置0xC0 设置第 6、7 位为 1(硬件规范要求)
  2. 列选择回显:将当前选择的列值加入结果
  3. 行状态返回:根据选择的列返回对应的 4 位行状态

矩阵扫描的工作流程:

0x10
0x20
其他
CPU 写入列选择
选择哪一列?
返回功能键状态
返回方向键状态
返回全高电平
CPU 读取按键状态

6.4. MMU 集成

在 MMU 中完善输入寄存器的处理逻辑:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 读取I/O寄存器 - 支持硬件组件路由
*/
readIORegister(address) {
// 🎮 按键输入寄存器 (0xFF00) - 第6章实现 ✅ 已完成
if (address === IO_REGISTERS.JOYPAD) {
if (this.inputController && typeof this.inputController.readJoypadRegister === 'function') {
return this.inputController.readJoypadRegister();
} else {
// 如果输入控制器未连接,返回默认值(所有按键未按下)
return 0xFF; // 默认:所有按键未按下
}
}

// ... 其他寄存器处理 ...
}

/**
* 写入I/O寄存器 - 支持硬件组件路由
*/
writeIORegister(address, value) {
// 🎮 按键输入寄存器 (0xFF00) - 第6章实现 ✅ 已完成
if (address === IO_REGISTERS.JOYPAD) {
if (this.inputController && typeof this.inputController.writeJoypadRegister === 'function') {
this.inputController.writeJoypadRegister(value);
}
// 保存到本地数组
this.ioRegisters[address - MEMORY_REGIONS.IO_START] = value;
return;
}

// ... 其他寄存器处理 ...
}

/**
* 获取I/O寄存器状态 (调试用)
* @returns {Object} I/O寄存器状态
*/
getIORegisterStatus() {
const status = {
gpu: {
lcdc: this.readByte(IO_REGISTERS.LCDC).toString(16).padStart(2, '0'),
stat: this.readByte(IO_REGISTERS.STAT).toString(16).padStart(2, '0'),
scy: this.readByte(IO_REGISTERS.SCY),
scx: this.readByte(IO_REGISTERS.SCX),
ly: this.readByte(IO_REGISTERS.LY),
bgp: this.readByte(IO_REGISTERS.BGP).toString(16).padStart(2, '0')
},
input: {
joypad: this.readByte(IO_REGISTERS.JOYPAD).toString(16).padStart(2, '0')
},
interrupts: {
ie: this.readByte(IO_REGISTERS.IE).toString(16).padStart(2, '0'),
if: this.readByte(IO_REGISTERS.IF).toString(16).padStart(2, '0')
}
};

// 如果输入控制器可用,添加详细的输入状态
if (this.inputController) {
status.input.detailed = this.inputController.getInputStatus();
}

return status;
}

6.5. 系统集成

在主系统中创建和连接输入控制器:

gameboy.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 创建硬件组件实例
*/
async createHardwareComponents() {
// ...

// 创建输入控制器
this.inputController = new GameBoyInputController();
console.log('✅ 输入控制器已创建');

// 设置全局引用(兼容性)
window.InputController = this.inputController;
}

/**
* 连接硬件组件
*/
connectComponents() {
// ...

// 连接输入控制器到 MMU
this.mmu.connectHardware('input', this.inputController);

console.log('🔗 硬件组件连接完成');
}

/**
* 检查必要的类是否已加载
*/
checkRequiredClasses() {
const requiredClasses = [
// ...
{ name: 'GameBoyInputController', class: window.GameBoyInputController }
];

// ...
}

/**
* 系统重置
*/
reset() {
try {
// ...

if (this.inputController) this.inputController.reset();

// ...
} catch (error) {
// ...
}
}

/**
* 获取系统状态
* @returns {Object} 系统状态对象
*/
getStatus() {
const status = {
// ...
components: {
// ...
input: this.inputController ? this.inputController.getInputStatus() : '未初始化'
}
};
}

/**
* 获取调试信息
* @returns {string} 格式化的调试信息
*/
getDebugInfo() {
// ...

// 输入控制器状态
if (this.inputController) {
debugInfo += `\n\n🎮 输入控制器状态:\n${this.inputController.getDebugInfo()}`;
}

// ...
}

/**
* 销毁系统(清理资源)
*/
destroy() {
// ...
this.inputController = null;

// ...
if (window.inputController) delete window.InputController;
}

6.6. 调试和测试工具

输入系统包含了完整的调试功能:

input.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 获取完整的输入状态(调试专用)
*/
getInputStatus() {
const pressedKeyNames = Array.from(this.pressedKeys).map(keyCode => {
const keyInfo = this.keyCodeMap[keyCode];
return keyInfo ? keyInfo.name : `Unknown(${keyCode})`;
});

return {
selectedColumn: this.selectedColumn,
keyMatrix: [...this.keyMatrix], // 复制数组避免外部修改
pressedKeys: pressedKeyNames,
stats: { ...this.stats } // 复制对象
};
}

/**
* 获取按键映射信息(用于UI显示)
*/
getKeyMappings() {
return Object.values(KEY_MAPPING).map(keyInfo => ({
gameboyKey: keyInfo.name,
keyboardKey: keyInfo.key,
keyCode: keyInfo.keyCode,
isPressed: this.pressedKeys.has(keyInfo.keyCode)
}));
}

/**
* 模拟按键操作(测试专用)
*/
simulateKeyPress(keyName) {
const keyInfo = Object.values(KEY_MAPPING).find(info => info.name === keyName);
if (keyInfo) {
this.handleKeyDown({ keyCode: keyInfo.keyCode, preventDefault: () => {} });
}
}

simulateKeyRelease(keyName) {
const keyInfo = Object.values(KEY_MAPPING).find(info => info.name === keyName);
if (keyInfo) {
this.handleKeyUp({ keyCode: keyInfo.keyCode });
}
}

/**
* 获取调试信息字符串
*/
getDebugInfo() {
const status = this.getInputStatus();
const mappings = this.getKeyMappings();

let debugInfo = `🎮 输入控制器状态:
选择列: 0x${status.selectedColumn.toString(16).padStart(2, '0')}
矩阵状态: [0x${status.keyMatrix[0].toString(16)}, 0x${status.keyMatrix[1].toString(16)}]
当前按下: ${status.pressedKeys.join(', ') || '无'}

📊 统计信息:
按键次数: ${status.stats.keyPressCount}
释放次数: ${status.stats.keyReleaseCount}
寄存器读取: ${status.stats.registerReads}
寄存器写入: ${status.stats.registerWrites}

🗝️ 按键映射:`;

mappings.forEach(mapping => {
const statusIcon = mapping.isPressed ? '🔴' : '⚪';
debugInfo += `\n${statusIcon} ${mapping.gameboyKey}: ${mapping.keyboardKey}`;
});

return debugInfo;
}

自动化测试功能可以验证所有按键:

gameboy.JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 测试输入系统(调试用)
*/
testInputSystem() {
if (!this.inputController) {
console.warn('⚠️ 输入控制器未初始化');
return;
}

console.log('🧪 开始输入系统测试...');

// 测试各个按键
const testKeys = ['A', 'B', 'Start', 'Select', 'Up', 'Down', 'Left', 'Right'];

testKeys.forEach((keyName, index) => {
setTimeout(() => {
this.inputController.simulateKeyPress(keyName);

setTimeout(() => {
this.inputController.simulateKeyRelease(keyName);
}, 200);
}, index * 500);
});
}

6.7. 用户界面完善

为了提高用户体验,需要实现一个可视化的虚拟手柄。虚拟手柄的核心在于将触摸 / 鼠标事件转换为 GameBoy 按键事件::

index.HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!-- 输入控制 -->
<div class="controls">
<!-- 虚拟按键显示 -->
<div class="control-section">
<div class="control-title">🕹️ 虚拟手柄</div>
<div class="virtual-gamepad" id="virtual-gamepad">
<!-- 方向键 -->
<div class="dpad-container">
<div class="dpad">
<div class="dpad-button dpad-up" data-key="Up"></div>
<div class="dpad-middle">
<div class="dpad-button dpad-left" data-key="Left"></div>
<div class="dpad-center"></div>
<div class="dpad-button dpad-right" data-key="Right"></div>
</div>
<div class="dpad-button dpad-down" data-key="Down"></div>
</div>
<div class="dpad-label">方向键</div>
</div>

<!-- 功能键 -->
<div class="action-buttons">
<div class="button-row">
<div class="action-button" data-key="Select">SELECT</div>
<div class="action-button" data-key="Start">START</div>
</div>
<div class="button-row">
<div class="action-button action-b" data-key="B">B</div>
<div class="action-button action-a" data-key="A">A</div>
</div>
</div>
</div>
</div>
</div>

<!-- 调试和图形控制 -->
<div class="controls"> ... </div>

样式:

index.HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
<style>
/* 虚拟手柄样式 */
.virtual-gamepad {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: linear-gradient(145deg, #5a6b4d, #6a7b5d);
border-radius: 15px;
margin: 10px 0;
min-height: 120px;
}

.dpad-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}

.dpad {
display: grid;
grid-template-areas:
". up ."
"left center right"
". down .";
grid-template-columns: 30px 30px 30px;
grid-template-rows: 30px 30px 30px;
gap: 2px;
}

.dpad-button {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #7a8a6d, #6a7a5d);
border-radius: 5px;
font-size: 16px;
font-weight: bold;
color: #2c5530;
cursor: pointer;
transition: all 0.1s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
user-select: none;
}

.dpad-button:hover {
background: linear-gradient(145deg, #8a9a7d, #7a8a6d);
}

.dpad-button.pressed {
background: linear-gradient(145deg, #5a7c4a, #4a6c3a);
color: #fff;
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}

.dpad-up { grid-area: up; }
.dpad-down { grid-area: down; }
.dpad-left { grid-area: left; }
.dpad-right { grid-area: right; }

.dpad-middle {
grid-area: center;
display: flex;
align-items: center;
}

.dpad-center {
width: 30px;
height: 30px;
background: linear-gradient(145deg, #4a5c3a, #3a4c2a);
border-radius: 50%;
}

.dpad-label {
font-size: 10px;
color: #6b7a5e;
text-align: center;
}

.action-buttons {
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
}

.button-row {
display: flex;
gap: 15px;
}

.action-button {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
color: #2c5530;
cursor: pointer;
transition: all 0.1s ease;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
user-select: none;
background: linear-gradient(145deg, #7a8a6d, #6a7a5d);
}

.action-button:hover {
background: linear-gradient(145deg, #8a9a7d, #7a8a6d);
}

.action-button.pressed {
background: linear-gradient(145deg, #5a7c4a, #4a6c3a);
color: #fff;
transform: translateY(2px);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

.action-a {
background: linear-gradient(145deg, #a56565, #955555);
color: #fff;
}

.action-a:hover {
background: linear-gradient(145deg, #b57575, #a56565);
}

.action-a.pressed {
background: linear-gradient(145deg, #854545, #753535);
}

.action-b {
background: linear-gradient(145deg, #5a8a5a, #4a7a4a);
color: #fff;
}

.action-b:hover {
background: linear-gradient(145deg, #6a9a6a, #5a8a5a);
}

.action-b.pressed {
background: linear-gradient(145deg, #3a6a3a, #2a5a2a);
}

/* 按键映射显示 */
.key-mapping-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}

.key-mapping-content {
background: linear-gradient(145deg, #8b956d, #9bb583);
border-radius: 20px;
padding: 30px;
max-width: 500px;
width: 90%;
max-height: 80%;
overflow-y: auto;
position: relative;
}

.key-mapping-title {
font-size: 20px;
font-weight: bold;
color: #2c5530;
text-align: center;
margin-bottom: 20px;
}

.key-mapping-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}

.key-mapping-table th,
.key-mapping-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #5a6b4d;
}

.key-mapping-table th {
background: linear-gradient(145deg, #7a8a6d, #6a7a5d);
color: #2c5530;
font-weight: bold;
}

.key-mapping-table td {
background: linear-gradient(145deg, #9bb583, #8b956d);
color: #2c5530;
}

.key-status {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}

.key-status.pressed {
background: #ff4444;
}

.key-status.released {
background: #44ff44;
}

.close-btn {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
font-size: 24px;
color: #2c5530;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}

.close-btn:hover {
background: rgba(0, 0, 0, 0.1);
border-radius: 50%;
}

/* 隐藏虚拟手柄 */
.virtual-gamepad.hidden {
display: none;
}

/* 响应式调整 */
@media (max-width: 768px) {
.virtual-gamepad {
flex-direction: column;
gap: 20px;
padding: 15px;
}

.action-button {
width: 40px;
height: 40px;
font-size: 10px;
}

.dpad-button {
font-size: 14px;
}
}
</style>

虚拟手柄支持鼠标和触摸操作:

index.HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script>
let virtualGamepadState = {
pressedButtons = new Set(),
mouseDown: false,
};

function handleVirtualButtonDown(event) {
event.preventDefault();

const button = event.currentTarget;
const keyName = button.dataset.key;

if (!keyName || virtualGamepadState.pressedButtons.has(keyName)) {
return;
}

// 添加视觉反馈
button.classList.add('pressed');
virtualGamepadState.pressedButtons.add(keyName);

// 模拟按键按下
simulateKeyPress(keyName);
}

function simulateKeyPress(keyName) {
if (!systemInstance) return;

try {
const system = window.GameBoySystemController.getInstance();
if (system && system.inputController) {
system.inputController.simulateKeyPress(keyName);
}
} catch (error) {
console.error(`模拟按键失败: ${error.message}`);
}
}
</script>

index.html 中添加用于测试和调试的板块:

index.HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 输入控制 -->
<div class="controls">
<!-- 输入测试 -->
<div class="control-section">
<div class="control-title">🎮 输入控制</div>
<button class="btn" id="test-input-btn" disabled>测试输入系统</button>
<button class="btn" id="show-input-btn" disabled>显示输入状态</button>
<button class="btn" id="show-keymapping-btn">显示按键映射</button>
<button class="btn" id="toggle-virtual-keys-btn">切换虚拟按键</button>
<div class="keyboard-hint">
快捷键:<span class="key">I</span> 输入状态 <span class="key">K</span> 按键映射
</div>
</div>

<!-- 虚拟按键显示 -->
<div class="control-section"> ... </div>
</div>

页面最底部添加按键映射弹窗:

index.HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- 按键映射弹窗 -->
<div class="key-mapping-overlay" id="key-mapping-overlay">
<div class="key-mapping-content">
<button class="close-btn" id="close-key-mapping">&times;</button>
<div class="key-mapping-title">🎮 GameBoy 按键映射</div>
<table class="key-mapping-table">
<thead>
<tr>
<th>状态</th>
<th>GameBoy 按键</th>
<th>键盘按键</th>
<th>功能</th>
</tr>
</thead>
<tbody id="key-mapping-tbody">
<!-- 动态生成内容 -->
</tbody>
</table>
<div class="info-box">
<h4>使用说明:</h4>
<p>• 使用键盘按键控制 GameBoy 游戏</p>
<p>• 绿点表示按键未按下,红点表示按键已按下</p>
<p>• 也可以点击虚拟手柄上的按钮进行控制</p>
<p>• 按 ESC 键可以关闭此窗口</p>
</div>
</div>
</div>
index.HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
<script>
// UI 元素引用
const elements = {
// ...

// 输入相关元素
testInputBtn: document.getElementById('test-input-btn'),
showInputBtn: document.getElementById('show-input-btn'),
showKeymappingBtn: document.getElementById('show-keymapping-btn'),
toggleVirtualKeysBtn: document.getElementById('toggle-virtual-keys-btn'),

// 虚拟手柄
virtualGamepad: document.getElementById('virtual-gamepad'),

// 按键映射弹窗
keyMappingOverlay: document.getElementById('key-mapping-overlay'),
keyMappingTbody: document.getElementById('key-mapping-tbody'),
closeKeyMappingBtn: document.getElementById('close-key-mapping')
}

// 更新按钮状态
function updateButtonStates(systemState) {
// ...

elements.testInputBtn.disabled = !isInitialized;
elements.showInputBtn.disabled = !isInitialized;
}

function initializeVirtualGamePad() {
const gamepad = elements.virtualGamepad;
if (!gamepad) return;

// 获取所有虚拟按钮
const buttons = gamepad.querySelectorAll('[data-key]');

buttons.forEach(button => {
// 鼠标事件
button.addEventListener('mousedown', handleVirtualButtonDown);
button.addEventListener('mouseup', handleVirtualButtonUp);
button.addEventListener('mouseleave', handleVirtualButtonUp);

// 触摸事件(移动设备支持)
button.addEventListener('touchstart', handleVirtualButtonDown);
button.addEventListener('touchend', handleVirtualButtonUp);
button.addEventListener('touchcancel', handleVirtualButtonUp);

// 防止默认行为
button.addEventListener('contextmenu', e => e.preventDefault());
button.addEventListener('selectstart', e => e.preventDefault());
});

console.log('🕹️ 虚拟手柄已初始化');
}

function handleVirtualButtonUp(event) {
event.preventDefault();

const button = event.currentTarget;
const keyName = button.dataset.key;

if (!keyName || !virtualGamepadState.pressedButtons.has(keyName)) {
return;
}

// 移除视觉反馈
button.classList.remove('pressed');
virtualGamepadState.pressedButtons.delete(keyName);

// 模拟按键释放
simulateKeyRelease(keyName);

if (window.DEBUG_MODE) {
console.log(`🔓 虚拟按钮释放: ${keyName}`);
}
}

function simulateKeyRelease(keyName) {
if (!systemInstance) return;

try {
const system = window.GameBoySystemController.getInstance();
if (system && system.inputController) {
system.inputController.simulateKeyRelease(keyName);
}
} catch (error) {
console.error(`模拟按键释放失败: ${error.message}`);
}
}

// 输入控制函数
function testInputSystem() {
if (!systemInstance) {
addLog('❌ 系统未初始化', 'error');
return;
}

try {
const system = window.GameBoySystemController.getInstance();
if (system && system.inputController) {
addLog('🧪 开始输入系统测试...', 'debug');
system.testInputSystem();
addLog('✅ 输入系统测试已启动,请查看控制台输出', 'success');
} else {
addLog('❌ 输入控制器未初始化', 'error');
}
} catch (error) {
addLog(`❌ 输入系统测试失败: ${error.message}`, 'error');
}
}

function showInputStatus() {
if (!systemInstance) {
addLog('❌ 系统未初始化', 'error');
return;
}

try {
const system = window.GameBoySystemController.getInstance();
if (system && system.inputController) {
addLog('🎮 === 输入系统状态 ===', 'debug');
addLog(system.inputController.getDebugInfo(), 'debug');
addLog('===================', 'debug');
} else {
addLog('❌ 输入控制器未初始化', 'error');
}
} catch (error) {
addLog(`❌ 获取输入状态失败: ${error.message}`, 'error');
}
}

function showKeyMapping() {
updateKeyMappingDisplay();
elements.keyMappingOverlay.style.display = 'flex';
}

function hideKeyMapping() {
elements.keyMappingOverlay.style.display = 'none';
}

function toggleVirtualKeys() {
const gamepad = elements.virtualGamepad;
const isHidden = gamepad.classList.contains('hidden');

if (isHidden) {
gamepad.classList.remove('hidden');
elements.toggleVirtualKeysBtn.textContent = '隐藏虚拟按键';
addLog('👀 虚拟按键已显示', 'info');
} else {
gamepad.classList.add('hidden');
elements.toggleVirtualKeysBtn.textContent = '显示虚拟按键';
addLog('🙈 虚拟按键已隐藏', 'info');
}
}

function updateKeyMappingDisplay() {
if (!systemInstance) return;

try {
const system = window.GameBoySystemController.getInstance();
if (!system || !system.inputController) return;

const mappings = system.inputController.getKeyMappings();
const tbody = elements.keyMappingTbody;

tbody.innerHTML = '';

mappings.forEach(mapping => {
const row = document.createElement('tr');
const statusClass = mapping.isPressed ? 'pressed' : 'released';
const statusText = mapping.isPressed ? '按下' : '未按下';

row.innerHTML = `
<td><span class="key-status ${statusClass}"></span>${statusText}</td>
<td><strong>${mapping.gameboyKey}</strong></td>
<td><kbd>${mapping.keyboardKey}</kbd></td>
<td>${getKeyFunction(mapping.gameboyKey)}</td>
`;

tbody.appendChild(row);
});
} catch (error) {
console.error('更新按键映射显示失败:', error);
}
}

function getKeyFunction(gameboyKey) {
const functions = {
'A': '确认/攻击',
'B': '取消/跳跃',
'Start': '开始/暂停',
'Select': '选择/切换',
'Up': '向上移动',
'Down': '向下移动',
'Left': '向左移动',
'Right': '向右移动'
};
return functions[gameboyKey] || '未知功能';
}

// 性能监控
function startPerformanceMonitoring() {
// ...

// 启动按键状态监控,每秒更新一次按键映射显示
setInterval(updateKeyMappingDisplay, 1000);
}

// 绑定事件监听器
function bindEventListeners() {
// ...

// 按键映射弹窗的ESC键关闭
elements.keyMappingOverlay.addEventListener('click', function(event) {
if (event.target === elements.keyMappingOverlay) {
hideKeyMapping();
}
});

// 初始化虚拟手柄
initializeVirtualGamepad();
}

// 键盘快捷键
document.addEventListener('keydown', function(event) {
// 防止在输入框中触发快捷键
if (event.target.tagName === 'INPUT') return;

// 如果按键映射弹窗打开,ESC键关闭
if (elements.keyMappingOverlay.style.display === 'flex' && event.code === 'Escape') {
event.preventDefault();
hideKeyMapping();
return;
}

switch(event.code) {
// ...

case 'KeyI':
event.preventDefault();
showInputStatus();
break;
case 'KeyK':
event.preventDefault();
showKeyMapping();
break;
case 'Escape':
event.preventDefault();
if (elements.keyMappingOverlay.style.display === 'flex') {
hideKeyMapping();
}
break;
}
});

// 页面加载完成后的初始化
window.addEventListener('load', function() {
bindEventListeners();

// 检查组件加载状态
setTimeout(() => {
const components = [
// ...
{ name: '输入控制器', check: () => typeof GameBoyInputController !== 'undefined' }
];
});

// ...
});
</script>