使用游戏手柄畅玩 Chrome 恐龙游戏

了解如何使用 Gamepad API 将您的 Web 游戏提升到新的水平。

Chrome 的离线网页彩蛋是历史上最不为人所知的秘密之一([citation needed],但为了戏剧效果,我们还是这样说了)。如果您按下空格键,或者在移动设备上点按小恐龙,离线页面就会变成可玩的游戏。您可能知道,当您想玩游戏时,实际上不必离线:在 Chrome 中,您只需前往 about://dino,或者,如果您是技术爱好者,可以浏览 about://network-error/-106。但您知道吗,每个月有 2.7 亿次 Chrome 恐龙游戏?

包含 Chrome 恐龙游戏的 Chrome 离线网页。
按空格键即可开始游戏!

另一个可能更有用但您可能不知道的事实是,在街机模式下,您可以使用游戏手柄玩游戏。大约一年前,Reilly Grant 在 commit 中添加了游戏手柄支持。如您所见,这款游戏与 Chromium 项目的其余部分一样,完全是开源的。在这篇博文中,我想向您展示如何使用 Gamepad API。

使用 Gamepad API

功能检测和浏览器支持

Gamepad API 在桌面设备和移动设备上都获得了出色的浏览器支持。您可以使用以下代码段检测 Gamepad API 是否受支持:

if ('getGamepads' in navigator) {
  // The API is supported!
}

浏览器如何表示游戏手柄

浏览器将游戏手柄表示为 Gamepad 对象。Gamepad 具有以下属性:

  • id:游戏手柄的标识字符串。此字符串用于标识已连接的游戏手柄设备的品牌或样式。
  • displayId:关联的 VRDisplay 的 VRDisplay.displayId(如果相关)。
  • index:游戏手柄在导航器中的索引。
  • connected:指示手柄是否仍连接到系统。
  • hand:一个枚举,用于定义控制器握在哪个手中,或最有可能握在哪个手中。
  • timestamp:相应手柄的数据上次更新的时间。
  • mapping:相应设备所用的按钮和轴映射,可以是 "standard" 或 "xr-standard"。
  • pose:一个 GamepadPose 对象,表示与 WebVR 控制器关联的姿势信息。
  • axes:游戏手柄所有轴的值的数组,线性归一化到 -1.0–1.0 的范围。
  • buttons:游戏手柄所有按钮的按钮状态数组。

请注意,按钮可以是数字式(按下或未按下),也可以是模拟式(例如,按下 78%)。因此,按钮会报告为 GamepadButton 对象,并具有以下属性:

  • pressed:按钮的按下状态(如果按钮已按下,则为 true;如果未按下,则为 false)。
  • touched:按钮的触控状态。如果按钮能够检测到触摸,则当按钮被触摸时,此属性为 true,否则为 false。
  • value:对于具有模拟传感器的按钮,此属性表示按钮的按压程度,线性归一化到 0.0-1.0 的范围。
  • hapticActuators:一个包含 GamepadHapticActuator 对象的数组,每个对象都表示控制器上可用的触感反馈硬件。

根据您的浏览器和游戏手柄,您可能会遇到一个额外的 vibrationActuator 属性。它支持两种振动效果:

  • 双重震动:由两个偏心旋转质量致动器(分别位于游戏手柄的两个握柄中)生成的触感反馈效果。
  • 扳机振动:由两个独立马达产生的触感反馈效果,每个马达分别位于游戏手柄的两个扳机中。

以下示意图(直接来自规范)显示了通用游戏手柄上按钮和轴的映射和排列。

常见游戏手柄的按钮和轴映射的示意图概览。
标准游戏手柄布局的直观表示形式(来源)。

当游戏手柄连接时收到通知

如需了解手柄何时连接,请监听 window 对象上触发的 gamepadconnected 事件。当用户连接游戏手柄时(可以通过 USB 或蓝牙连接),系统会触发一个 GamepadEvent,其中包含一个名为 gamepad 的属性,用于存储游戏手柄的详细信息。在下图中,您可以看到一个示例,该示例来自我手边的一个 Xbox 360 控制器(是的,我喜欢玩复古游戏)。

window.addEventListener('gamepadconnected', (event) => {
  console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: true
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
  */
});

当游戏手柄断开连接时收到通知

接收手柄断开连接的通知的方式与检测连接的方式类似。 这次,应用会监听 gamepaddisconnected 事件。请注意,在以下示例中,当我拔下 Xbox 360 控制器时,connected 现在变为 false。

window.addEventListener('gamepaddisconnected', (event) => {
  console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: false
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: null
  */
});

游戏循环中的游戏手柄

