Wprowadzenie
Niech nowicjusze grają w przygodówki na klawiaturze, niech tną owoce na ekranach dotykowych i niech udają, że tańczą jak Michael Jackson, przed swoimi wymyślnymi czujnikami ruchu. (Spoiler: nie mogą). Ale Ty jesteś inny. Wszystko jest w porządku. Jesteś mistrzem. Dla Ciebie gry zaczynają się i kończą z padem do gier w rękach.
Ale zaraz. Czy to oznacza, że nie można obsługiwać pada do gier w aplikacji internetowej? To już nie jest problem. Z pomocą 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 to tak nowa funkcja, że dopiero w zeszłym tygodniu pojawiła się w Chrome 21, a wkrótce będzie też obsługiwana w Firefoksie (obecnie jest dostępna w specjalnej wersji).
Okazało się, że to świetny moment, ponieważ niedawno mieliśmy okazję wykorzystać tę funkcję w doodle Google Bieg przez płotki 2012. W tym artykule krótko wyjaśnimy, jak dodaliśmy interfejs Gamepad API do doodle i czego się przy tym nauczyliśmy.
Tester pada do gier
Interaktywne doodle, choć są dostępne tylko przez krótki czas, są dość skomplikowane. Aby łatwiej było zademonstrować, o co chodzi, wzięliśmy kod pada do gier z doodle i stworzyliśmy prosty tester. Możesz go użyć, aby sprawdzić, czy Twój pad do gier 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ę?
Jakich padów do gier można używać?
Zasadniczo powinien działać każdy nowoczesny pad do gier, który jest natywnie obsługiwany przez Twój system. Przetestowaliśmy różne pady do gier – od kontrolerów USB mniej znanych marek na PC, przez pady do PlayStation 2 podłączone do Maca za pomocą klucza sprzętowego, aż po kontrolery Bluetooth sparowane z notebookiem z Chrome OS.
Oto zdjęcie kontrolerów, których używaliśmy do testowania doodle’a. „Tak, mamo, naprawdę tym się zajmuję w pracy”. Jeśli kontroler nie działa lub elementy sterujące są nieprawidłowo przypisane, zgłoś błąd w Chrome lub Firefox . (Przetestuj w najnowszej wersji każdej przeglądarki, aby upewnić się, że problem nie został już rozwiązany).
Wykrywanie funkcji interfejsu Gamepad API<
W Chrome:
var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;
W Firefoksie nie można jeszcze tego wykryć – wszystko opiera się na zdarzeniach, a wszystkie moduły obsługi zdarzeń muszą być dołączone do okna, co uniemożliwia działanie typowej techniki wykrywania modułów obsługi zdarzeń.
ale jesteśmy pewni, że to tylko tymczasowe. Niezwykle przydatna biblioteka Modernizr już teraz informuje 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 połączonych padach do gier
Nawet jeśli podłączysz pad do gier, nie będzie on w żaden sposób widoczny, dopóki użytkownik nie naciśnie któregoś z jego przycisków. Ma to zapobiegać fingerprintingowi, ale stanowi pewne wyzwanie dla wygody użytkowników: nie można poprosić użytkownika o naciśnięcie przycisku ani podać instrukcji dotyczących pada do gier, ponieważ nie wiadomo, czy kontroler został podłączony.
Gdy już pokonasz tę przeszkodę, czekają na Ciebie kolejne.
Ankiety
Implementacja interfejsu API w Chrome udostępnia funkcję navigator.webkitGetGamepads(), której możesz użyć, aby uzyskać listę wszystkich padów do gier podłączonych obecnie do systemu wraz z ich bieżącym stanem (przyciski i drążki). Pierwszy połączony pad do gier zostanie zwrócony jako pierwszy wpis w tablicy itd.
(To wywołanie funkcji niedawno zastąpiło tablicę, do której można było uzyskać bezpośredni dostęp – navigator.webkitGamepads[]. Na początku sierpnia 2012 r. dostęp do tej tablicy jest nadal konieczny w Chrome 21, podczas gdy 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 i będzie stopniowo udostępniane we wszystkich zainstalowanych przeglądarkach Chrome).
Zaimplementowana do tej pory 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. Użyliśmy funkcji requestAnimationFrame(), aby skonfigurować odpytywanie w najbardziej wydajny i oszczędny dla baterii sposób. W przypadku naszego doodle'a, mimo że mamy już pętlę requestAnimationFrame() do obsługi animacji, utworzyliśmy drugą, całkowicie oddzielną – była prostsza do zakodowania i nie powinna 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 pad do gier, uzyskanie jego danych może być tak proste jak:
var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];
Jeśli chcesz, aby gra była bardziej zaawansowana lub obsługiwała więcej niż 1 gracza jednocześnie, musisz dodać kilka dodatkowych wierszy kodu, aby reagować na bardziej złożone scenariusze (np. podłączenie co najmniej 2 padów do gier, odłączenie niektórych z nich w trakcie gry itp.). Aby zobaczyć, jak można rozwiązać ten problem, zapoznaj się z kodem źródłowym funkcji pollGamepads(), która służy do testowania.
Wydarzenia
Firefox używa alternatywnego, lepszego sposobu opisanego w specyfikacji Gamepad API. Zamiast prosić o odpytywanie, udostępnia 2 zdarzenia – MozGamepadConnected i MozGamepadDisconnected – które są wywoływane, gdy pad do gier zostanie podłączony (lub, dokładniej, podłączony i „ogłoszony” przez naciśnięcie dowolnego przycisku) lub odłączony. Obiekt pada do gier, który będzie nadal 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 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 pad do gier podłączony do systemu będzie reprezentowany przez obiekt, który wygląda 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, która pozwala odróżnić różne pady do gier podłączone do jednego komputera.timestamp: sygnatura czasowa ostatniej aktualizacji stanu przycisku/osi (obecnie obsługiwana tylko w Chrome).
Przyciski i drążki
Dzisiejsze pady do gier nie przypominają tych, których Twój dziadek mógł używać do ratowania księżniczki w niewłaściwym zamku – zwykle mają co najmniej 16 osobnych przycisków (niektóre dyskretne, inne analogowe) oraz 2 drążki analogowe. Interfejs Gamepad API informuje o wszystkich przyciskach i drążkach analogowych zgłaszanych przez system operacyjny.
Gdy uzyskasz bieżący stan obiektu pada do gier, możesz uzyskać dostęp do przycisków za pomocą tablic .buttons[] i drążków za pomocą tablic .axes[]. Oto wizualne podsumowanie tego, czemu odpowiadają:
Specyfikacja wymaga od przeglądarki zmapowania 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. Pamiętaj, że nie gwarantujemy, że kontroler będzie miał 16 przycisków i 4 osie – niektóre z nich mogą być po prostu niezdefiniowane.
Przyciski mogą przyjmować wartości od 0,0 (niewciśnięty) do 1,0 (całkowicie wciśnięty). Osie mają wartości od -1,0 (całkowicie w lewo lub w górę) przez 0,0 (środek) do 1,0 (całkowicie w prawo lub w dół).
Analogowy czy dyskretny?
Teoretycznie każdy przycisk może być analogowy – jest to dość powszechne na przykład w przypadku przycisków bocznych. Dlatego najlepiej jest ustawić próg, a nie tylko porównywać wartość z 1,00 (co się stanie, jeśli przycisk analogowy będzie lekko zabrudzony? Może nigdy nie osiągnąć wartości 1,00). W naszym 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);
};
Możesz też przekształcić drążki analogowe w cyfrowe. Oczywiście zawsze jest pad kierunkowy, ale Twój pad do gier 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 drążka
Wydarzenia
W niektórych przypadkach, np. w symulatorze lotu, ciągłe sprawdzanie i reagowanie na pozycje drążka lub naciśnięcia przycisków ma więcej sensu… ale w przypadku takich rzeczy jak doodle Bieg przez płotki 2012? Możesz się zastanawiać, dlaczego musisz sprawdzać przyciski w każdej klatce. Dlaczego nie mogę otrzymywać zdarzeń tak jak w przypadku klawiatury lub myszy?
Dobra wiadomość jest taka, że możesz to zrobić. Zła wiadomość jest taka, że w przyszłości. Jest w specyfikacji, ale nie została jeszcze zaimplementowana w żadnej przeglądarce.
Ankiety
Tymczasem możesz porównać bieżący i poprzedni stan i wywołać 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 „najpierw klawiatura” w doodlu Bieg przez płotki 2012
Ponieważ bez pada do gier preferowaną metodą wprowadzania danych w dzisiejszym doodlu jest klawiatura, postanowiliśmy, że pad będzie ją dość dokładnie emulować. Oznaczało to podjęcie 3 decyzji:
Doodle potrzebuje tylko 3 przycisków – 2 do biegania i 1 do skakania – ale pad do gier prawdopodobnie ma ich znacznie więcej. Dlatego przypisaliśmy wszystkie 16 znanych przycisków i 2 znane drążki do tych 3 funkcji logicznych w sposób, który naszym zdaniem miał największy sens. Dzięki temu użytkownicy mogli biegać, naciskając naprzemiennie przyciski A/B, naprzemiennie przyciski boczne, naciskając lewy/prawy przycisk na padzie kierunkowym lub gwałtownie przesuwając drążek w lewo i w prawo (niektóre z tych opcji będą oczywiście bardziej skuteczne niż inne). 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),Każde wejście analogowe potraktowaliśmy jako dyskretne, używając opisanych wcześniej funkcji progowych.
Posunęliśmy się nawet do tego, że wejście z pada do gier zostało dołączone do doodle, a nie wbudowane w niego – nasza pętla odpytywania faktycznie syntetyzuje niezbędne zdarzenia keydown i keyup (z odpowiednim kodem keyCode) 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 widoczny w przeglądarce, dopóki nie naciśniesz przycisku.
- Jeśli testujesz pada do gier w różnych przeglądarkach jednocześnie, pamiętaj, że tylko jedna z nich może go wykryć. Jeśli nie otrzymujesz żadnych zdarzeń, zamknij inne strony, które mogą go używać. Z naszego doświadczenia wynika, że czasami przeglądarka może „trzymać” kontroler, nawet jeśli zamkniesz kartę lub samą przeglądarkę. Ponowne uruchomienie systemu jest czasem jedynym sposobem na rozwiązanie problemu.
- Jak zawsze używaj Chrome Canary i odpowiedników w innych przeglądarkach, aby mieć pewność, że korzystasz z najlepszej obsługi. Jeśli zauważysz, że starsze wersje zachowują się inaczej, podejmij odpowiednie działania.
Przyszłość
Mamy nadzieję, że ten artykuł rzucił nieco światła na ten nowy interfejs API, który jest jeszcze trochę niestabilny, ale już teraz daje dużo radości.
Oprócz brakujących elementów interfejsu API (np. zdarzeń) i szerszej obsługi przeglądarek mamy nadzieję, że w przyszłości pojawią się takie funkcje jak sterowanie wibracjami, dostęp do wbudowanych żyroskopów itp. Liczymy też na większą obsługę różnych typów padów do gier – jeśli zauważysz, że któryś z nich działa nieprawidłowo lub wcale, zgłoś błąd w Chrome lub w Firefoksie.
Zanim jednak to zrobisz, zagraj w naszego doodle'a z biegiem przez płotki z 2012 roku i zobacz, o ile przyjemniej gra się za pomocą pada. Czy właśnie powiedziałaś, że możesz osiągnąć wynik lepszy niż 10,7 sekundy? Dawaj.