你以为游戏手柄接网页只能靠 getGamepads 每帧轮询?今天 Edge 154 用一个事件把这件事彻底原生化了

做过浏览器游戏的人都踩过这个坑——手柄按下去了,游戏里的人物却慢了半拍才动。不是游戏写得烂,是 navigator.getGamepads() 的原罪:它本质是一个轮询接口,你得在 requestAnimationFrame 里每帧去问一次「现在按键是什么状态」,中间那段延迟是躲不掉的。

这个问题在节奏游戏里最明显——云游戏更严重,每一帧的延迟都要算进用户体验里。Edge 154 今天用 gamepadrawinputchanged 事件把这件事彻底原生化了:手柄一动,事件就飞过来,不用你每帧去问。

旧方案:RAF 轮询的代价

传统写法是这样的:

function readGamepad() {
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    if (!gamepad) continue;
    gamepad.buttons.forEach((button, i) => {
      if (button.pressed) handleButton(i);
    });
    gamepad.axes.forEach((axis, i) => {
      if (Math.abs(axis) > 0.1) handleAxis(i, axis);
    });
  }
  requestAnimationFrame(readGamepad);
}

你不知道用户什么时候按,只能每帧去查。最理想情况下,你在一帧结束前刚好查到这一次按键——延迟大概 8~16ms(取决于刷新率)。但如果用户恰好在你查完之后才按,那就要等下一帧,最多延迟 16ms。对于 60fps 的游戏,这是可以接受的;但对于云游戏或高精度场景,这 16ms 的不确定性会直接传导到画面上。

新方案:事件驱动,直达硬件

gamepadrawinputchanged 登场后,代码变成了这样:

window.addEventListener("gamepadrawinputchanged", (event) => {
  // 哪些轴变化了
  for (const axisIndex of event.axesChanged) {
    const value = event.gamepad.axes[axisIndex];
    handleAxis(axisIndex, value);
  }
  // 哪些按钮被按下
  for (const buttonIndex of event.buttonsPressed) {
    handleButton(buttonIndex);
  }
  // 哪些按钮被松开
  for (const buttonIndex of event.buttonsReleased) {
    handleButtonRelease(buttonIndex);
  }
  // 哪些按钮的值变化了(模拟量,如扳机键)
  for (const buttonIndex of event.buttonsValueChanged) {
    const buttonValue = event.gamepad.buttons[buttonIndex].value;
    handleAnalogButton(buttonIndex, buttonValue);
  }
});

事件对象直接告诉你「哪些轴动了、哪些键按了/松了」,不用再自己遍历整个 buttons 和 axes 数组做对比。更关键的是:事件在输入状态变化的瞬间就触发,没有 RAF 的帧等待,不会有跨帧延迟。

事件对象的核心字段

axesChanged:本次变化的轴索引
buttonsPressed:本次新按下的按钮索引
buttonsReleased:本次新松开的按钮索引
buttonsValueChanged:值有变化的按钮(含模拟量扳机)
touchesChanged:触摸板/触摸面有变化的触点索引

事件没有合并(no coalescing)——每次输入变化都单独触发,保证你能精确到毫秒级捕捉每一次状态变迁。这对于节奏游戏、光枪游戏、云游戏这类对延迟敏感的应用意义重大。

对谁有用

节奏游戏:节拍误差从一帧(~16ms)压缩到实际输入延迟,几乎等于硬件直达
云游戏:云端每一帧都算钱,减少不确定延迟可以直接降低卡顿感
实时对战:手柄延迟越低,竞技体验越接近原生应用
创意工具:用游戏手柄当 CAD 参数旋钮、音频轨道推子,事件驱动比轮询更省电

兼容性现状

目前 gamepadrawinputchanged 在 Edge 154 稳定版和基于 Chromium 154+ 的浏览器上可用。Chrome 尚未跟进,Firefox/Safari 暂无公开计划。游戏类应用可以把它当作渐进增强:浏览器支持就注册事件,不支持就回退到 RAF 轮询,用户体验自动降级,不会报错。

下一步

如果你正在做游戏类 Web 应用,或者想用手柄做非游戏类交互(控制面板、硬件参数调节),现在可以给现有 RAF 轮询方案加一行 feature detection:

if ("ongamepadrawinputchanged" in window) {
  window.addEventListener("gamepadrawinputchanged", onRawInput);
} else {
  // 降级到 RAF 轮询
  requestAnimationFrame(pollLoop);
}

Edge 154 的 gamepadrawinputchanged 把浏览器游戏的输入延迟从「被动查询」变成了「主动送达」——这件事在游戏手柄这个场景里,等了多少年?大概从 Gamepad API 诞生那天起就有人在等了。

评论区

0 条评论

登录后可评论。

阿跨·跨端开发 14 阅读