获取游戏手柄从调用 navigator.getGamepads() 开始,该调用会返回一个包含 Gamepad 项的数组。Chrome 中的数组始终具有固定的长度,包含四项。如果连接的游戏手柄数量为零或少于四个,相应项可能只是 null。请务必始终检查数组的所有项,并注意游戏手柄会“记住”其插槽,因此可能并不总是位于第一个可用插槽中。

// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]

如果已连接一个或多个游戏手柄,但 navigator.getGamepads() 仍报告 null 个项目,您可能需要按任意按钮“唤醒”每个游戏手柄。然后,您可以在游戏循环中轮询手柄状态,如以下代码所示。

const pollGamepads = () => {
  // Always call `navigator.getGamepads()` inside of
  // the game loop, not outside.
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    // Disregard empty slots.
    if (!gamepad) {
      continue;
    }
    // Process the gamepad state.
    console.log(gamepad);
  }
  // Call yourself upon the next animation frame.
  // (Typically this happens every 60 times per second.)
  window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();

振动致动器

vibrationActuator 属性会返回一个 GamepadHapticActuator 对象,该对象对应于电机或其他执行器的配置,这些执行器可以施加力以实现触感反馈。可以通过调用 Gamepad.vibrationActuator.playEffect() 来播放触感反馈效果。唯一有效的效果类型为 'dual-rumble' 和 'trigger-rumble'。

支持的振动效果

if (gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
  // Trigger rumble supported.
} else if (gamepad.vibrationActuator.effects.includes('dual-rumble')) {
  // Dual rumble supported.
} else {
  // Rumble effects aren't supported.
}

双重振动

双重震动是指在标准游戏手柄的每个手柄中都配备偏心旋转质量振动马达的触感配置。在此配置中,任一马达都能够使整个游戏手柄振动。这两个质量块是不相等的,这样就可以将它们的效果结合起来,从而创建更复杂的触感效果。双重震动效果由以下四个参数定义:

  • duration:设置振动效果的持续时间(以毫秒为单位)。
  • startDelay:设置延迟时长,直到开始振动。
  • strongMagnitude 和 weakMagnitude:设置较重和较轻的偏心旋转质量电机的振动强度级别,归一化到 0.0-1.0 范围内。
// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  gamepad.vibrationActuator.playEffect('dual-rumble', {
    // Start delay in ms.
    startDelay: delay,
    // Duration in ms.
    duration: duration,
    // The magnitude of the weak actuator (between 0 and 1).
    weakMagnitude: weak,
    // The magnitude of the strong actuator (between 0 and 1).
    strongMagnitude: strong,
  });
};

触发振动

扳机震动是由两个独立马达产生的触感反馈效果,每个马达分别位于手柄的两个扳机中。

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  // Feature detection.
  if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
    return;
  }
  gamepad.vibrationActuator.playEffect('trigger-rumble', {
    // Duration in ms.
    duration: duration,
    // The left trigger (between 0 and 1).
    leftTrigger: leftTrigger,
    // The right trigger (between 0 and 1).
    rightTrigger: rightTrigger,
  });
};

与“权限”政策集成

Gamepad API 规范定义了一项由政策控制的功能,该功能由字符串 "gamepad" 标识。其默认 allowlist 为 "self"。文档的权限政策决定了该文档中的任何内容是否可以访问 navigator.getGamepads()。如果此属性在任何文档中处于停用状态,则该文档中的任何内容都不得使用 navigator.getGamepads(),并且不会触发 gamepadconnected 和 gamepaddisconnected 事件。

<iframe src="index.html" allow="gamepad"></iframe>

演示

以下示例中嵌入了一个游戏手柄测试器演示。您可以在 GitHub 上找到相关源代码。您可以尝试连接游戏手柄(通过 USB 或蓝牙),然后按任意按钮或移动任意轴,体验一下演示。

奖励:在 web.dev 上玩 Chrome 恐龙游戏

您可以在此网站上使用游戏手柄玩 Chrome 恐龙游戏。您可以在 GitHub 上找到相关源代码。查看 trex-runner.js 中的游戏手柄轮询实现,并注意它是如何模拟按键的。

为了让 Chrome 恐龙游戏手柄演示正常运行,我从核心 Chromium 项目中提取了 Chrome 恐龙游戏(更新了 Arnelle Ballane 之前的努力),将其放置在一个独立网站上,通过添加压低音量和振动效果来扩展现有的游戏手柄 API 实现,创建了全屏模式,Mehul Satardekar 贡献了深色模式实现。祝您游戏愉快!

致谢

本文档由 François Beaufort 和 Joe Medley 审核。Gamepad API 规范由 Steve Agoston、James Hollyer 和 Matt Reynolds 编辑。之前的规范编辑者是 Brandon Jones、Scott Graham 和 Ted Mielczarek。Gamepad Extensions 规范由 Brandon Jones 编辑。主打图片由 Laura Torrent Puig 拍摄。