Pokonywanie przeszkód dzięki interfejsowi Gamepad API

Marcin Wichary
Marcin Wichary

Wprowadzenie

Pozwól początkującym zachować klawiaturę do gier przygodowych, cenne palce dotykowe do krojenia owoców i nowoczesne czujniki ruchu do udawania, że potrafisz tańczyć jak Michael Jackson. (Nie mogą tego zrobić). Ale ty jesteś inny. Nie ma sprawy. Jesteś mistrzem. Dla Ciebie gry zaczynają się i kończą na kontrolerze w rękach.

Ale chwila. Czy nie masz możliwości obsługi kontrolera w aplikacji internetowej? Już nie. Na ratunek przychodzi zupełnie nowy interfejs Gamepad API, który umożliwia odczytywanie stanu dowolnego kontrolera do gier podłączonego do komputera za pomocą JavaScriptu. Jest tak świeża, że trafiła do Chrome 21 dopiero w zeszłym tygodniu. Wkrótce będzie też obsługiwana w Firefoksie (obecnie dostępna w wersji specjalnej).

Okazało się, że był to świetny moment, ponieważ niedawno mogliśmy go użyć w Google Doodle z okazji Światowego Dnia Wyścigów przez Płot w 2012 r.. Z tego artykułu dowiesz się, jak dodaliśmy do doodle interfejs Gamepad API i co udało nam się w tym czasie dowiedzieć.

Google Doodle na temat lekkiej atletyki w 2012 r.
Hurdles 2012 Google doodle

Tester pada do gier

Mimo że są ulotne, interaktywne doodle są pod maską dość skomplikowane. Aby ułatwić Ci zrozumienie, o czym mówimy, wykorzystaliśmy kod kontrolera z doodle i utworzyliśmy prosty test kontrolera. Możesz go użyć, aby sprawdzić, czy kontroler USB działa prawidłowo, a także zajrzeć pod maskę, aby zobaczyć, jak to działa.

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

Browser Support

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

Source

Jakich kontrolerów można używać?

Zazwyczaj działają wszystkie nowoczesne kontrolery, które są obsługiwane przez system. Testowaliśmy różne pady do gier USB innych firm na komputerach PC, pady PlayStation 2 podłączone przez dongle do komputerów Mac oraz pady Bluetooth sparowane z notebookami z Chrome OS.

Pady do gier
Pady do gier

To zdjęcie kontrolerów, których używaliśmy do testowania naszego rysunku. „Tak, mamo, naprawdę tak pracuję”. Jeśli kontroler nie działa lub elementy sterujące są nieprawidłowo mapowane, zgłoś błąd w Chrome lub Firefox . (przetestuj to w najnowszej wersji każdej przeglądarki, aby upewnić się, że problem został już rozwiązany).

Funkcja wykrywania interfejsu Gamepad API<

W Chrome wystarczy:

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

Wygląda na to, że nie jest to jeszcze możliwe w Firefoxie – wszystko jest oparte na zdarzeniach, a wszyscy przetwarzacze zdarzeń muszą być przywiązani do okna, co uniemożliwia działanie typowej techniki wykrywania przetwarzaczy zdarzeń.

Jesteśmy jednak pewni, że jest to sytuacja tymczasowa. Niezwykle przydatna biblioteka Modernizr zawiera już informacje o interfejsie Gamepad API, dlatego zalecamy jej używanie do wszystkich obecnych i przyszłych potrzeb związanych z wykrywaniem:

var gamepadSupportAvailable = Modernizr.gamepads;

Sprawdzanie informacji o podłączonych padach do gier

Nawet jeśli podłączysz gamepada, nie będzie on w żaden sposób widoczny, dopóki użytkownik nie naciśnie żadnego przycisku. Ma to na celu zapobieganie fingerprintingowi, ale powoduje pewne problemy w eksperymentowaniu z urządzeniem: nie możesz poprosić użytkownika o wciśnięcie przycisku ani podać instrukcji dotyczących konkretnego kontrolera, ponieważ nie wiesz, czy podłączono kontroler.

Gdy pokonasz tę przeszkodę (przepraszam…), czekają Cię jeszcze inne wyzwania.

Sondowanie

Implementacja interfejsu API w Chrome udostępnia funkcję navigator.webkitGetGamepads(), za pomocą której można uzyskać listę wszystkich podłączonych do systemu kontrolerów wraz z ich bieżącym stanem (przyciski i drążki). Pierwszy podłączony kontroler będzie zwracany jako pierwszy element tablicy itd.

