Pokonywanie przeszkód dzięki interfejsowi Gamepad API

Marcin Wichary
Marcin Wichary

Wstęp

Początkujący mają do dyspozycji klawiaturę do gier przygodowych, wielodotykowe palce do cięcia owoców i nowe, fantazyjne czujniki ruchu, które umożliwiają udawanie, że potrafią tańczyć jak Michael Jackson. (Newsflash: nie może). Ale Ty się wyróżniasz. Dobrze Ci idzie. Jesteś mistrzem. Na początku i na końcu gry masz w rękach pada do gier.

Ale zaraz. Może to słuszne rozwiązanie, jeśli chcesz dodać obsługę pada do gier w swojej aplikacji internetowej? Już nie. Z pomocą przyszedł nam nowy interfejs Gamepad API, który umożliwia odczytywanie stanu każdego kontrolera podłączonego do komputera za pomocą kodu JavaScript. Jest tak świeża z prasy, że w zeszłym tygodniu pojawiła się w Chrome w wersji 21. Wkrótce będzie też obsługiwana w Firefoksie (obecnie jest dostępna w specjalnej kompilacji).

To był dobry moment, bo ostatnio mieliśmy okazję wykorzystać go w doodle Google w 2012 roku. W tym artykule pokrótce wyjaśnimy, jak dodaliśmy interfejs Gamepad API do doodla i czego dowiedzieliśmy się podczas tego procesu.

Doodle Google 2012 przez Hurdles
Doodle Google na konferencji Hudles 2012

Tester pada do gier

Doodle interaktywne są dość skomplikowane, ponieważ są efemeryczne. Aby łatwiej było zademonstrować, o czym rozmawiamy, wykorzystaliśmy kod pada do gier z doodle'a i stworzyliśmy prostego testera pada do gier. Możesz go użyć, aby sprawdzić, czy Twój pad do gier USB działa prawidłowo, a także zajrzeć pod maskę, żeby zobaczyć, jak to działa.

Które przeglądarki obecnie obsługują tę funkcję?

Obsługa przeglądarek

  • 21
  • 12
  • 29
  • 10.1

Źródło

Jakich padów do gier można używać?

Każdy nowoczesny pad do gier, który jest natywnie obsługiwany przez Twój system, powinien działać prawidłowo. Testowaliśmy różne pady do gier – od niemarkowych kontrolerów USB na PC przez pady do gier PlayStation 2 podłączone kablem do Maca, aż po kontrolery Bluetooth sparowane z notebookiem z Chrome OS.

Pady do gier
Pady do gier

To zdjęcie kilku kontrolerów, które testowaliśmy podczas testowania naszego doodla – „Tak, mamo, właśnie to robię w pracy”. Jeśli kontroler nie działa lub elementy sterujące są nieprawidłowo zmapowane, zgłoś błąd dotyczący Chrome lub Firefoksa . (Przetestuj ją w absolutnie najnowszej wersji każdej przeglądarki, aby się upewnić, że problem nie został już rozwiązany).

Feature Detecting the Gamepad API

W Chrome jest to dość proste:

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

Obecnie nie jest to możliwe w Firefoksie. Wszystko zależy od zdarzeń, a wszystkie moduły obsługi zdarzeń muszą być podłączone do okna, co uniemożliwia działanie typowej metody wykrywania modułów obsługi zdarzeń.

Jesteśmy jednak pewni, że jest to sytuacja tymczasowa. Wspaniały interfejs Modernizr już opowiada o interfejsie Gamepad API, dlatego polecamy go na potrzeby obecnych i przyszłych potrzeb związanych z wykrywaniem:

var gamepadSupportAvailable = Modernizr.gamepads;

Znajdowanie połączonych padów do gier

Nawet gdy podłączysz pada do gier, nie będzie się on w żaden sposób pojawiać, chyba że użytkownik najpierw naciśnie którykolwiek z przycisków. Ma to na celu zapobieganie odciskom odcisków cyfrowych, choć bywa to problematyczne, ponieważ nie można prosić użytkownika o naciskanie przycisku ani podawać instrukcji dotyczących konkretnego pada do gier, ponieważ nie wiadomo, czy podał kontroler.

Kiedy już poradzisz sobie z tą przeszkodą (przepraszamy...), ale to już na Ciebie czeka.

Ankiety

Implementacja interfejsu API w Chrome ujawnia funkcję – navigator.webkitGetGamepads() – która pozwala wyświetlić listę wszystkich padów do gier aktualnie podłączonych do systemu wraz z ich obecnym stanem (przyciski + gałki). Pierwszy podłączony pad do gier zostanie zwrócony jako pierwszy wpis w tablicy itd.

