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

Введение

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

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

Это оказалось очень удачным моментом, потому что недавно у нас появилась возможность использовать его в дудле Google «Hurdles 2012» . В этой статье кратко объясняется, как мы добавили API геймпада в рисунок и что мы узнали в ходе этого процесса.

Дудл Google "Бег с препятствиями", 2012 год
Дудл Google "Бег с препятствиями", 2012 год

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

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

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

Поддержка браузера

  • 21
  • 12
  • 29
  • 10.1

Источник

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

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

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

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

Функция обнаружения API геймпада<

Достаточно просто в Chrome:

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

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

Но мы уверены, что это временно. Невероятно потрясающий Modernizr уже рассказывает вам об 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)

Кнопки и палочки

Сегодняшние геймпады — это не совсем то, что ваш дедушка мог использовать, чтобы спасти принцессу не в том замке — они обычно имеют как минимум шестнадцать отдельных кнопок (некоторые дискретные, некоторые аналоговые) в дополнение к двум аналоговым джойстикам. 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
};

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

Кнопки могут принимать значения от 0,0 (не нажаты) до 1,0 (нажаты полностью). Оси изменяются от -1,0 (полностью влево или вверх) через 0,0 (центр) до 1,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);
}

Подход «сначала клавиатура» в дудле «Hurdles 2012»

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

  1. Для каракуля нужны всего три кнопки — две для бега и одна для прыжков, но на геймпаде их, скорее всего, гораздо больше. Поэтому мы сопоставили все шестнадцать известных кнопок и два известных джойстика с этими тремя логическими функциями таким образом, который, по нашему мнению, имел наибольший смысл, чтобы люди могли бегать: чередуя кнопки 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. Мы дошли до того, что прикрепили ввод с геймпада к рисунку, вместо того, чтобы запекать его — наш цикл опроса фактически синтезирует необходимые события нажатия и нажатия клавиш (с правильным кодом клавиши) и отправляет их обратно в 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, если вы видите ошибку, которая работает неправильно или вообще не работает.

Но перед этим поиграйте с нашим дудлом Hurdles 2012 и посмотрите, насколько веселее будет играть на геймпаде. О, ты только что сказал, что можешь сделать лучше, чем 10,7 секунды? Принеси это.

дальнейшее чтение