Mit der Gamepad API die Hürden überwinden

Marcin Wichary
Marcin Wichary

Einleitung

Die Neulinge können die Tastatur für Abenteuerspiele, die kostbaren Fingerspitzen zum Obstschneiden und die raffinierten, neuartigen Bewegungssensoren, mit denen sie so tun, als würden sie wie Michael Jackson tanzen, die Tastatur behalten. (Das geht nicht.) Aber du bist anders. Du bist schon besser. Du bist ein echter Profi. Für dich beginnt und enden die Spiele mit einem Gamepad in deinen Händen.

Aber Moment. Pech gehabt, wenn Sie in Ihrer Webanwendung ein Gamepad unterstützen möchten? Doch das ist nicht mehr der Fall. Die brandneue Gamepad API kommt hier zur Hilfe. Sie ermöglicht es Ihnen, mit JavaScript den Status eines an Ihren Computer angeschlossenen Gamepad-Controllers zu lesen. Die Version ist so frisch, dass sie erst letzte Woche in Chrome 21 eingeführt wurde. Sie steht kurz davor, auch in Firefox unterstützt zu werden (derzeit in einem speziellen Build verfügbar).

Das war ein tolles Timing, denn wir haben jetzt die Chance, es beim Google-Doodle für Hurdles 2012 zu verwenden. In diesem Artikel wird kurz erklärt, wie wir die Gamepad API zu dem Doodle hinzugefügt haben und was wir dabei gelernt haben.

Google-Doodle für Hürden 2012
Hürden 2012 Google-Doodle

Gamepad-Tester

Flüchtige Zeichnungen, interaktive Doodles sind aber in der Regel ziemlich komplex. Damit wir einfacher zeigen können, wovon wir sprechen, haben wir den Gamepad-Code aus dem Doodle genommen und einen einfachen Gamepad-Tester erstellt. So könnt ihr sehen, ob euer USB-Gamepad richtig funktioniert – und unter der Haube sehen, wie das funktioniert.

Welche Browser unterstützen diese Funktion derzeit?

Unterstützte Browser

  • 21
  • 12
  • 29
  • 10.1

Quelle

Welche Gamepads können verwendet werden?

Im Allgemeinen sollte jedes moderne Gamepad funktionieren, das nativ von Ihrem System unterstützt wird. Wir haben verschiedene Gamepads von USB-Controllern anderer Hersteller auf einem PC getestet – ü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

Dies ist ein Foto von Controllern, die wir zum Testen unseres Doodles verwendet haben: „Ja, Mama, das mache ich wirklich bei der Arbeit.“ Wenn Ihr Controller nicht funktioniert oder die Steuerelemente falsch zugeordnet sind, melden Sie den Fehler in Chrome oder Firefox . Führen Sie einen Test mit der neuesten Version des jeweiligen Browsers durch, um sicherzustellen, dass das Problem nicht bereits behoben wurde.

Funktion zur Erkennung der Gamepad API

So einfach ist das in Chrome:

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

Es scheint in Firefox noch nicht möglich zu sein, dies zu erkennen – alles ist ereignisbasiert und alle Event-Handler müssen an das Fenster angehängt werden, wodurch eine typische Technik zur Erkennung von Event-Handlern nicht funktioniert.

Wir sind uns aber sicher, dass das vorübergehend ist. Der beeindruckende Modernizr informiert Sie bereits über die Gamepad API. Daher empfehlen wir dies für alle Ihre aktuellen und zukünftigen Erkennungsanforderungen:

var gamepadSupportAvailable = Modernizr.gamepads;

Informationen zu verbundenen Gamepads

Selbst wenn Sie das Gamepad anschließen, zeigt es sich erst dann an, wenn der Nutzer zuerst eine der Tasten drückt. Dadurch soll das Fingerprinting verhindert werden. Das ist aber für die Nutzer eine Herausforderung: Sie können den Nutzer nicht bitten, die Taste zu drücken oder Gamepad-spezifische Anweisungen zu geben, da Sie nicht wissen, ob der Controller angeschlossen wurde.

Sobald Sie die Hürde überwunden haben (Tut mir leid...), warten weitere Neuerungen auf Sie.

Polling

