Играйте в динозавровую игру Chrome с помощью геймпада

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

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

Оффлайн-страница Chrome с игрой Chrome Dino.
Нажмите клавишу пробела, чтобы играть!

Еще один факт, о котором, возможно, полезнее знать и о котором вы, возможно, не знаете, заключается в том, что в аркадном режиме вы можете играть в игру с помощью геймпада. Поддержка геймпада была добавлена ​​примерно год назад, на момент написания этой статьи, в коммите Рейли Гранта . Как видите, игра, как и весь проект Chromium, имеет полностью открытый исходный код . В этом посте я хочу показать вам, как использовать API геймпада.

Используйте API геймпада

Обнаружение функций и поддержка браузера

API геймпада имеет отличную поддержку браузеров как на настольных, так и на мобильных устройствах. Вы можете определить, поддерживается ли 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.01.0 .
  • buttons : Массив состояний кнопок для всех кнопок геймпада.

Обратите внимание, что кнопки могут быть цифровыми (нажаты или не нажаты) или аналоговыми (например, нажаты на 78%). Вот почему кнопки сообщаются как объекты GamepadButton со следующими атрибутами:

  • pressed : состояние нажатой кнопки ( true , если кнопка нажата, и false если она не нажата.
  • touched : состояние кнопки при касании. Если кнопка способна обнаруживать касание, это свойство имеет true , если к кнопке прикасаются, и false в противном случае.
  • value : для кнопок с аналоговым датчиком это свойство представляет величину нажатия кнопки 1.0 линейно нормализованную в диапазоне 0.0 .
  • hapticActuators : массив, содержащий объекты GamepadHapticActuator , каждый из которых представляет оборудование тактильной обратной связи, доступное на контроллере.

Еще одна вещь, с которой вы можете столкнуться в зависимости от вашего браузера и геймпада, — это свойство vibrationActuator . Он позволяет использовать два типа эффектов грохота:

  • Dual-Rumble: эффект тактильной обратной связи, создаваемый двумя эксцентриковыми вращающимися массовыми приводами, по одному на каждой рукоятке геймпада.
  • Trigger-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' . Dual-rumble описывает тактильную конфигурацию с эксцентриковым вибромотором с вращающейся массой в каждой ручке стандартного геймпада. В этой конфигурации любой двигатель способен вызывать вибрацию всего геймпада. Две массы неравны, поэтому эффекты каждой из них можно комбинировать для создания более сложных тактильных эффектов. Эффекты двойного грохота определяются четырьмя параметрами:

  • duration : устанавливает продолжительность эффекта вибрации в миллисекундах.
  • startDelay : устанавливает продолжительность задержки до начала вибрации.
  • strongMagnitude и weakMagnitude : установите уровни интенсивности вибрации для более тяжелых и легких двигателей 1.0 эксцентриковой вращающейся массой, нормированные в диапазоне 0.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() , а также не будут срабатывать события gamepadconnected и gamepaddisconnected .

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

Демо

Демо-версия тестера геймпада встроена в следующий пример. Исходный код доступен на Glitch . Попробуйте демо-версию, подключив геймпад через USB или Bluetooth и нажав любую из его кнопок или переместив любую из его осей.

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

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

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

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

Этот документ был рецензирован Франсуа Бофором и Джо Медли . Спецификацию Gamepad API редактируют Стив Агостон , Джеймс Холлиер и Мэтт Рейнольдс . Бывшими редакторами спецификаций являются Брэндон Джонс , Скотт Грэм и Тед Мельчарек . Спецификацию расширений геймпада редактирует Брэндон Джонс . Изображение героя от Лауры Торрент Пуч.