游戏控制为什么必须用 code 而不是 key

工具相关 ·

一个只在别人电脑上出现的 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 组合键
阅读 15