Mit der Gamepad API die Hürden überwinden

Marcin Wichary
Marcin Wichary

Einführung

Lassen Sie die Neulinge ihre Tastaturen für Abenteuerspiele, ihre wertvollen Multi-Touch-Fingerspitzen zum Schneiden von Obst und ihre ausgefallenen neuen Bewegungssensoren, um so zu tun, als könnten sie wie Michael Jackson tanzen. (Achtung: Das ist nicht möglich.) Aber Sie sind anders. Sie sind besser. Du bist ein echter Profi. Für Sie beginnen und enden die Spiele mit einem Gamepad in der Hand.

Aber Moment. Ist es nicht unmöglich, ein Gamepad in Ihrer Webanwendung zu unterstützen? Zum Glück nicht. Die brandneue Gamepad API kommt hier ins Spiel. Mit ihr können Sie den Status eines an Ihren Computer angeschlossenen Gamepads mit JavaScript lesen. Die Funktion ist so neu, dass sie erst letzte Woche in Chrome 21 eingeführt wurde. Sie wird demnächst auch in Firefox unterstützt (derzeit in einer besonderen Version verfügbar).

Das war ein glücklicher Zufall, denn wir konnten das Bild vor Kurzem für das Google-Doodle zum Thema Hürdenlauf 2012 verwenden. In diesem Artikel wird kurz erklärt, wie wir die Gamepad API dem Doodle hinzugefügt haben und was wir dabei gelernt haben.

Google-Doodle zum Thema Hürdenlauf 2012
Hürdenlauf-Google-Doodle 2012

Gamepad-Tester

Interaktive Doodles sind zwar kurzlebig, aber unter der Haube ziemlich komplex. Damit es einfacher ist, zu zeigen, wovon wir sprechen, haben wir den Gamepad-Code aus dem Doodle verwendet und einen einfachen Gamepad-Tester zusammengestellt. So können Sie prüfen, ob Ihr USB-Gamepad richtig funktioniert, und sich auch die Funktionsweise genauer ansehen.

Welche Browser unterstützen das derzeit?

Unterstützte Browser

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

Quelle

Welche Gamepads können verwendet werden?

Im Allgemeinen sollte jedes moderne Gamepad funktionieren, das von Ihrem System nativ unterstützt wird. Wir haben verschiedene Gamepads getestet, von USB-Controllern von Drittanbietern auf einem PC über PlayStation 2-Gamepads, die über einen Dongle mit einem Mac verbunden waren, bis hin zu Bluetooth-Controllern, die mit einem ChromeOS-Notebook gekoppelt waren.

Gamepads
Gamepads

Das ist ein Foto einiger Controller, die wir zum Testen unseres Doodels verwendet haben. „Ja, Mama, das ist wirklich das, was ich bei der Arbeit mache.“ Wenn Ihr Controller nicht funktioniert oder die Steuerelemente falsch zugeordnet sind, melden Sie bitte einen Fehler in Chrome oder Firefox . Bitte testen Sie in der jeweils neuesten Version der einzelnen Browser, um sicherzugehen, dass das Problem nicht bereits behoben wurde.

Feature Detecting the Gamepad API<

In Chrome geht das ganz einfach:

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

In Firefox ist das derzeit nicht möglich. Alles ist ereignisbasiert und alle Ereignishandler müssen an das Fenster angehängt werden. Das verhindert, dass eine gängige Methode zum Erkennen von Ereignishandlern funktioniert.

Wir sind uns aber sicher, dass das nur vorübergehend ist. Die immer wieder nützliche Modernizr informiert Sie bereits über die Gamepad API. Wir empfehlen sie für alle aktuellen und zukünftigen Erkennungsanforderungen:

var gamepadSupportAvailable = Modernizr.gamepads;

Informationen zu verbundenen Gamepads

Auch wenn Sie das Gamepad verbinden, wird es erst dann angezeigt, wenn der Nutzer eine der Tasten drückt. Dies soll das Fingerprinting verhindern, stellt aber eine kleine Herausforderung für die Nutzerfreundlichkeit dar: Sie können den Nutzer nicht auffordern, die Schaltfläche zu drücken oder gamepadspezifische Anweisungen geben, da Sie nicht wissen, ob er seinen Controller angeschlossen hat.

Sobald Sie diese Hürde genommen haben (sorry…), warten jedoch noch weitere Herausforderungen auf Sie.

Polling

Die Chrome-Implementierung der API stellt die Funktion navigator.webkitGetGamepads() bereit, mit der Sie eine Liste aller derzeit an das System angeschlossenen Gamepads sowie deren aktuellen Status (Tasten + Sticks) abrufen können. Das erste verbundene Gamepad wird als erster Eintrag im Array zurückgegeben usw.