Die API-Implementierung in Chrome stellt die Funktion navigator.webkitGetGamepads() zur Verfügung, mit der du eine Liste aller derzeit an das System angeschlossenen Gamepads zusammen mit ihrem aktuellen Status (Schaltflächen und Sticks) abrufen kannst. 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 können: navigator.webkitGamepads[]. Seit Anfang August 2012 ist der Zugriff auf dieses Array in Chrome 21 weiterhin erforderlich, während der Funktionsaufruf in Chrome 22 und höher funktioniert. Der Funktionsaufruf ist künftig die empfohlene Methode zur Verwendung der API und wird nach und nach an alle installierten Chrome-Browser weitergegeben.)

Bei dem bisher implementierten Teil der Spezifikation müssen Sie den Status der verbundenen Gamepads kontinuierlich prüfen (und ihn bei Bedarf mit dem vorherigen vergleichen), anstatt Ereignisse auszulösen, wenn sich etwas ändert. Mit requestAnimationFrame() haben wir die Abfrage so effizient und energiesparend wie möglich eingerichtet. Für unser Doodle haben wir, obwohl wir bereits eine requestAnimationFrame()-Schleife zur Unterstützung von Animationen haben, eine zweite, völlig separate Schleife erstellt. Es war einfacher zu programmieren und sollte sich in keiner Weise 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 es Ihnen nur um ein einziges Gamepad geht, kann der Datenabruf so einfach sein:

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

Wenn Sie geschickter sein oder mehr als einen Spieler gleichzeitig unterstützen möchten, müssen Sie ein paar weitere Codezeilen hinzufügen, um auf komplexere Szenarien zu reagieren (zwei oder mehr Gamepads verbunden, einige von ihnen werden mittendrin getrennt usw.). Im Quellcode unseres Testers, der Funktion pollGamepads(), finden Sie einen Lösungsansatz.

Veranstaltungen

Firefox verwendet eine alternative, bessere Methode, die in der Gamepad-API-Spezifikation beschrieben ist. Statt eine Abfrage durchzuführen, werden zwei Ereignisse angezeigt: MozGamepadConnected und MozGamepadDisconnected. Diese werden ausgelöst, wenn ein Gamepad angeschlossen (oder genauer gesagt, eingesteckt und durch Drücken einer der Tasten angekündigt) oder nicht angeschlossen wird. Das Gamepad-Objekt, das seinen zukünftigen Status weiterhin 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

Am Ende sieht unsere Initialisierungsfunktion im Tester, die beide Ansätze unterstützt, 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();
    }
    }
},

Gamepad-Informationen

Jedes mit dem System verbundene Gamepad wird durch ein Objekt repräsentiert, 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, um zwischen verschiedenen an einen Computer angeschlossenen Gamepads zu unterscheiden
  • timestamp: Zeitstempel der letzten Aktualisierung des Schaltflächen- bzw. Achsenstatus (derzeit nur in Chrome unterstützt)

Tasten und Sticks

Die heutigen Gamepads sind nicht genau das, was dein Großvater verwendet hätte, um die Prinzessin im falschen Schloss zu retten. Sie haben normalerweise mindestens sechzehn separate Tasten (einige einzelne, einige analoge Tasten) zusätzlich zu zwei Analogsticks. Die Gamepad API informiert Sie über alle Tasten und Analogsticks, die vom Betriebssystem gemeldet werden.

Sobald Sie den aktuellen Status im Gamepad-Objekt abrufen, können Sie über .buttons[] auf die Schaltflächen und über .axes[]-Arrays auf Sticks zugreifen. Hier ist eine visuelle Zusammenfassung dessen, wofür sie sich beziehen:

Gamepad-Diagramm
Gamepad-Diagramm

Die Spezifikation fordert den Browser auf, die ersten sechzehn Schaltflächen und vier Achsen folgender Elemente 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 sechzehn Schaltflächen noch vier Achsen garantiert sind. Stellen Sie sicher, dass einige davon einfach undefiniert sind.

Für die Schaltflächen sind Werte von 0,0 (nicht gedrückt) bis 1,0 (vollständig gedrückt) möglich. Die Achsen reichen von -1,0 (vollständig links oder oben) über 0,0 (mittig) bis 1,0 (vollständig rechts oder unten).

Analog oder diskret?

Vermutlich könnte jede Taste eine analoge Taste sein. Dies ist beispielsweise bei Tasten an der Vorderseite üblich. Daher ist es am besten, einen Schwellenwert festzulegen, anstatt ihn einfach stumpf mit 1,00 zu vergleichen (was ist, wenn eine analoge Schaltfläche etwas verschmutzt ist? Der Wert wird möglicherweise nie bei „1,00“ erreicht. In unserem Doodle 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);
};

