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

了解如何使用 Gamepad API 让您的网页游戏更上一层楼。

Chrome 的离线页面复活节彩蛋是历史上保存最差的秘密之一([citation needed],但声称可以实现惊人的效果)。如果您按空格键或在移动设备上点按恐龙,离线页面就变成了街机游戏。您可能知道,在享受游戏乐趣时,其实并不需要离线:在 Chrome 中,只需转到 about://dino 即可;如果您是极客,也可以浏览到 about://network-error/-106。但您知道吗?每月有 2.7 亿 Chrome 恐龙游戏玩

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

另一个事实可能更值得了解,但您可能不知道,那就是在街机模式下,您可以使用游戏手柄来玩游戏。游戏手柄支持大约在一年前就由 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 属性。它支持两种混战效果:

  • Dual-Rumble:一种触感反馈效果,由两个偏心的旋转质量致动器产生,在游戏手柄上每个手柄各有一个。
  • Trigger-Rumble:一种触感反馈效果,由两个独立的电机产生,一个电机位于游戏手柄的每个触发器上。

以下示意图直接取自规范,显示了通用游戏手柄上按钮和轴的映射及排列方式。

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

游戏手柄连接时的通知

如需了解游戏手柄的连接时间,请监听对 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 个,则 item 可能只是 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'。Dual-Rumble 是指标准游戏手柄的每个手柄上有一个离心的旋转质量振动电机。在此配置中,任一电机都能振动整个游戏手柄。这两个质量不相等,因此可以组合各自的效果来制作更复杂的触感反馈效果。双重 rumble 效果由四个参数定义:

  • duration:设置振动效果的持续时间(以毫秒为单位)。
  • startDelay:设置延迟时长,直到振动开始为止。
  • strongMagnitudeweakMagnitude:为更重和更轻的偏心旋转大型电机设置振动强度,标准化范围为 0.0-1.0

支持的乱斗特效

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.
}

对决

// 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 dino

您可以在这个网站上使用游戏手柄玩 Chrome dinoGitHub 上提供了源代码。查看 trex-runner.js 中的游戏手柄轮询实现,并注意它如何模拟按键操作。

为了让 Chrome dino 游戏手柄演示能够正常发挥作用,我从核心 Chromium 项目中剥离了 Chrome 恐龙游戏(更新了 Arnelle Ballane 之前所做的一项工作),将其放置在独立网站上,通过添加闪避和振动效果来扩展现有的游戏手柄 API 实现,创建了全屏深色模式,并贡献了 karMehul Satar 模式。祝你游戏愉快!

致谢

本文档由 François BeaufortJoe Medley 审核。Gamepad API 规范由 Steve AgostonJames HollyerMatt Reynolds 修改。之前的规范编辑者是 Brandon JonesScott GrahamTed Mielczarek。游戏手柄扩展程序规范由 Brandon Jones 修改。主打图片:Laura Torrent Puig。