(Dieser Funktionsaufruf hat vor Kurzem ein Array ersetzt, auf das Sie direkt zugreifen konnten: navigator.webkitGamepads[]. Anfang August 2012 ist der Zugriff auf dieses Array in Chrome 21 noch erforderlich, während der Funktionsaufruf in Chrome 22 und höher funktioniert. Künftig wird der Funktionsaufruf als empfohlene Methode zur Verwendung der API empfohlen und wird nach und nach in allen installierten Chrome-Browsern eingeführt.)

Der bisher implementierte Teil der Spezifikation erfordert, dass Sie den Status der verbundenen Gamepads kontinuierlich prüfen und bei Bedarf mit dem vorherigen vergleichen, anstatt Ereignisse auszulösen, wenn sich etwas ändert. Wir haben requestAnimationFrame() verwendet, um das Polling auf die effizienteste und Akku schonendste Weise einzurichten. Für unser Doodle haben wir zwar bereits eine requestAnimationFrame()-Schleife zur Unterstützung von Animationen, aber wir haben eine zweite, völlig separate erstellt. Das war einfacher zu programmieren und sollte sich nicht auf die Leistung auswirken.

Hier ist der Code des Testers:

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

Wenn Sie nur an den Daten eines Gamepads interessiert sind, können Sie sie so abrufen:

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

Wenn Sie etwas cleverer sein oder mehr als einen Spieler gleichzeitig unterstützen möchten, müssen Sie einige Codezeilen hinzufügen, um auf komplexere Szenarien zu reagieren (z. B. zwei oder mehr verbundene Gamepads, von denen einige während des Spiels getrennt werden). Im Quellcode unseres Testers, der Funktion pollGamepads(), finden Sie einen Ansatz zur Lösung dieses Problems.

Ereignisse

Firefox verwendet eine alternative, bessere Methode, die in der Gamepad API-Spezifikation beschrieben ist. Anstatt Sie zu einer Abfrage aufzufordern, werden zwei Ereignisse – MozGamepadConnected und MozGamepadDisconnected – freigegeben, die ausgelöst werden, wenn ein Gamepad angeschlossen (genauer gesagt: angeschlossen und durch Drücken einer beliebigen Taste „angekündigt“) oder getrennt wird. Das Gamepad-Objekt, das den zukünftigen Status widerspiegelt, wird als .gamepad-Parameter des Ereignisobjekts übergeben.

Über den Quellcode des Testers:

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

Zusammenfassung

Unsere Initialisierungsfunktion im Tester, die beide Ansätze unterstützt, sieht am Ende so aus:

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

Informationen zum Gamepad

Jedes mit dem System verbundene Gamepad wird durch ein Objekt dargestellt, das in etwa so aussieht:

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

Allgemeine Informationen

Die obersten Felder sind einfache Metadaten:

  • id: eine Textbeschreibung des Gamepads
  • index: Eine Ganzzahl, mit der sich verschiedene Gamepads unterscheiden lassen, die an einen Computer angeschlossen sind.
  • timestamp: Der Zeitstempel der letzten Aktualisierung des Schaltflächen-/Achsenstatus (derzeit nur in Chrome unterstützt)

Tasten und Sticks

Die heutigen Gamepads sind nicht genau das, was Ihr Großvater vielleicht verwendet hat, um die Prinzessin im falschen Schloss zu retten. Sie haben in der Regel mindestens 16 separate Tasten (einige diskret, andere analog) sowie zwei analoge Sticks. Die Gamepad API informiert Sie über alle Schaltflächen und Analogsticks, die vom Betriebssystem gemeldet werden.

Nachdem Sie den aktuellen Status im Gamepad-Objekt abgerufen haben, können Sie über .buttons[] auf die Schaltflächen und über .axes[]-Arrays auf die Sticks zugreifen. Hier eine visuelle Zusammenfassung der entsprechenden Werte:

Gamepad-Diagramm
Gamepad-Diagramm

Die Spezifikation fordert den Browser auf, die ersten 16 Schaltflächen und vier Achsen so zuzuordnen:

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

Die zusätzlichen Schaltflächen und Achsen werden an die oben genannten angehängt. Beachten Sie jedoch, dass weder 16 Tasten noch 4 Achsen garantiert sind. Einige davon sind möglicherweise einfach nicht definiert.

Die Schaltflächen können Werte von 0,0 (nicht gedrückt) bis 1,0 (vollständig gedrückt) haben. Die Achsen reichen von -1,0 (ganz links oder oben) über 0,0 (Mitte) bis 1,0 (ganz rechts oder unten).

Analog oder diskret?

Theoretisch könnte jede Taste analog sein – das ist beispielsweise bei Schultertasten üblich. Daher ist es am besten, einen Grenzwert festzulegen, anstatt den Wert einfach mit 1,00 zu vergleichen.Was ist, wenn eine analoge Taste leicht verschmutzt ist? Sie erreicht möglicherweise nie den Wert 1,00. In unserem Beispiel gehen wir so vor:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

