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

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

Chrome 的离线页面彩蛋是历史上保密最差的秘密之一([citation needed],但声明是为了达到戏剧性效果)。如果您按 Space 键(在移动设备上,请点按恐龙),离线页面就会变成可玩的街机游戏。您可能知道,其实您无需离线即可畅玩游戏:在 Chrome 中,您只需前往 about://dino 即可;如果您是技术爱好者,则可以前往 about://network-error/-106。但您知道吗?每个月有 2.7 亿人玩 Chrome 恐龙游戏

Chrome 的离线页面,其中显示了 Chrome 恐龙游戏。
按空格键即可开始游戏!

还有一个可能更实用且您可能不知道的事实是,在街机模式下,您可以使用游戏手柄玩游戏。本文撰写时,游戏手柄支持大约在一年前由 Reilly Grantcommit中添加。正如您所见,该游戏与 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.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 中的数组始终固定为 4 项。如果连接的游戏手柄数量为零或少于 4 个,则某个项可能只是 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.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(),并且系统也不会触发 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 提供。