一个只在别人电脑上出现的 bug
你的网页游戏在自己机器上跑得很顺,WASD 控制流畅。发给一位法国同事试玩,他一按「上」,角色却往左走。代码没改过,浏览器也一样,差别只有一个:他用的是 AZERTY 键盘。
问题的根源在于判断按键时用了 e.key。
key 与 code 描述的不是同一件事
e.key 给出的是这次按键在当前键盘布局下产生的字符。AZERTY 键盘左上角第一个字母键上印着 A,所以按下去 e.key 是 "a";而 QWERTY 键盘同一位置印着 Q,e.key 是 "q"。
e.code 给出的是物理位置。不管键盘上印什么字母,左上角第一个字母键的 code 永远是 "KeyQ"。
document.addEventListener('keydown', (e) => {
console.log(e.key, e.code);
// AZERTY 用户按左上角第一键 → "a" "KeyQ"
});
于是用 key 判断 WASD 的代码,在 AZERTY 用户那里就完全错位了:他想按的是「印着 W 的那个键」,代码收到的却是别的字符。
什么时候该关心物理位置
核心判断标准是:用户的操作对象是「键位」还是「字符」?
游戏控制关心键位。玩家记住的是手指放在哪、按下哪个位置,就像手柄上的按钮一样。WASD 之所以能用,是因为它形成了符合手感的几何形状,而不是因为 W、A、S、D 这四个字母有什么特殊含义。这种场景必须用 code。
文本输入关心字符。用户在搜索框里敲字、在聊天框里打字,他要的是「我输入了 a」这个语义结果。这种场景应该用 key,甚至更好的做法是直接监听 input 事件,让浏览器处理输入法与组合键的全部细节。
code 的命名规则
code 遵循 W3C UI Events 规范,命名有固定规律:
| 键位 | code | 说明 |
|---|---|---|
| 字母键 | KeyA … KeyZ | 按 QWERTY 位置命名 |
| 主键盘数字 | Digit1 … Digit0 | 不含小键盘 |
| 小键盘数字 | Numpad1 … Numpad0 | 与主键盘区分开 |
| 方向键 | ArrowUp / ArrowDown / ArrowLeft / ArrowRight | |
| 功能键 | F1 … F24 | |
| 空格 / 回车 | Space / Enter |
字母键一律按 QWERTY 位置命名,这正是 code 能在不同布局间保持一致的原因。小键盘与主键盘的数字键 code 不同,需要区分时正好用得上。
别再用 keyCode 做游戏
老教程里常见的 if (e.keyCode === 87) 这类写法,今天应该彻底放弃:
keyCode从未被标准化,不同浏览器、不同事件类型(keydown与keypress)给出的值可能不同;- 它同样与布局相关,解决不了 AZERTY 的问题;
- 多媒体键等新按键根本没有分配码值,一律返回 0。
长按连发要自己处理
浏览器在按键持续按住时会自动重复触发 keydown,这是操作系统层面的连发。对移动类控制来说这通常不是你要的效果——角色会以不可控的频率加速。
常见做法是维护一个按下状态集合,在渲染循环里读取:
const pressed = new Set();
addEventListener('keydown', e => pressed.add(e.code));
addEventListener('keyup', e => pressed.delete(e.code));
function loop() {
const speed = 4;
if (pressed.has('KeyW')) y -= speed;
if (pressed.has('KeyS')) y += speed;
if (pressed.has('KeyA')) x -= speed;
if (pressed.has('KeyD')) x += speed;
requestAnimationFrame(loop);
}
这样移动速度由帧率决定,与键盘连发的重复频率无关,手感才稳定,斜向移动也更容易做归一化处理。
一个容易忽略的收尾
切出页面或失去焦点时,keyup 可能永远收不到,状态集合里会残留按下的键,导致回到页面后角色一直朝某个方向跑。记得监听 blur 清空集合:
addEventListener('blur', () => pressed.clear());
检查清单
- 涉及方向、移动、键位判定的逻辑,是否统一用了
code - 是否已清除代码中所有
keyCode比较 - 长按是否改为状态集合加每帧读取,而不是依赖
keydown重复触发 - 是否在
blur时清理按键状态 - 是否只在真正需要时调用
preventDefault(),避免拦掉用户的方向键滚动或 Ctrl 组合键