Analoge Sticks können Sie auf die gleiche Weise in digitale Joysticks umwandeln. Sicherlich gibt es immer das digitale Steuerkreuz, aber Ihr Gamepad hat möglicherweise keins. Hier ist der Code dafür:

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

Tastendrücke und Stickbewegungen

Ereignisse

In einigen Fällen, z. B. in einem Flugsimulatorspiel, ist es sinnvoller, die Stickpositionen oder Tastendrücke kontinuierlich zu prüfen und darauf zu reagieren. Aber bei Dingen wie dem Doodle „Hürden 2012“? Sie fragen sich vielleicht: Warum muss ich jeden Frame auf Schaltflächen prüfen? Warum erhalte ich keine Ereignisse wie bei der Tastatur oder Maus (nach oben/unten)?

Die gute Nachricht ist: Das ist möglich. Die schlechte Nachricht ist: erst in Zukunft. Sie ist in der Spezifikation enthalten, aber noch in keinem Browser implementiert.

Polling

In der Zwischenzeit können Sie den aktuellen und den vorherigen Status vergleichen und Funktionen aufrufen, wenn Sie einen Unterschied feststellen. Beispiel:

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

Der Tastatur-first-Ansatz im Doodle „Hürden 2012“

Da ohne Gamepad die Tastatur die bevorzugte Eingabemethode für unser heutiges Doodle ist, haben wir uns entschieden, das Gamepad ziemlich genau zu emulieren. Das bedeutete drei Entscheidungen:

  1. Für das Doodle sind nur drei Schaltflächen erforderlich – zwei zum Laufen und eine zum Springen –, aber das Gamepad wird wahrscheinlich viele mehr haben. Deshalb haben wir alle 16 bekannten Tasten und 2 bekannten Sticks auf diese drei logischen Funktionen so verteilt, wie es uns am sinnvollsten erschien. So können Nutzer laufen, indem sie die A‑/B‑Tasten abwechselnd drücken, die Schultertasten abwechselnd drücken, das Steuerkreuz nach links/rechts drücken oder einen der Sticks heftig nach links und rechts schwingen. Einige dieser Methoden sind natürlich effizienter als andere. Beispiel:

    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. Wir haben jeden analogen Eingang als diskreten Eingang behandelt und dabei die zuvor beschriebenen Grenzwertfunktionen verwendet.

  3. Wir haben die Gamepad-Eingabe nicht in das Doodle integriert, sondern es an das Doodle angeschraubt. Unsere Abfrageschleife synthetisiert die erforderlichen keydown- und keyup-Ereignisse (mit einer korrekten keyCode) und sendet sie an das DOM zurück:

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

Das war's auch schon.

Tipps und Tricks

  • Das Gamepad ist erst sichtbar, wenn eine Taste gedrückt wird.
  • Wenn Sie das Gamepad in verschiedenen Browsern gleichzeitig testen, kann nur einer von ihnen den Controller erkennen. Wenn Sie keine Ereignisse erhalten, schließen Sie alle anderen Seiten, auf denen es verwendet wird. Außerdem kann es vorkommen, dass ein Browser das Gamepad „festhält“, auch wenn Sie den Tab schließen oder den Browser beenden. Manchmal ist ein Neustart des Systems die einzige Möglichkeit, das Problem zu beheben.
  • Wie immer empfehlen wir, Chrome Canary und die entsprechenden Versionen anderer Browser zu verwenden, um den bestmöglichen Support zu erhalten. Wenn Sie feststellen, dass sich ältere Versionen anders verhalten, sollten Sie entsprechend reagieren.

Die Zukunft

Wir hoffen, dass wir damit etwas Licht ins Dunkel dieser neuen API gebracht haben – noch ein bisschen wackelig, aber schon sehr viel Spaß.

Neben den fehlenden Teilen der API (z. B. Ereignisse) und einer breiteren Browserunterstützung hoffen wir auch, dass es irgendwann Funktionen wie eine Rumble-Steuerung und den Zugriff auf integrierte Gyroskope geben wird. Außerdem wünschen wir uns mehr Unterstützung für verschiedene Arten von Gamepads. Bitte melden Sie einen Fehler in Chrome und/oder melden Sie einen Fehler in Firefox, wenn Sie einen Controller finden, der nicht richtig oder gar nicht funktioniert.

Aber vorher können Sie noch unser Doodle „Hürdenlauf 2012“ ausprobieren und sehen, wie viel mehr Spaß es mit einem Gamepad macht. Oh, haben Sie gerade gesagt, dass Sie es in weniger als 10,7 Sekunden schaffen können? Los gehts.

Weitere Informationen