Преодоление препятствий с помощью Gamepad API

Введение

Пусть новички играют в квесты на клавиатуре, режут фрукты на сенсорных экранах и танцуют перед датчиками движения, воображая себя Майклом Джексоном. (Спойлер: нет). Но ты не такой. Ты лучше. Вы — профи. Для вас игры начинаются и заканчиваются с геймпадом в руках.

Но подождите. Что делать, если вы хотите добавить поддержку геймпада в веб-приложение? Не проблема! Новый Gamepad API позволяет использовать JavaScript для считывания состояния любого геймпада, подключенного к компьютеру. Он настолько новый, что появился в Chrome 21 только на прошлой неделе, а в Firefox его поддержка пока доступна только в специальной сборке.

Это оказалось очень кстати, потому что недавно мы использовали его в дудле Google "Бег с барьерами – 2012". В этой статье мы кратко расскажем, как добавили Gamepad API в дудл и что узнали в процессе.

Дудл Google "Бег с барьерами 2012"
Дудл Google "Бег с барьерами" (2012)

Тестер геймпада

Несмотря на то, что интерактивные дудлы доступны всего один день, они представляют собой довольно сложные программы. Чтобы наглядно продемонстрировать, о чем идет речь, мы взяли код геймпада из дудла и создали простой тестер геймпада. С его помощью можно проверить, правильно ли работает USB-геймпад, и посмотреть, как это реализовано.

Какие браузеры поддерживают его сейчас?

Browser Support

  • Chrome: 21.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 10.1.

Source

Какие геймпады можно использовать?

Как правило, поддерживаются все современные геймпады, совместимые с вашей системой. Мы протестировали различные геймпады, от USB-контроллеров неизвестных брендов на ПК до геймпадов PlayStation 2, подключенных через адаптер к Mac, и контроллеров Bluetooth, подключенных к ноутбуку с Chrome OS.

Геймпады
Геймпады

На этой фотографии показаны контроллеры, которые мы использовали для тестирования дудла. "Да, мама, именно этим я и занимаюсь на работе". Если контроллер не работает или кнопки назначены неправильно, сообщите об ошибке в Chrome или Firefox . (Проверьте, не устранена ли проблема в последней версии браузера.)

Определение поддержки Gamepad API<

В браузере Chrome:

var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;

Похоже, что в Firefox это пока невозможно. Все основано на событиях, и все обработчики событий должны быть прикреплены к окну, что мешает работе типичного метода обнаружения обработчиков событий.

Но мы уверены, что это временно. Потрясающая библиотека Modernizr уже сообщает вам о Gamepad API, поэтому мы рекомендуем использовать ее для всех текущих и будущих задач по обнаружению:

var gamepadSupportAvailable = Modernizr.gamepads;

Как узнать, какие геймпады подключены

Даже если вы подключите геймпад, он никак не проявит себя, пока пользователь не нажмет на нем какую-либо кнопку. Это сделано для предотвращения цифровых отпечатков, хотя и создает некоторые проблемы для пользователей: вы не можете попросить пользователя нажать кнопку или предоставить инструкции для конкретного геймпада, потому что вы не знаете, подключил ли он свой контроллер.

Но это не единственное препятствие.

Опросы

Реализация API в Chrome предоставляет функцию navigator.webkitGetGamepads(), с помощью которой можно получить список всех геймпадов, подключенных к системе, и их текущее состояние (кнопки и стики). Первый подключенный геймпад будет возвращен как первая запись в массиве и т. д.