Auf die gleiche Weise können Sie analoge Sticks in digitale Joysticks verwandeln. Natürlich gibt es immer das digitale Pad (D-Pad), aber möglicherweise nicht Ihr Gamepad. Hier ist unser Code, um dies zu handhaben:

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

Veranstaltungen

In manchen Fällen, z. B. bei einem Flugsimulator, ist es sinnvoller, ständig die Stickpositionen oder das Drücken von Tasten zu prüfen und darauf zu reagieren. Aber für Dinge wie Hurdles 2012-Doodle? Sie fragen sich vielleicht: Warum muss ich bei jedem Frame nach Schaltflächen suchen? Warum kann ich keine Ereignisse wie für die Tastatur oder die Maus nach oben/unten abrufen?

Die gute Nachricht ist, dass Sie es können. Schlechte Nachrichten sind: in der Zukunft. Es ist in der Spezifikation enthalten, aber noch in keinem Browser implementiert.

Polling

In der Zwischenzeit können Sie den aktuellen mit dem vorherigen Status vergleichen und Funktionen aufrufen, falls 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 Tastaturansatz im Hurdles 2012-Doodle

Da unser Doodle heute ohne Gamepad verwendet wird, ist die Tastatur die bevorzugte Eingabemethode. Daher haben wir uns entschieden, dass das Gamepad sie eher eng emuliert. Daraus ergeben sich drei Entscheidungen:

  1. Das Doodle braucht nur drei Tasten – zwei zum Laufen und eine zum Springen – aber das Gamepad hat wahrscheinlich noch viele weitere. Aus diesem Grund haben wir alle sechzehn bekannten Tasten und zwei bekannten Sticks auf eine Weise auf diese drei logischen Funktionen platziert, die wir für am sinnvollsten erachtet haben. So konnten wir damit arbeiten: abwechselnde A/B-Tasten, abwechselnde Schultertasten, das Drücken der Steuerkreuze nach links/rechts oder das Schwingen eines der Sticks kräftig nach links und rechts (einige davon sind natürlich effizienter als die anderen). 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 jede analoge Eingabe unter Verwendung der zuvor beschriebenen Schwellenwertfunktionen als diskrete Eingabe behandelt.

  3. Wir haben die Gamepad-Eingabe nicht erst integriert, sondern synthetisiert die notwendigen Keydown- und Keyup-Ereignisse (mit einem richtigen keyCode) und zurück zum 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);

Das ist alles!

Tipps und Tricks

  • Das Gamepad wird in deinem Browser erst angezeigt, wenn eine Schaltfläche gedrückt wird.
  • Wenn Sie das Gamepad gleichzeitig in verschiedenen Browsern testen, beachten Sie, dass nur einer von ihnen den Controller erkennen kann. Falls Sie keine Ereignisse erhalten, schließen Sie alle anderen Seiten, auf denen das Ereignis möglicherweise verwendet wird. Außerdem kann ein Browser manchmal das Gamepad "festhalten", auch wenn Sie den Tab schließen oder den Browser selbst schließen. Manchmal ist ein Neustart des Systems die einzige Möglichkeit, Probleme zu beheben.
  • Nutze wie immer Chrome Canary und die entsprechenden Browser für andere Browser, um sicherzustellen, dass du die bestmögliche Unterstützung hast. Wenn du ältere Versionen bemerkst, dass sie anders verhalten, verhalten Sie sich dann entsprechend.

Die Zukunft

Wir hoffen, dass wir euch hiermit ein wenig über diese neue API informieren können. Sie ist zwar ein bisschen bedenklich, macht aber auch viel Spaß.

Neben den fehlenden API-Elementen (z. B. Ereignissen) und der umfassenderen Browserunterstützung hoffen wir auch, bald auch Funktionen wie die Rauschensteuerung oder den Zugriff auf integrierte Gyroskope anbieten zu können. Und mehr Unterstützung für verschiedene Arten von Gamepads. Bitte melde in Chrome einen Fehler und/oder melde einen Fehler in Firefox, falls du eine Funktion findest, die nicht funktioniert oder überhaupt nicht funktioniert.

Aber vorher können Sie auch unser Hurdles 2012-Doodle ausprobieren und sehen, wie viel mehr Spaß es auf dem Gamepad macht. Oh, hast du gerade gesagt, dass du das besser als 10,7 Sekunden machen könntest? Los gehts.

Weitere Informationen