(Ten wywołanie funkcji zastąpiło tablicę, do której można było uzyskać dostęp bezpośrednio – navigator.webkitGamepads[]. Na początku sierpnia 2012 r. w Chrome 21 nadal konieczny jest dostęp do tego tablicowego ciągu znaków, ale wywołanie funkcji działa w Chrome 22 i nowszych wersjach. W przyszłości wywołanie funkcji będzie zalecanym sposobem korzystania z interfejsu API. Będzie ono stopniowo udostępniane we wszystkich zainstalowanych przeglądarkach Chrome.

Dotychczas zaimplementowana część specyfikacji wymaga ciągłego sprawdzania stanu połączonych kontrolerów (i w razie potrzeby porównywania go ze stanem poprzednim) zamiast uruchamiania zdarzeń, gdy coś się zmieni. Aby skonfigurować polling w sposób najbardziej efektywny i przyjazny dla baterii, użyliśmy funkcji requestAnimationFrame(). Mimo że w przypadku naszej animacji mamy już pętlę requestAnimationFrame(), która obsługuje animacje, utworzyliśmy drugą, zupełnie osobną pętlę. Było to prostsze w zakresie kodowania i nie miało w żaden sposób wpływać 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 kontroler, możesz uzyskać jego dane w ten sposób:

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

Jeśli chcesz być nieco sprytniejszy lub obsługiwać więcej niż 1 gracza jednocześnie, musisz dodać kilka dodatkowych linii kodu, aby reagować na bardziej złożone scenariusze (podłączenie co najmniej 2 kontrolerów, odłączenie niektórych z nich w trakcie gry itp.). Aby poznać jedno z podejść do rozwiązania tego problemu, możesz zapoznać się z kodem źródłowym testera, funkcji pollGamepads().

Wydarzenia

Firefox używa alternatywnej, lepszej metody opisanej w specyfikacji interfejsu Gamepad API. Zamiast prosić o przeprowadzenie ankiety, udostępnia 2 zdarzenia – MozGamepadConnectedMozGamepadDisconnected – które są wywoływane, gdy podłączasz kontroler (a ściślej mówiąc, gdy podłączysz i „ogłosisz” to przez naciśnięcie dowolnego przycisku) lub odłączasz go. Obiekt gamepadu, który będzie odzwierciedlać jego 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 w testerze, która obsługuje 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 podłączony do systemu kontroler będzie reprezentowany przez obiekt wyglądający mniej więcej tak:

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óżniania różnych kontrolerów podłączonych do jednego komputera
  • timestamp: sygnatura czasu ostatniej aktualizacji stanu przycisku lub osi (obecnie obsługiwana tylko w Chrome)

Przyciski i palce

Dzisiejsze kontrolery nie są takie jak te, których używał Twój dziadek, aby uratować księżniczkę w niewłaściwym zamku. Mają zwykle co najmniej 16 oddzielnych przycisków (niektóre są dyskretne, a niektóre analogowe) oraz 2 joysticki analogowe. Interfejs Gamepad API podaje informacje o wszystkich przyciskach i analogowych dżojstikach, które są zgłaszane przez system operacyjny.

Po uzyskaniu bieżącego stanu w obiekcie kontrolera możesz uzyskać dostęp do przycisków za pomocą tablic .buttons[] i palców za pomocą tablic .axes[]. Oto wizualizacja tego, do czego się one odnoszą:

Schemat pada do gier
Schemat kontrolera do gier

Specyfikacja prosi przeglądarkę o przypisanie 16 przycisków i 4 osi do:

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 osi zostaną dodane do tych, które są widoczne powyżej. Pamiętaj, że nie gwarantujemy ani 16 przycisków, ani 4 osi. Niektóre z nich mogą być po prostu nieokreślone.

Przyciski mogą przyjmować wartości od 0,0 (nie wciśnięte) do 1,0 (wciśnięte całkowicie). Osie sięgają od -1,0 (całkowicie w lewo lub w górę) przez 0,0 (pośrodku) do 1,0 (całkowicie w prawo lub w dół).

Analogowe czy dyskretne?

Teoretycznie każdy przycisk może być analogowy – jest to dość powszechne w przypadku przycisków na uchwycie. Dlatego lepiej jest ustawić próg zamiast porównywać go bezpośrednio z wartością 1, 00 (co jeśli przycisk analogowy jest lekko zabrudzony? Może ona nigdy nie osiągnąć wartości 1,00). W naszym przypadku wygląda to tak:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

Możesz też w ten sam sposób przekształcić analogowe drążki w cyfrowe. Oczywiście zawsze można użyć pada cyfrowego, ale Twój kontroler może go nie mieć. Oto kod, który to obsługuje:

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 przycisków i ruchy grzybkiem;

Wydarzenia

W niektórych przypadkach, np. w grze symulacyjnej lotów, ciągłe sprawdzanie i reagowanie na pozycje kontrolera lub naciśnięcia przycisków ma sens, ale w przypadku doodle Hurdles 2012? Możesz się zastanawiać, dlaczego musisz sprawdzać przyciski w każdej klatce. Dlaczego nie mogę uzyskać zdarzeń, jak w przypadku klawiatury lub myszy?

Mamy dobrą wiadomość: możesz to zrobić. Złe wieści – w przyszłości. Jest to określone w specyfikacji, ale nie zostało jeszcze zaimplementowane w żadnej przeglądarce.

Sondowanie

Tymczasem możesz porównać bieżący stan z poprzednim i wywoływać funkcje, jeśli zauważysz różnicę. 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);
}