(Этот вызов функции недавно заменил массив, к которому можно было обращаться напрямую – navigator.webkitGamepads[]. По состоянию на начало августа 2012 года доступ к этому массиву по-прежнему необходим в Chrome 21, а вызов функции работает в Chrome 22 и более поздних версиях. В дальнейшем вызов функции станет рекомендуемым способом использования API и будет постепенно внедрен во все установленные браузеры Chrome.

Реализованная на данный момент часть спецификации требует, чтобы вы постоянно проверяли состояние подключенных геймпадов (и при необходимости сравнивали его с предыдущим), а не запускали события при изменении ситуации. Мы использовали requestAnimationFrame(), чтобы настроить опрос наиболее эффективным и экономичным способом. Для нашего дудла, несмотря на то, что у нас уже есть цикл requestAnimationFrame() для поддержки анимации, мы создали второй, полностью отдельный цикл – его было проще закодировать, и он никак не должен повлиять на производительность.

Вот код от тестировщика:

/**
 * Starts a polling loop to check for gamepad state.
 */
startPolling: function() {
    // Don't accidentally start a second loop, man.
    if (!gamepadSupport.ticking) {
    gamepadSupport.ticking = true;
    gamepadSupport.tick();
    }
},

/**
 * Stops a polling loop by setting a flag which will prevent the next
 * requestAnimationFrame() from being scheduled.
 */
stopPolling: function() {
    gamepadSupport.ticking = false;
},

/**
 * A function called with each requestAnimationFrame(). Polls the gamepad
 * status and schedules another poll.
 */
tick: function() {
    gamepadSupport.pollStatus();
    gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
    // Only schedule the next frame if we haven't decided to stop via
    // stopPolling() before.
    if (gamepadSupport.ticking) {
    if (window.requestAnimationFrame) {
        window.requestAnimationFrame(gamepadSupport.tick);
    } else if (window.mozRequestAnimationFrame) {
        window.mozRequestAnimationFrame(gamepadSupport.tick);
    } else if (window.webkitRequestAnimationFrame) {
        window.webkitRequestAnimationFrame(gamepadSupport.tick);
    }
    // Note lack of setTimeout since all the browsers that support
    // Gamepad API are already supporting requestAnimationFrame().
    }
},

/**
 * Checks for the gamepad status. Monitors the necessary data and notices
 * the differences from previous state (buttons for Chrome/Firefox,
 * new connects/disconnects for Chrome). If differences are noticed, asks
 * to update the display accordingly. Should run as close to 60 frames per
 * second as possible.
 */
pollStatus: function() {
    // (Code goes here.)
},

Если вас интересует только один геймпад, получить его данные может быть так же просто, как:

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

Если вы хотите сделать игру более интересной или добавить поддержку нескольких игроков, вам нужно будет добавить ещё несколько строк кода, чтобы реагировать на более сложные сценарии (подключено два или более геймпада, некоторые из них отключаются в процессе игры и т. д.). Один из способов решения этой проблемы можно найти в исходном коде нашего инструмента тестирования (функция pollGamepads()).

События

В Firefox используется альтернативный, более эффективный способ, описанный в спецификации Gamepad API. Вместо опроса он использует два события – MozGamepadConnected и MozGamepadDisconnected, которые запускаются всякий раз, когда геймпад подключается (или, точнее, подключается и "объявляется" нажатием любой из его кнопок) или отключается. Объект геймпада, который будет отражать его будущее состояние, передается в качестве параметра .gamepad объекта события.

Из исходного кода инструмента проверки:

/**
 * React to the gamepad being connected. Today, this will only be executed
 * on Firefox.
 */
onGamepadConnect: function(event) {
    // Add the new gamepad on the list of gamepads to look after.
    gamepadSupport.gamepads.push(event.gamepad);

    // Start the polling loop to monitor button changes.
    gamepadSupport.startPolling();

    // Ask the tester to update the screen to show more gamepads.
    tester.updateGamepads(gamepadSupport.gamepads);
},

Сводка

В итоге функция инициализации в тестировщике, поддерживающая оба подхода, выглядит следующим образом:

/**
 * Initialize support for Gamepad API.
 */
init: function() {
    // As of writing, it seems impossible to detect Gamepad API support
    // in Firefox, hence we need to hardcode it in the third clause.
    // (The preceding two clauses are for Chrome.)
    var gamepadSupportAvailable = !!navigator.webkitGetGamepads ||
        !!navigator.webkitGamepads ||
        (navigator.userAgent.indexOf('Firefox/') != -1);

    if (!gamepadSupportAvailable) {
    // It doesn't seem Gamepad API is available – show a message telling
    // the visitor about it.
    tester.showNotSupported();
    } else {
    // Firefox supports the connect/disconnect event, so we attach event
    // handlers to those.
    window.addEventListener('MozGamepadConnected',
                            gamepadSupport.onGamepadConnect, false);
    window.addEventListener('MozGamepadDisconnected',
                            gamepadSupport.onGamepadDisconnect, false);

    // Since Chrome only supports polling, we initiate polling loop straight
    // away. For Firefox, we will only do it if we get a connect event.
    if (!!navigator.webkitGamepads || !!navigator.webkitGetGamepads) {
        gamepadSupport.startPolling();
    }
    }
},

Информация о геймпаде

Каждый геймпад, подключенный к системе, будет представлен объектом, который выглядит примерно так:

id: "PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)"
index: 1
timestamp: 18395424738498
buttons: Array[8]
    0: 0
    1: 0
    2: 1
    3: 0
    4: 0
    5: 0
    6: 0.03291
    7: 0
axes: Array[4]
    0: -0.01176
    1: 0.01961
    2: -0.00392
    3: -0.01176

Основная информация

Первые несколько полей – это простые метаданные:

  • id – текстовое описание геймпада;
  • index – целое число, которое позволяет различать геймпады, подключенные к одному компьютеру.
  • timestamp – временная метка последнего обновления состояния кнопки или оси (в настоящее время поддерживается только в Chrome).

Кнопки и стики

Современные геймпады не похожи на те, с помощью которых ваши дедушки спасали принцесс из замков. Обычно у них есть как минимум 16 отдельных кнопок (некоторые дискретные, некоторые аналоговые) и два аналоговых стика. Gamepad API сообщит вам обо всех кнопках и аналоговых джойстиках, о которых сообщает операционная система.

Получив текущее состояние в объекте геймпада, вы можете получить доступ к кнопкам через массивы .buttons[] и к стикам через массивы .axes[]. Ниже показано, как они соотносятся.

Схема геймпада
Схема геймпада

Спецификация требует, чтобы браузер сопоставил первые шестнадцать кнопок и четыре оси со следующими элементами:

gamepad.BUTTONS = {
    FACE_1: 0, // Face (main) buttons
    FACE_2: 1,
    FACE_3: 2,
    FACE_4: 3,
    LEFT_SHOULDER: 4, // Top shoulder buttons
    RIGHT_SHOULDER: 5,
    LEFT_SHOULDER_BOTTOM: 6, // Bottom shoulder buttons
    RIGHT_SHOULDER_BOTTOM: 7,
    SELECT: 8,
    START: 9,
    LEFT_ANALOGUE_STICK: 10, // Analogue sticks (if depressible)
    RIGHT_ANALOGUE_STICK: 11,
    PAD_TOP: 12, // Directional (discrete) pad
    PAD_BOTTOM: 13,
    PAD_LEFT: 14,
    PAD_RIGHT: 15
};

gamepad.AXES = {
    LEFT_ANALOGUE_HOR: 0,
    LEFT_ANALOGUE_VERT: 1,
    RIGHT_ANALOGUE_HOR: 2,
    RIGHT_ANALOGUE_VERT: 3
};

Дополнительные кнопки и оси будут добавлены к указанным выше. Обратите внимание, что наличие 16 кнопок и четырех осей не гарантируется. Некоторые из них могут быть не определены.

Кнопки могут принимать значения от 0,0 (не нажата) до 1,0 (нажата полностью). Значения осей варьируются от -1,0 (крайнее левое или верхнее положение) до 1,0 (крайнее правое или нижнее положение). Значение 0,0 соответствует центру.

Аналоговый или дискретный?

В принципе, любая кнопка может быть аналоговой. Например, это часто встречается у триггеров. Поэтому лучше установить пороговое значение, а не просто сравнивать его с 1,00 (что, если аналоговая кнопка окажется немного грязной? (значение может никогда не достичь 1,00). В нашем дудле это делается так:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

gamepad.buttonPressed_ = function(pad, buttonId) {
    return pad.buttons[buttonId] &&
            (pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};

Аналогичным образом можно превратить аналоговые стики в цифровые джойстики. Конечно, всегда есть цифровая панель (d-pad), но на вашем геймпаде ее может не быть. Вот код, который мы используем для этого:

gamepad.AXIS_THRESHOLD = .75;

gamepad.stickMoved_ = function(pad, axisId, negativeDirection) {
    if (typeof pad.axes[axisId] == 'undefined') {
    return false;
    } else if (negativeDirection) {
    return pad.axes[axisId] < -gamepad.AXIS_THRESHOLD;
    } else {
    return pad.axes[axisId] > gamepad.AXIS_THRESHOLD;
    }
};

Нажатия кнопок и движения стиков

События

В некоторых случаях, например в имитации полёта, непрерывная проверка и реакция на положение джойстика или нажатие кнопок имеет больше смысла… но для таких вещей, как дудл Hurdles 2012? Вы можете спросить: зачем мне проверять кнопки в каждом кадре? Почему я не могу получать события, как в случае с нажатием клавиш или кнопок мыши?

Хорошая новость: это возможно. Плохая новость – в будущем. Оно есть в спецификации, но пока не реализовано ни в одном браузере.

Опросы

Пока же вы можете сравнивать текущее и предыдущее состояния и вызывать функции, если видите разницу. Пример:

if (buttonPressed(pad, 0) != buttonPressed(oldPad, 0)) {
    buttonEvent(0, buttonPressed(pad, 0) ? 'down' : 'up');
}
for (var i in gamepadSupport.gamepads) {
    var gamepad = gamepadSupport.gamepads[i];

    // Don't do anything if the current timestamp is the same as previous
    // one, which means that the state of the gamepad hasn't changed.
    // This is only supported by Chrome right now, so the first check
    // makes sure we're not doing anything if the timestamps are empty
    // or undefined.
    if (gamepadSupport.prevTimestamps[i] &&
        (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
    continue;
    }
    gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

    gamepadSupport.updateDisplay(i);
}

Дудл "Бег с барьерами – 2012", в котором управление осуществляется с клавиатуры

Поскольку без геймпада предпочтительным методом ввода для сегодняшнего дудла является клавиатура, мы решили, что геймпад должен эмулировать ее довольно точно. Это означало три решения:

  1. Для дудла нужно всего три кнопки: две для бега и одна для прыжков. На геймпаде их, скорее всего, будет больше. Поэтому мы сопоставили все 16 известных кнопок и два известных стика с этими тремя логическими функциями так, как нам показалось наиболее разумным, чтобы люди могли бегать, чередуя кнопки A/B, чередуя плечевые кнопки, нажимая влево/вправо на d-pad или сильно качая любой стик влево и вправо (некоторые из этих способов, конечно, будут более эффективными, чем другие). Пример:

    newState[gamepad.STATES.LEFT] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.PAD_LEFT) ||
        gamepad.stickMoved_(pad, gamepad.AXES.LEFT_ANALOGUE_HOR, true) ||
        gamepad.stickMoved_(pad, gamepad.AXES.RIGHT_ANALOGUE_HOR, true),
    
    newState[gamepad.STATES.PRIMARY_BUTTON] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.FACE_1) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER_BOTTOM) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.SELECT) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.START) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_ANALOGUE_STICK),
    
  2. Мы рассматривали каждый аналоговый вход как дискретный, используя пороговые функции, описанные ранее.

  3. Мы даже не стали встраивать поддержку геймпада в дудл, а просто добавили ее. Наш цикл опроса синтезирует необходимые события keydown и keyup (с правильным keyCode) и отправляет их обратно в DOM:

    // Create and dispatch a corresponding key event.
    var event = document.createEvent('Event');
    var eventName = down ? 'keydown' : 'keyup';
    event.initEvent(eventName, true, true);
    event.keyCode = gamepad.stateToKeyCodeMap_[state];
    gamepad.containerElement_.dispatchEvent(event);

