Играйте в Chrome Dino с геймпадом

Узнайте, как использовать Gamepad API, чтобы вывести свои браузерные игры на новый уровень.

Пасхалка на офлайн-странице Chrome – один из самых плохо скрываемых секретов в истории ([citation needed], но это утверждение сделано для драматического эффекта). Если нажать пробел или коснуться динозавра на мобильном устройстве, страница превратится в аркадную игру. Возможно, вы знаете, что для игры не обязательно отключаться от интернета. В Chrome можно просто перейти по адресу about://dino или about://network-error/-106. Но знаете ли вы, что каждый месяц в Chrome Dino играют 270 миллионов раз?

Офлайн-страница 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: VRDisplay.displayId связанного объекта VRDisplay (если применимо).
  • 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 – эффект виброотклика, создаваемый двумя эксцентриковыми вращающимися приводами, по одному в каждой рукоятке геймпада.
  • Вибрация триггеров – эффект виброотклика, создаваемый двумя независимыми моторами, по одному в каждом триггере геймпада.

На схеме ниже, взятой из спецификации, показано сопоставление и расположение кнопок и осей на стандартном геймпаде.

Схематический обзор сопоставлений кнопок и осей обычного геймпада.
Визуальное представление стандартной раскладки геймпада (источник).

Уведомление о подключении геймпада

Чтобы узнать, когда геймпад подключен, прослушивайте событие gamepadconnected, которое активируется в объекте window. Когда пользователь подключает геймпад через USB или Bluetooth, активируется событие 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. Обратите внимание, что в следующем примере connected меняется на false, когда я отключаю контроллер Xbox 360.

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,
  });
};

Интеграция с Permissions Policy

Спецификация Gamepad API определяет функцию, управляемую правилами, которая идентифицируется строкой "gamepad". Значение по умолчанию для allowlist – "self". Правила в отношении разрешений для документа определяют, может ли контент в этом документе получить доступ к navigator.getGamepads(). Если отключить эту функцию в документе, то ни один его элемент не сможет использовать navigator.getGamepads(), а события gamepadconnected и gamepaddisconnected не будут активироваться.

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

Демоверсии

Ниже встроена демонстрация тестера геймпада. Исходный код доступен на GitHub. Чтобы попробовать демоверсию, подключите геймпад через USB или Bluetooth и нажмите любую кнопку или переместите любую ось.

Бонус: играйте в Chrome Dino на web.dev

Вы можете играть в Chrome Dino с помощью геймпада прямо на этом сайте. Исходный код доступен на GitHub. Посмотрите, как реализован опрос геймпада в trex-runner.js, и обратите внимание на то, как эмулируются нажатия клавиш.

Чтобы продемонстрировать геймпад для игры Chrome Dino, я извлек игру из основного проекта Chromium (обновив предыдущую версию, созданную Арнеллом Балланом), разместил ее на отдельном сайте, расширил существующую реализацию Gamepad API, добавив эффекты приседания и вибрации, создал полноэкранный режим, а Мехул Сатардекар добавил темную тему. Приятной игры!

Благодарности

Этот документ проверили Франсуа Бофор (François Beaufort) и Джо Медли (Joe Medley). Спецификацию Gamepad API редактируют Стив Агостон (Steve Agoston), Джеймс Холлиер (James Hollyer) и Мэтт Рейнольдс (Matt Reynolds). Предыдущие редакторы спецификации: Брэндон Джонс (Brandon Jones), Скотт Грэм (Scott Graham) и Тед Мильчарек (Ted Mielczarek). Редактор спецификации Gamepad Extensions – Брэндон Джонс (Brandon Jones).