Podejście z użyciem klawiatury w doodle z 2012 roku na temat przeszkód

Ponieważ bez pada do gier preferowaną metodą wprowadzania danych w dzisiejszym rysunku jest klawiatura, postanowiliśmy, że pad do gier będzie ją ściśle emulował. Wymagało to podjęcia 3 rozwiązań:

  1. Rysunek potrzebuje tylko 3 przycisków – 2 do biegania i 1 do skakania – ale kontroler ma ich prawdopodobnie znacznie więcej. Dlatego wszystkie 16 znanych przycisków i 2 znane dżojstiki przyporządkowaliśmy do tych 3 funkcji logicznych w sposób, który naszym zdaniem był najbardziej odpowiedni. Dzięki temu użytkownicy mogą uruchamiać gry, naprzemiennie naciskając przyciski A/B, naprzemiennie naciskając przyciski na uchwycie, naciskając przyciski D-pad w kierunku lewo/prawo lub gwałtownie poruszając dowolnym dżojstikiem w lewo i w prawo (niektóre z tych sposobów będą oczywiście skuteczniejsze 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żdy analogowy sygnał wejściowy traktowaliśmy jako dyskretny, używając opisanych wcześniej funkcji progowych.

  3. Zamiast wstawiać sterowanie za pomocą gamepada bezpośrednio w doodle, postanowiliśmy zastosować pętlę ankiety, która syntetyzuje niezbędne zdarzenia keydown i keyup (z odpowiednim keyCode) i przesył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 dopóki nie naciśniesz przycisku, pad do gier nie będzie widoczny w przeglądarce.
  • Jeśli testujesz kontroler w różnych przeglądarkach jednocześnie, pamiętaj, że tylko jedna z nich może wykryć kontroler. Jeśli nie otrzymujesz żadnych zdarzeń, zamknij inne strony, które mogą ich używać. Z naszych doświadczeń wynika, że czasami przeglądarka może „trzymać” kontroler, nawet jeśli zamkniesz kartę lub samą przeglądarkę. Czasami ponowne uruchomienie systemu jest jedynym sposobem na rozwiązanie problemu.
  • Jak zawsze, używaj Chrome Canary i odpowiednich wersji innych przeglądarek, aby mieć pewność, że korzystasz z najlepszej obsługi. Jeśli zauważysz, że starsze wersje zachowują się inaczej, odpowiednio się zachowaj.

Przyszłość

Mamy nadzieję, że ten artykuł pomoże Ci lepiej poznać nowy interfejs API – wciąż trochę niepewny, ale już bardzo użyteczny.

Oprócz brakujących elementów interfejsu API (np.zdarzeń) i szerszego wsparcia w przypadku przeglądarek chcielibyśmy wprowadzić też takie funkcje jak kontrola wibracji, dostęp do wbudowanych żyroskopów itp. oraz większą obsługę różnych typów kontrolerów. Jeśli zauważysz, że kontroler działa nieprawidłowo lub wcale, zgłoś błąd w Chrome lub zgłoś błąd w Firefoksie.

Ale zanim to zrobisz, zagraj w doodle z 2012 roku z tematem przeszkód i sprawdź, jak dużo więcej zabawy jest z użyciem kontrolera. Czy właśnie powiedziałaś, że możesz zrobić to w mniej niż 10,7 sekundy? Dawaj.

Więcej informacji