Вот и все!

Полезные советы

  • Помните, что геймпад не будет виден в браузере, пока вы не нажмете кнопку.
  • Если вы тестируете геймпад в нескольких браузерах одновременно, учтите, что контроллер будет распознаваться только в одном из них. Если вы не получаете события, закройте другие страницы, на которых может использоваться этот идентификатор. Кроме того, по нашему опыту, иногда браузер может "удерживать" геймпад, даже если вы закроете вкладку или сам браузер. Иногда перезапуск системы – единственный способ устранить неполадки.
  • Как всегда, используйте Chrome Canary и эквиваленты для других браузеров, чтобы обеспечить наилучшую поддержку, а затем действуйте соответствующим образом, если вы видите, что старые версии ведут себя по-другому.

Будущее

Надеемся, что эта статья помогла вам разобраться в новом API. Он пока не очень стабилен, но уже весьма интересен.

Помимо недостающих частей API (например, событий) и более широкой поддержки браузеров, мы также надеемся со временем увидеть такие вещи, как управление вибрацией, доступ к встроенным гироскопам и т.д. И, конечно, поддержку большего количества типов геймпадов. Если вы обнаружите, что какой-то из них работает некорректно или не работает вообще, сообщите об ошибке в Chrome и/или сообщите об ошибке в Firefox.

А пока попробуйте поиграть в дудл "Бег с барьерами" 2012 года с геймпадом. Ты только что сказал, что можешь пробежать быстрее, чем за 10,7 секунды? Поехали!

Дополнительные сведения