(Wywołanie tej funkcji niedawno zastąpiło tablicę, do której możesz uzyskać bezpośredni dostęp – navigator.webkitGamepads[]. Od początku sierpnia 2012 r. dostęp do tej tablicy jest nadal niezbędny w Chrome 21, natomiast wywołanie funkcji działa w Chrome 22 i nowszych wersjach. W przyszłości zalecanym sposobem korzystania z interfejsu API jest wywołanie funkcji. Będzie ono powoli docierać do wszystkich zainstalowanych przeglądarek Chrome.

Wcześniej zaimplementowana część specyfikacji wymaga ciągłego sprawdzania stanu połączonych padów do gier (i w razie potrzeby porównywania go z poprzednim) zamiast wywoływania zdarzeń, gdy coś się zmieni. Wykorzystanie funkcji requestAnimationFrame() pozwoliło nam skonfigurować odpytywanie w najwydajniejszy i najbardziej przyjazny dla baterii sposób. Choć mamy już pętlę requestAnimationFrame() do obsługi animacji, utworzyliśmy drugą zupełnie osobną pętlę – było łatwiejsze do kodowania i nie powinno w żaden sposób wpłynąć na wydajność.

Oto kod od testera:

/**
 * 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.)
},

Jeśli interesuje Cię tylko jeden pad do gier, pobranie danych z niego może być tak proste, jak:

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

Jeśli chcesz wykazać się większą wiedzą albo obsługiwać więcej niż jednego gracza jednocześnie, musisz dodać kilka linijek kodu, aby zareagować w bardziej złożonym scenariuszu (dwa lub więcej padów do gier podłączonych, niektóre z nich rozłączone w środku itp.). Rozwiązanie tego problemu znajdziesz w kodzie źródłowym naszego testera (funkcji pollGamepads()).

Wydarzenia

Firefox używa alternatywnego, lepszego sposobu opisanego w specyfikacji interfejsu Gamepad API. Zamiast pytania o ankietę wyświetla 2 zdarzenia – MozGamepadConnected i MozGamepadDisconnected – które są uruchamiane za każdym razem, gdy pad do gier jest podłączony (a dokładniej: po podłączeniu i „ogłoszeniu” po naciśnięciu dowolnego przycisku) lub odłączeniu. Obiekt do gier, który nadal będzie odzwierciedlać swój przyszły stan, jest przekazywany jako parametr .gamepad obiektu zdarzenia.

Z kodu źródłowego testera:

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

Podsumowanie

Ostatecznie nasza funkcja inicjowania na platformie testera, obsługująca oba podejścia, wygląda tak:

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

Informacje o padzie do gier

Każdy pad do gier podłączony do systemu będzie reprezentowany przez obiekt podobny do tego:

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

Podstawowe informacje

Kilka pierwszych pól to proste metadane:

  • id: tekstowy opis pada do gier,
  • index: liczba całkowita przydatna do rozróżnienia różnych padów do gier podłączonych do jednego komputera
  • timestamp: sygnatura czasowa ostatniej aktualizacji stanu przycisku/osi (obecnie obsługiwana tylko w Chrome).

Guziki

Dzisiejsze pady do gier nie są dokładnie tym, z czego Twój dziadek mógł uratować księżniczkę w niewłaściwym zamku – oprócz dwóch gałków analogowych mają zazwyczaj co najmniej 16 przycisków (niektóre dyskretne, a inne analogowe). Interfejs Gamepad API poinformuje Cię o wszystkich przyciskach i gałkach analogowych zgłaszanych przez system operacyjny.

Po uzyskaniu bieżącego stanu w obiekcie do gier w obiekcie do gier możesz uzyskiwać dostęp do przycisków za pomocą funkcji .buttons[] i gałków za pomocą tablic .axes[]. Oto wizualne podsumowanie ich znaczenia:

Schemat pada do gier
Wykres pada do gier

Specyfikacja prosi przeglądarkę o zmapowanie pierwszych 16 przycisków i 4 osi na:

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

Dodatkowe przyciski i osie zostaną dołączone do tych powyżej. Nie możemy jednak zagwarantować ani 16 przycisków, ani 4 osi. Przygotuj się na niezdefiniowanie niektórych z nich.

Przyciski mogą przyjmować wartości od 0,0 (nienaciśnięty) do 1,0 (naciśnięcie całkowicie). Osie przesuwają się od -1,0 (całkowicie w lewo lub w górę) do 0,0 (w środku) do 1,0 (całkowicie w prawo lub w dół).

Analogowy czy dyskretny?

Podejrzewa się, że każdy przycisk może być analogowym – jest to typowa sytuacja w przypadku np. przycisków z przodu. Dlatego lepiej ustawić wartość progową zamiast bezkrótko porównywać ją do 1,00 (co, jeśli przycisk analogowy jest lekko zabrudzony? może nigdy nie osiągnąć 1,00). W doodle robimy to w ten sposób:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

W ten sam sposób możesz zamienić joysticki analogowe w cyfrowe. Oczywiście, że zawsze jest cyfrowy pad (pad kierunkowy), ale może go nie mieć Twój pad do gier. Oto nasz kod, który to umożliwia:

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

Naciśnięcia przycisku i ruchy drążków

Wydarzenia

W niektórych przypadkach, takich jak symulator lotu, sensowne jest ciągłe sprawdzanie pozycji kijka i reagowanie na niego, ale na przykład doodle Hurdles 2012? Być może zadajesz sobie pytanie, dlaczego muszę sprawdzać przyciski w każdej klatce? Dlaczego nie mogę otrzymywać zdarzeń takich jak dzieje się w przypadku korzystania z klawiatury lub myszy w górę/w dół?

Dobra wiadomość jest taka, że tak. Zła wiadomość jest w przyszłości. Jest on w specyfikacji, ale nie został jeszcze wdrożony w żadnej przeglądarce.

Ankiety

Tymczasem możesz wychodzić z rytmu i porównywać bieżący stan z poprzednim stanem i w razie potrzeby wywoływać funkcje. Na przykład:

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

Doodle w Hurdles 2012 kładzie nacisk na klawiaturę

Bez gamepada naszą preferowaną metodą wprowadzania doodli jest klawiatura, więc zdecydowaliśmy się na raczej emulację tego pada. Wiązało się to z 3 decyzją:

  1. Doodle potrzebuje tylko 3 przycisków – 2 do biegania i 1 do skakania – ale na pada zwykle jest ich znacznie więcej. Dlatego zmapowano wszystkie szesnaście znanych przycisków i dwa znane elementy do tych trzech funkcji logicznych w sposób, który uznaliśmy za najbardziej odpowiedni. Dzięki temu można biegać za pomocą naprzemiennych przycisków A/B, naprzemiennych przycisków z boku, naciskania w lewo i w prawo na padzie kierunkowym lub gwałtownie machania gałkami w lewo i w prawo (niektóre z nich będą oczywiście bardziej efektywne od innych). Na przykład:

    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. Każde analogowe dane wejściowe traktujemy jako osobne, używając opisanych wcześniej funkcji progowych.

  3. Zamiast wkręcać dane wejściowe pada do gry, zaczęliśmy syntetyzować niezbędne zdarzenia klawiszy i zdarzeń (z odpowiednim kodem klucza) i wysyłać je z powrotem do 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);

To wszystko.

Porady i wskazówki

  • Pamiętaj, że pad do gier nie będzie w ogóle widoczny w przeglądarce przed naciśnięciem przycisku.
  • Jeśli testujesz pada do gier jednocześnie w różnych przeglądarkach, pamiętaj, że tylko jedna z nich może wykryć kontroler. Jeśli nie otrzymujesz żadnych zdarzeń, zamknij inne strony, które mogą z niego korzystać. Poza tym czasami przeglądarka „przytrzymuje” pada do gier, nawet gdy zamkniesz kartę lub samą przeglądarkę. Czasami jedynym sposobem rozwiązania problemu jest ponowne uruchomienie systemu.
  • Jak zawsze używaj Chrome Canary i jego odpowiedniki w innych przeglądarkach, aby mieć pewność, że korzystasz z najlepszej obsługi. Jeśli zauważysz, że starsze wersje działają inaczej, podejmij odpowiednie działania.

Przyszłość

Mamy nadzieję, że pomoże Ci to rzucić nieco światła na nowy interfejs API – nadal trochę niepewny, ale już pełen zabawy.

Oprócz brakujących elementów interfejsu API (np. wydarzeń) i szerszej obsługi przeglądarek mamy nadzieję, że w przyszłości uda nam się uzyskać takie funkcje jak sterowanie drżeniem, dostęp do wbudowanych żyroskopów itp. Większą obsługę różnych rodzajów padów do gier

Zanim to jednak zrobisz, pobaw się naszym doodlem Hudles 2012 i zobacz, ile jeszcze radośniej Ci to umożliwi na padzie do gier. Hmm, czy mówisz, że dasz radę lepiej niż 10,7 sekundy? Dawaj.

Więcej informacji