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

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

Chrome 的离线页面彩蛋是历史上保密最差的秘密之一([citation needed],但声明是为了达到戏剧效果)。如果您按 Space 键(在移动设备上,请点按恐龙),离线页面就会变成可玩的街机游戏。您可能已经意识到,您完全不必离线玩乐,而是无需离线:在 Chrome 中,您可以直接转到 about://dino,或者,如果您是游戏迷,也可以转到 about://network-error/-106。但您知道吗?每个月有 2.7 亿人玩 Chrome 恐龙游戏

Chrome 的离线网页,其中包含 Chrome dino 游戏。
按空格键即可开始游戏!

还有一个可能更实用且您可能不知道的事实是,在街机模式下,您可以使用游戏手柄玩游戏。本文撰写时,游戏手柄支持大约在一年前由 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.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 个,则某个项可能只是 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(),则不允许文档中的任何内容使用 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 提供。