Управление браузером с геймпада
По мере того, как популярность HTML-игр постепенно растет, поставщики начинают внедрять новые захватывающие API, чтобы сделать игры немного приятнее как для нас, разработчиков, так и для наших конечных игроков. Одним из них является GamepadAPI, который позволяет подключить старый добрый консольный геймпад к компьютеру и использовать его для игр на основе браузера, в стиле «подключи и работай». Давайте погрузимся в!
Введение: Что такое API-интерфейс Gamepad?
Короче говоря, API Gamepad позволяет вам взаимодействовать с вашим браузером, используя контроллер игровой консоли, также известный как геймпад. Это не требует специального драйвера или плагина для работы, это так же просто, как подключи и играй!
Будучи консольным геймером, а не настольным игроком, я предпочитаю взаимодействовать с играми с помощью геймпада, и с предстоящим ростом игр на основе HTML и JavaScript это станет действительно полезным инструментом, делающим игры более доступными для ваши пользователи.
API Gamepad недоступен для публичного выпуска, но мы можем начать использовать его для себя с предварительными версиями Firefox. Поэтому, прежде чем мы застрянем, нам нужно несколько вещей.
Что вам нужно
Как я уже упоминал, API-интерфейс Gamepad еще не доступен для публичного выпуска, поэтому вам нужно сначала получить себе Nightly-сборку Firefox и убедиться, что у вас установлено дополнение Firebug (только для целей отладки).
Кроме того, вы не можете забыть геймпад! Я собираюсь использовать контроллер PlayStation 3 для этого урока, но контроллер Xbox подойдет просто отлично.
После того, как вы установили Nightly и добавили Firebug, вы готовы к работе!
(NB. Последние сборки Chromium также имеют поддержку Gamepad API, но это руководство не было проверено на них.)
Шаг 1: Подключение геймпада к вашему браузеру
Давайте начнем с базового HTML-файла (index.html) с источником “gamepad.js” (пустой файл JavaScript).
Если вы разрабатываете игры на HTML и JavaScript, то эта статья для вас. Мы уже много писали о том, что под Windows 8.x можно разрабатывать приложения на HTML/JS, причем, как правило, вы можете с легкостью просто взять и использовать ваш текущий движок, работающий в современных браузерах.
Просто в качестве примера: если вы делаете платформер, то вы можете воспользоваться таким движком, как Phaser (кстати, он поддерживает разработку на TypeScript!), или, нашим Platformer Game StarterKit для Windows 8. К слову, если вы хотите сделать игрушку в жанре Tower Defense, то у нас есть еще один Starter Kit. А если вы хотите создать что-то трехмерное с использованием WebGL, то наше все для вас – это Babylon.js.
GamePad
Но в этой статье я не буду рассказывать, как создать саму игру. Мы зададимся другим вопросом: как подключить к игре для Windows 8.x или в браузере геймпад? Например, игровой контроллер от Xbox 360 или Xbox One:
Будем считать, что вы уже подключили сам геймпад к своему ПК (инструкция для Xbox 360, инструкция для Xbox One). Теперь давайте разберемся, что вам нужно сделать, чтобы добавить его поддержку в своей игре.
В качестве примера я буду использовать платформер RubbaRabbit из приведенного выше стартет-кита. Мы рассмотрим два варианта: игра для Windows 8.x и игра в браузере.
Игра для Windows 8.x на HTML и JavaScript
Чтобы добавить поддержку геймпада в игру на JavaScript под Windows 8.x вам понадобится научиться работать с интерфейсами XInput. Это может звучать страшновато, потому что для этого нужно погрузиться в код на C++, но мы уже сделали практически все, что вам нужно, чтобы не пересекаться с ними напрямую.
Для работы вам нужно скачать пример XInput and JavaScript controller sketch. Внутри него вы легко найдете папочку с кодом на C++, в которой находится проект библиотеки-обвязки над XInput, с которой вы в свою очередь сможете работать в своей игре на JavaScript.
Этот проект вам нужно добавить в свой солюшн с игрой и добавить ссылку на него внутри проекта для Windows 8.x:
При желании вы можете залезть внутрь файлов на C++ и выяснить, что они фактически выставляют наружу очень простой интерфейс доступа к контроллеру: конструктор для получения ссылки на геймпад и функцию getState для получения его текущего состояния с проекцией на кнопки геймпада Xbox.
Теперь нужно добавить поддержку геймпада в самой игре. Для этого, обычно, нужно сделать довольно небольшие изменения в обработке ввода со стороны игрока.
Игровой цикл
Как правило, внутри кода игры у вас есть регулярный цикл, в рамках которого формируется каждый следующий кадр игры. Для того, чтобы понять, как изменилась игровая обстановка, вы в частности проверяете, нажал ли пользователь определенные кнопки.
В моем случае игровой цикл задается внутри функции update:
Где-то внутри этой функции есть обработка нажатий на кнопки, которая выглядит примерно так:
Как вы можете заметить, разработчики движка уже позаботились о том, что пользователь может передавать команды не только с разных кнопок с клавиатуры, но и, например, с виртуальных кнопок на сенсорном устройстве (переменные touchleft, touchright и т.п.).
В целом, скорее всего, в вашем коде должны быть какие-то переменные, отвечающие различным «игровым действиям» и аккумулирующие в себе различные способы ввода для их активации. Именно на работу с такими переменными и будет завязано добавление поддержки геймпада.
Поддержка геймпада
Чтобы добавить поддержку геймпада осталось совсем немного: нужно инициализировать работу с контроллером при старте игрушки и далее регулярно получать его состояние.
Для инициализации контроллера мы создаем объект Controller через конструктор, доступный нам из подключенной ранее библиотеки на C++:
Далее нам нужно с определенной периодичностью обновлять состояние контроллера, для этого опишем функцию updateState, которая будет запрашивать вызов себя на каждый кадр анимации:
Это весь(!) код, который необходимо добавить в игрушку, чтобы она научилась взаимодействовать с геймпадом от Xbox 360. Не забудьте только при старте вызвать саму функцию initXboxpad.
Обратите внимание, что в данном случае мы «эксплуатируем» уже существующие переменные, аккумулирующие в себе команды от возможного сенсорного интерфейса, и обновляем их в зависимости от того, каково текущее состояние элементов управления на геймпаде. Например, если игрок нажал кнопку «A», то соответствующее состояние state.a будет равно true и мы его проецируем на «прыжок» в игре.
Кстати, через функцию setState геймпада вы можете заставить его вибрировать.
Игра в браузере
Теперь давайте посмотрим, что мы можем сделать в браузере. Для браузеров в W3C разрабатывается специальный стандарт Gamepad API, который позволит единообразно работать с разными типами игровых геймпадов.
Стандарт предполагает, что есть некоторая «общая модель», к которой можно свести разные игровые контроллеры:
Если вы хотите привязать обозначения к конкретному типу устройства (например, геймпаду Xbox), вам это нужно будет сделать самостоятельно, прописав в своем коде соответствующее отображение.
В сам стандарт в этой статье погружаться не буду, благо в интернете уже есть достаточное количество обзорных статьей. Например, вот документация от Mozilla. Единственное, что хочу тут отметить – это то, что она устарела относительно предположения, что Gamepad API не поддерживает в Internet Explorer. На самом деле, в свежих сборках Internet Explorer 11 Gamepad API уже поддерживается.
В контексте данной статьи про стандарт нужно знать только одно: он предоставляет доступ к сырым данным, разбитым по осям и нумерованным кнопкам. Чтобы понять реальное соответствие, вам нужно будет проделать определенные умственные операции в своем коде.
Игровой цикл и код игры
Для демонстрации я продолжаю использовать тот же пример из стартер кита, только на этот раз создаю пустой веб-проект и копирую в него оригинальные исходные файлы. Так как они не имеют на самом деле никакой завязки на платформу Windows, то игра просто работает в браузере:
Все остальное пока остается неизменными, и верны те же самые предположения про игровой цикл, которые мы делали для проекта под Windows 8.
Поддержка геймпада
Чтобы упростить себе работу с геймпадом, я воспользуюсь готовым кодом из библиотеки Babylon.js – babylon.gamepads.ts (GitHub). Вы можете просто скопировать библиотеку себе или сделать ее форк.
- умеет поверх Gamepad API симулировать событийную модель (если вам вдруг нужно);
- упрощает доступ к отдельными элементами контроллера (например, объединяет оси джойстика в один объект);
- различает контроллер Xbox, делая необходимое мне отображение обобщенных кнопок на конкретные.
Не забудьте подключить библиотеку на страницу с игрой:
Далее схема подключения геймпада очень похожа на то, что мы делали в случае с Windows 8.x:
Внутрь функции BABYLON.Gamepads передается обработчик события подключения геймпада у компьютеру. Как видите, добавление в проект поддежки геймпада – это примерно 20 строчек кода!
Вариант кода с событийной моделью:
В результате мы легко можем управлять действиями героя в игре прямо с подключенного геймпада:
Что есть в GamepadAPI?
Поддерживается API в фаерфоксе, в хромиуме, опере.
В полной версии:
navigator.getGamepads(); возвращают массив джойстиков, объектов Gamepad.
События подключения и отключения джойстика в объекте window (именно, получение джойстиков из navigator , а события в window ): "gamepadconnected", "gamepaddisconnected" .
в функцию передаётся объект события, где свойство e.gamepad — джойстик, который подключился или отключился.
- id содержит vendor id, product id (USB) и описание. Формат записи не регламентирован;
- index которое по счёту подключение;
- mapping строка, в которой пишется был ли ремапинг и если да, то какой;
- connected подключен ли джойстик;
- timestamp DOMHighResTimeStamp когда последний раз обновлялись данные по джойстику;
- axes массив осей и значений от -1 до 1;
- buttons массив кнопок, объектов, содержащих pressed (boolean) и value [0; 1] т.к. у триггеров может быть плавное изменение значения, то это следует учесть иногда.
- никакого connected ;
- никаких событий подключения и отключения. Хуже: чтобы массив, возвращаемый navigator.webkitGetGamepads() проявился, во время вызова этой функции джойстик должен быть активен (например нажата кнопка);
- maping пуст, хотя ремаппинг есть;
- buttons массив значений, а не объектов.
- Если модель джойстика неизвестна, то её вообще нет. В этом случае фф хотя бы даёт интерфейс «как есть», правда он все модели не знает (в исходниках посмотрев видно, что он обращается к API ОС чтобы работать с джойстиками стандартно и без заморочек);
- Объекты Gamepad не обновляются, пока не вызван navigator.webkitGetGamepads() или navigator.getGamepads() (если он есть, при чём, если он есть, а вызвать старую версию, то будет брошено «внимание» и вообще ничего не обновится). Т.е. получив объект из функции не обязательно получать его заново, но обязательно просто вызывать эту функцию.
Что же и как совершенствовал?
Писать я решил на coffeescript.
Он мне ближе, в нём есть классы, (так же я допилил немного процессор и выложил его, теперь в нём есть почти полноценный Си-шный препроцессор!) Поэтому и примеры дальше на кофескрипте.
Кто не знаком с таковым, но знаком с РНР, препроцессор включает файлы аналогично include и определяет константы аналогично define, то тут они есть. Нормальное описание о препроцессоре си можете найти у Кернигана и Ричи, а так же на просторах всемирной паутины.
Тем кто знаком скажу, что define в функциональном стиле не заработает, а так же передавать определения через командную строку (-DDEBUG например) пока нельзя. (папки включений же можно). В остальном стандарт реализовывал предельно близко к С++11, включая папки включения, замены в заменах, условные операторы. Но исходных констант нет, а include сохраняет отступы (включает файл, добавляя отступы перед строками, равные отступу, на котором написана директива. Нужно из-за синтаксиса языка).
- Ассоциации элементов или маппинг. В фаерфоксе его нет, в хромиуме есть.
- Отсутствие событийности. Нельзя взять и навесить слушатель на кнопку или стик.
Ассоциации элементов или маппинг.
- dpad или, в народе, крестовина
- lrtb триггеры и бамперы (не знаю, как назвать)
- menu кнопки меню
- axes стики и их кнопки
- face главные экшн-кнопки
Нагло взяв исходные коды ассоциаций кнопок из проекта хромиум я создал карты ассоциаций для джойстиков. Оказывается, они зависят от платформ, а значит для окошек и для пингвина они отличаются от макинтошей. Но что делать, если это новый и/или малоизвестный джойстик? На этот случай класс GamepadMap вынес отдельно. Объект, созданный из этого класса, можно передать в конструктор интерфейса.
Но не всегда всё так плохо! Бывает, что ассоциации в норме. Чтобы отличать готовый маппинг от сырого, ориентируюсь по количеству «осей». В случае, если их не 4 (вертикальная и горизонтальная для каждого из двух стиков), то пытаюсь найти карту ассоциаций получив из свойства "id" VID и PID. Это не безопасно с одной стороны, но с другой параметра лучше найти не смог. Даже значение параметра «mapping» не даёт ничего: в хромиуме, роботающим только с префиксом webkit, этот параметр пуст, но ассоциации уже готовы, как писал выше.
Внедряем событийность.
Единственные события, которые есть в GamepadAPI это gamepadconnected и gamepaddisconnected . Нажатия на кнопки и изменения в стиках надо получать самостоятельно. Теоретически это полезно, но на практике не всегда удобно. Особенно, если создавать альтернативу «клавамыши».
И тут я познал дзен в 5 шагов:
Получение состояния.
Т.к. W3C не даёт вообще никаких рекомендаций на счёт изменение состояния объекта Gamepad в зависимости от реального изменения состояния, то хромиум не стал утруждаться, что в первый (на первых парах), что и во второй раз (поддерживая стандарт полностью): свойства объекта Gamepad актуализируются только при опросе через navigator.getGamepads() или navigator.webkitGetGamepads() . В огнелисе же всё проще, состояние обновляется автоматически. Поэтому если webkit, то дёргаем этот метод каждый раз перед опросом.
EventTarget интерфейс.
Захотелось воссоздать EventTarget интерфейс для элементов, но нельзя просто взять и создать extends EventTarget . Пришлось «наколеночить» свою реализацию, но соблюдая стандарт. Почему не взять готовый Emet? В нём нет и близко соблюдения стандарта, а мне хотелось выполнить всё стандартно там, где это возможно.
Немного полезных методов, таких, как on, off, emet, цепочки и вуаля, класс EventTargetEmiter :
свойство _subscribe доступно извне, но это не беда, кто правит протектные свойства (с подчёркиванием) готов к выстрелу себе в ногу. К объекту можно приписать родительский объект, в который передастся «всплывающее» событие.
Event и CustomEvent.
Чтобы понять, кто вызвал событие, следует создавать Event , но просто создать Event и задать ему свойства нам не позволяют. На выручку приходит CustomEvent , в котором свойство detail настраиваемо. А чтобы событие вызывалось и в родительских элементах не забываем устанавливать canBubble в true в конструкторе.
Опрос состояний или pooling.
Во всех примерах связанных с GamepadAPI для опроса состояния используют requestAnimationFrame . В этом есть плюс и минус:
плюс в том, что когда окно не активно, то и опрашивать состояние незачем.
Но с другой стороны, если это игра, то этот вызов необходим для отрисовки, иначе может пострадать плавность анимации.
Поэтому я решил пойти алтернативным «старинным» путём: focus/blur для окна, setInterval для планировщика и единичный requestAnimationFrame для первого запуска (ведь окно может загрузиться в фоне). Таким образом, браузер сам займётся списком заданий, выполнит необходимые между отрисовками.
Один геймпад? Вы забыли, как мы играли вдвоём?
Но нам этого мало, нам бы ещё EventTargetEmiter унаследовать. Сделать это напрямую в кофескрипте не получилось. Поэтому мне помогла простенькая функция, которая передаёт методы и свойства в this :
Так получился простенький класс массива с событиями, только конструктор не принимает длину массива:
Дальше всё было относительно тривиально: блоки, кнопки, стики, создание структуры. Эту рутину, по-моему нет смысла описывать, потому что в ней нет ничего нового или нетривиального.
Итого:
Создал Gamepads для работы с джойстиками, а так же Gamepad2 и GamepadMap для ручных и тонких настроек.
Стандарт из рекомендаций и «белых пятен» это плохо. Уж очень много не очевидных моментов.
К джойстику никак нельзя обращаться из воркера. Может быть вредно, если основная логика находится в нём.
Хром старается всё преподнести в лучшем виде, но отвергать неизвестные джойстики, и это, по-моему, перебор (хотя и логичный). Мозилла даёт нам всё «как есть» и «беситесь, как хотите».
Если вы разрабатываете игры на HTML и JavaScript, то эта статья для вас. Мы уже много писали о том, что под Windows 8.x можно разрабатывать приложения на HTML/JS, причем, как правило, вы можете с легкостью просто взять и использовать ваш текущий движок, работающий в современных браузерах.
Просто в качестве примера: если вы делаете платформер, то вы можете воспользоваться таким движком, как Phaser (кстати, он поддерживает разработку на TypeScript!), или, нашим Platformer Game StarterKit для Windows 8. К слову, если вы хотите сделать игрушку в жанре Tower Defense, то у нас есть еще один Starter Kit. А если вы хотите создать что-то трехмерное с использованием WebGL, то наше все для вас – это Babylon.js.
GamePad
Но в этой статье я не буду рассказывать, как создать саму игру. Мы зададимся другим вопросом: как подключить к игре для Windows 8.x или в браузере геймпад? Например, игровой контроллер от Xbox 360 или Xbox One:
Будем считать, что вы уже подключили сам геймпад к своему ПК (инструкция для Xbox 360, инструкция для Xbox One). Теперь давайте разберемся, что вам нужно сделать, чтобы добавить его поддержку в своей игре.
В качестве примера я буду использовать платформер RubbaRabbit из приведенного выше стартет-кита. Мы рассмотрим два варианта: игра для Windows 8.x и игра в браузере.
Игра для Windows 8.x на HTML и JavaScript
Чтобы добавить поддержку геймпада в игру на JavaScript под Windows 8.x вам понадобится научиться работать с интерфейсами XInput. Это может звучать страшновато, потому что для этого нужно погрузиться в код на C++, но мы уже сделали практически все, что вам нужно, чтобы не пересекаться с ними напрямую.
Для работы вам нужно скачать пример XInput and JavaScript controller sketch. Внутри него вы легко найдете папочку с кодом на C++, в которой находится проект библиотеки-обвязки над XInput, с которой вы в свою очередь сможете работать в своей игре на JavaScript.
Этот проект вам нужно добавить в свой солюшн с игрой и добавить ссылку на него внутри проекта для Windows 8.x:
При желании вы можете залезть внутрь файлов на C++ и выяснить, что они фактически выставляют наружу очень простой интерфейс доступа к контроллеру: конструктор для получения ссылки на геймпад и функцию getState для получения его текущего состояния с проекцией на кнопки геймпада Xbox.
Теперь нужно добавить поддержку геймпада в самой игре. Для этого, обычно, нужно сделать довольно небольшие изменения в обработке ввода со стороны игрока.
Игровой цикл
Как правило, внутри кода игры у вас есть регулярный цикл, в рамках которого формируется каждый следующий кадр игры. Для того, чтобы понять, как изменилась игровая обстановка, вы в частности проверяете, нажал ли пользователь определенные кнопки.
В моем случае игровой цикл задается внутри функции update:
Где-то внутри этой функции есть обработка нажатий на кнопки, которая выглядит примерно так:
Как вы можете заметить, разработчики движка уже позаботились о том, что пользователь может передавать команды не только с разных кнопок с клавиатуры, но и, например, с виртуальных кнопок на сенсорном устройстве (переменные touchleft, touchright и т.п.).
В целом, скорее всего, в вашем коде должны быть какие-то переменные, отвечающие различным «игровым действиям» и аккумулирующие в себе различные способы ввода для их активации. Именно на работу с такими переменными и будет завязано добавление поддержки геймпада.
Поддержка геймпада
Чтобы добавить поддержку геймпада осталось совсем немного: нужно инициализировать работу с контроллером при старте игрушки и далее регулярно получать его состояние.
Для инициализации контроллера мы создаем объект Controller через конструктор, доступный нам из подключенной ранее библиотеки на C++:
Далее нам нужно с определенной периодичностью обновлять состояние контроллера, для этого опишем функцию updateState, которая будет запрашивать вызов себя на каждый кадр анимации:
Это весь(!) код, который необходимо добавить в игрушку, чтобы она научилась взаимодействовать с геймпадом от Xbox 360. Не забудьте только при старте вызвать саму функцию initXboxpad.
Обратите внимание, что в данном случае мы «эксплуатируем» уже существующие переменные, аккумулирующие в себе команды от возможного сенсорного интерфейса, и обновляем их в зависимости от того, каково текущее состояние элементов управления на геймпаде. Например, если игрок нажал кнопку «A», то соответствующее состояние state.a будет равно true и мы его проецируем на «прыжок» в игре.
Кстати, через функцию setState геймпада вы можете заставить его вибрировать.
Игра в браузере
Теперь давайте посмотрим, что мы можем сделать в браузере. Для браузеров в W3C разрабатывается специальный стандарт Gamepad API, который позволит единообразно работать с разными типами игровых геймпадов.
Стандарт предполагает, что есть некоторая «общая модель», к которой можно свести разные игровые контроллеры:
Если вы хотите привязать обозначения к конкретному типу устройства (например, геймпаду Xbox), вам это нужно будет сделать самостоятельно, прописав в своем коде соответствующее отображение.
В сам стандарт в этой статье погружаться не буду, благо в интернете уже есть достаточное количество обзорных статьей. Например, вот документация от Mozilla. Единственное, что хочу тут отметить – это то, что она устарела относительно предположения, что Gamepad API не поддерживает в Internet Explorer. На самом деле, в свежих сборках Internet Explorer 11 Gamepad API уже поддерживается.
В контексте данной статьи про стандарт нужно знать только одно: он предоставляет доступ к сырым данным, разбитым по осям и нумерованным кнопкам. Чтобы понять реальное соответствие, вам нужно будет проделать определенные умственные операции в своем коде.
Игровой цикл и код игры
Для демонстрации я продолжаю использовать тот же пример из стартер кита, только на этот раз создаю пустой веб-проект и копирую в него оригинальные исходные файлы. Так как они не имеют на самом деле никакой завязки на платформу Windows, то игра просто работает в браузере:
Все остальное пока остается неизменными, и верны те же самые предположения про игровой цикл, которые мы делали для проекта под Windows 8.
Поддержка геймпада
Чтобы упростить себе работу с геймпадом, я воспользуюсь готовым кодом из библиотеки Babylon.js – babylon.gamepads.ts (GitHub). Вы можете просто скопировать библиотеку себе или сделать ее форк.
- умеет поверх Gamepad API симулировать событийную модель (если вам вдруг нужно);
- упрощает доступ к отдельными элементами контроллера (например, объединяет оси джойстика в один объект);
- различает контроллер Xbox, делая необходимое мне отображение обобщенных кнопок на конкретные.
Не забудьте подключить библиотеку на страницу с игрой:
Далее схема подключения геймпада очень похожа на то, что мы делали в случае с Windows 8.x:
Внутрь функции BABYLON.Gamepads передается обработчик события подключения геймпада у компьютеру. Как видите, добавление в проект поддежки геймпада – это примерно 20 строчек кода!
Вариант кода с событийной моделью:
В результате мы легко можем управлять действиями героя в игре прямо с подключенного геймпада:
2014-11-11 в 13:06, admin , рубрики: coffeescript, GamePad API, javascript, Веб-разработка
Здравствуй!
Что есть в GamepadAPI?
Поддерживается API в фаерфоксе, в хромиуме, опере.
В полной версии:
navigator.getGamepads(); возвращают массив джойстиков, объектов Gamepad.
События подключения и отключения джойстика в объекте window (именно, получение джойстиков из navigator , а события в window ): "gamepadconnected", "gamepaddisconnected" .
- id содержит vendor id, product id (USB) и описание. Формат записи не регламентирован;
- index которое по счёту подключение;
- mapping строка, в которой пишется был ли ремапинг и если да, то какой;
- connected подключен ли джойстик;
- timestamp DOMHighResTimeStamp когда последний раз обновлялись данные по джойстику;
- axes массив осей и значений от -1 до 1;
- buttons массив кнопок, объектов, содержащих pressed (boolean) и value [0; 1] т.к. у триггеров может быть плавное изменение значения, то это следует учесть иногда.
Но есть две жуткие оговорки:
Т.к. хромиум пытался ввести поддержку впереди планеты всей, ориентируясь по черновикам, поэтому оговорок для браузеров, в которых только префикс webkit, больше:
- никакого connected ;
- никаких событий подключения и отключения. Хуже: чтобы массив, возвращаемый navigator.webkitGetGamepads() проявился, во время вызова этой функции джойстик должен быть активен (например нажата кнопка);
- maping пуст, хотя ремаппинг есть;
- buttons массив значений, а не объектов.
Часть проблем прошла сквозь время и проявляется даже после полной поддержки стандарта (т.е. существуют во всех версиях хромиумов, где джойстики вообще есть):
- Если модель джойстика неизвестна, то её вообще нет. В этом случае фф хотя бы даёт интерфейс «как есть», правда он все модели не знает (в исходниках посмотрев видно, что он обращается к API ОС чтобы работать с джойстиками стандартно и без заморочек);
- Объекты Gamepad не обновляются, пока не вызван navigator.webkitGetGamepads() или navigator.getGamepads() (если он есть, при чём, если он есть, а вызвать старую версию, то будет брошено «внимание» и вообще ничего не обновится). Т.е. получив объект из функции не обязательно получать его заново, но обязательно просто вызывать эту функцию.
Что же и как совершенствовал?
Писать я решил на coffeescript.
Он мне ближе, в нём есть классы, (так же я допилил немного процессор и выложил его, теперь в нём есть почти полноценный Си-шный препроцессор!) Поэтому и примеры дальше на кофескрипте.
Кто не знаком с таковым, но знаком с РНР, препроцессор включает файлы аналогично include и определяет константы аналогично define, то тут они есть. Нормальное описание о препроцессоре си можете найти у Кернигана и Ричи, а так же на просторах всемирной паутины.
Тем кто знаком скажу, что define в функциональном стиле не заработает, а так же передавать определения через командную строку (-DDEBUG например) пока нельзя. (папки включений же можно). В остальном стандарт реализовывал предельно близко к С++11, включая папки включения, замены в заменах, условные операторы. Но исходных констант нет, а include сохраняет отступы (включает файл, добавляя отступы перед строками, равные отступу, на котором написана директива. Нужно из-за синтаксиса языка).
Первые две проблемы, которые вылезли сразу:
- Ассоциации элементов или маппинг. В фаерфоксе его нет, в хромиуме есть.
- Отсутствие событийности. Нельзя взять и навесить слушатель на кнопку или стик.
Ассоциации элементов или маппинг.
Для удобства я разделил кнопки джойстика на логические блоки.
- dpad или, в народе, крестовина
- lrtb триггеры и бамперы (не знаю, как назвать)
- menu кнопки меню
- axes стики и их кнопки
- face главные экшн-кнопки
Сделано это ещё для того, чтобы отслеживать изменения у группы элементов.
Нагло взяв исходные коды ассоциаций кнопок из проекта хромиум я создал карты ассоциаций для джойстиков. Оказывается, они зависят от платформ, а значит для окошек и для пингвина они отличаются от макинтошей. Но что делать, если это новый и/или малоизвестный джойстик? На этот случай класс GamepadMap вынес отдельно. Объект, созданный из этого класса, можно передать в конструктор интерфейса.
Но не всегда всё так плохо! Бывает, что ассоциации в норме. Чтобы отличать готовый маппинг от сырого, ориентируюсь по количеству «осей». В случае, если их не 4 (вертикальная и горизонтальная для каждого из двух стиков), то пытаюсь найти карту ассоциаций получив из свойства "id" VID и PID. Это не безопасно с одной стороны, но с другой параметра лучше найти не смог. Даже значение параметра «mapping» не даёт ничего: в хромиуме, роботающим только с префиксом webkit, этот параметр пуст, но ассоциации уже готовы, как писал выше.
Внедряем событийность.
Единственные события, которые есть в GamepadAPI это gamepadconnected и gamepaddisconnected . Нажатия на кнопки и изменения в стиках надо получать самостоятельно. Теоретически это полезно, но на практике не всегда удобно. Особенно, если создавать альтернативу «клавамыши».
И тут я познал дзен в 5 шагов:
Получение состояния.
Т.к. W3C не даёт вообще никаких рекомендаций на счёт изменение состояния объекта Gamepad в зависимости от реального изменения состояния, то хромиум не стал утруждаться, что в первый (на первых парах), что и во второй раз (поддерживая стандарт полностью): свойства объекта Gamepad актуализируются только при опросе через navigator.getGamepads() или navigator.webkitGetGamepads() . В огнелисе же всё проще, состояние обновляется автоматически. Поэтому если webkit, то дёргаем этот метод каждый раз перед опросом.
EventTarget интерфейс.
Захотелось воссоздать EventTarget интерфейс для элементов, но нельзя просто взять и создать extends EventTarget . Пришлось «наколеночить» свою реализацию, но соблюдая стандарт. Почему не взять готовый Emet? В нём нет и близко соблюдения стандарта, а мне хотелось выполнить всё стандартно там, где это возможно.
Немного полезных методов, таких, как on, off, emet, цепочки и вуаля, класс EventTargetEmiter :
свойство _subscribe доступно извне, но это не беда, кто правит протектные свойства (с подчёркиванием) готов к выстрелу себе в ногу. К объекту можно приписать родительский объект, в который передастся «всплывающее» событие.
Event и CustomEvent.
Чтобы понять, кто вызвал событие, следует создавать Event , но просто создать Event и задать ему свойства нам не позволяют. На выручку приходит CustomEvent , в котором свойство detail настраиваемо. А чтобы событие вызывалось и в родительских элементах не забываем устанавливать canBubble в true в конструкторе.
Опрос состояний или pooling.
Во всех примерах связанных с GamepadAPI для опроса состояния используют requestAnimationFrame . В этом есть плюс и минус:
плюс в том, что когда окно не активно, то и опрашивать состояние незачем.
Но с другой стороны, если это игра, то этот вызов необходим для отрисовки, иначе может пострадать плавность анимации.
Поэтому я решил пойти алтернативным «старинным» путём: focus/blur для окна, setInterval для планировщика и единичный requestAnimationFrame для первого запуска (ведь окно может загрузиться в фоне). Таким образом, браузер сам займётся списком заданий, выполнит необходимые между отрисовками.
Один геймпад? Вы забыли, как мы играли вдвоём?
Но нам этого мало, нам бы ещё EventTargetEmiter унаследовать. Сделать это напрямую в кофескрипте не получилось. Поэтому мне помогла простенькая функция, которая передаёт методы и свойства в this :
Так получился простенький класс массива с событиями, только конструктор не принимает длину массива:
Дальше всё было относительно тривиально: блоки, кнопки, стики, создание структуры. Эту рутину, по-моему нет смысла описывать, потому что в ней нет ничего нового или нетривиального.
Итого:
Создал Gamepads для работы с джойстиками, а так же Gamepad2 и GamepadMap для ручных и тонких настроек.
Стандарт из рекомендаций и «белых пятен» это плохо. Уж очень много не очевидных моментов.
К джойстику никак нельзя обращаться из воркера. Может быть вредно, если основная логика находится в нём.
Хром старается всё преподнести в лучшем виде, но отвергать неизвестные джойстики, и это, по-моему, перебор (хотя и логичный). Мозилла даёт нам всё «как есть» и «беситесь, как хотите».
Ссылки:
Тестер
Исходный код
Coffeescript width C-preprocessor.
Читайте также: