使用手柄畅玩 Chrome 恐龙游戏

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

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

Chrome's offline page with the Chrome dino game.
按空格键开始游戏!

另一个可能更有用且您可能不知道的事实是,在游戏机模式下,您可以使用手柄玩游戏。手柄支持大约在一年前添加到游戏中,撰写本文时,提交 者是 Reilly Grant。正如您所见,这款游戏就像 Chromium 项目的其余部分一样,完全是开源的。在这篇文章中,我想向您展示如何使用 Gamepad API。

使用 Gamepad API

功能检测和浏览器支持

Gamepad API 在桌面和移动设备上都具有普遍良好的浏览器支持。您可以使用以下代码段检测是否支持 Gamepad API

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

浏览器如何表示手柄

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

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

请注意,按钮可以是数字式的(按下或未按下)或模拟式的(例如,按下 78%)。这就是为什么按钮被报告为 GamepadButton 对象,具有以下属性

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

您可能会遇到的另一个额外的东西,取决于您的浏览器和您拥有的手柄,是 vibrationActuator 属性。它允许两种隆隆声效果

  • 双重隆隆声:由两个偏心旋转质量致动器生成的触觉反馈效果,每个致动器位于手柄的一个握把中。
  • 扳机隆隆声:由两个独立的电机生成的触觉反馈效果,一个电机位于手柄的每个扳机中。

以下示意图概述,取自规范,显示了通用手柄上按钮和轴的映射和排列。

Schematic overview of the button and axes mappings of a common gamepad.
标准手柄布局的可视化表示(来源)。

手柄连接时的通知

要了解手柄何时连接,请监听在 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:设置延迟的持续时间,直到振动开始。
  • strongMagnitudeweakMagnitude:设置较重和较轻的偏心旋转质量电机的振动强度级别,归一化到 0.01.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,
  });
};

与 Permissions Policy 集成

Gamepad API 规范定义了一个由字符串 "gamepad" 标识的策略控制功能。其默认 allowlist"self"。文档的权限策略确定该文档中的任何内容是否被允许访问 navigator.getGamepads()。如果在任何文档中禁用,则该文档中的任何内容都将不允许使用 navigator.getGamepads(),并且 gamepadconnectedgamepaddisconnected 事件也不会触发。

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

演示

以下示例中嵌入了一个 手柄测试器演示。源代码可在 Glitch 上 找到。通过 USB 或蓝牙连接手柄并按下其任何按钮或移动其任何轴来尝试演示。

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

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

为了使 Chrome 恐龙手柄 演示能够工作,我从核心 Chromium 项目中提取了 Chrome 恐龙游戏(更新了 Arnelle Ballane早期工作),将其放在一个独立的站点上,通过添加躲避和振动效果扩展了现有的手柄 API 实现,创建了全屏模式,并且 Mehul Satardekar 贡献了黑暗模式实现。祝您游戏愉快!

致谢

本文档由 François BeaufortJoe Medley 审阅。Gamepad API 规范由 Steve AgostonJames HollyerMatt Reynolds 编辑。以前的规范编辑是 Brandon JonesScott GrahamTed Mielczarek。Gamepad Extensions 规范由 Brandon Jones 编辑。英雄图片由 Laura Torrent Puig 提供。