Mit der Gamepad API die Hürden überwinden

Marcin Wichary
Marcin Wichary

Einführung

Lass die Anfänger ihre Tastaturen für Abenteuerspiele, ihre wertvollen Multi-Touch-Fingerspitzen zum Obstschneiden und ihre schicken, neumodischen Bewegungssensoren verwenden, um so zu tun, als könnten sie wie Michael Jackson tanzen. (Spoiler: Das können sie nicht.) Aber du bist anders. Du bist besser. Du bist ein echter Profi. Für dich beginnen und enden die Spiele mit einem Gamepad in der Hand.

Aber Moment. Haben Sie Pech, wenn Sie ein Gamepad in Ihrer Web-App unterstützen möchten? Das ist kein Problem mehr. Die brandneue Gamepad API schafft Abhilfe. Mit ihr können Sie den Status aller an Ihren Computer angeschlossenen Gamepad-Controller über JavaScript auslesen. Sie ist so neu, dass sie erst letzte Woche in Chrome 21 eingeführt wurde. Außerdem wird sie bald auch in Firefox unterstützt (derzeit in einem speziellen Build verfügbar).

Das war ein ziemlich gutes Timing, denn wir konnten es vor Kurzem im Google Doodle „Hürdenlauf 2012“ verwenden. In diesem Artikel wird kurz erläutert, wie wir die Gamepad API zum Doodle hinzugefügt haben und was wir dabei gelernt haben.

Google-Doodle „Hürdenlauf 2012“
Google Doodle „Hürdenlauf 2012“

Gamepad-Tester

So kurzlebig sie auch sind, interaktive Doodles sind im Hintergrund meist ziemlich komplex. Damit wir besser demonstrieren können, worüber wir sprechen, haben wir den Gamepad-Code aus dem Doodle genommen und einen einfachen Gamepad-Tester erstellt. Damit kannst du prüfen, ob dein USB-Gamepad richtig funktioniert, und dir auch ansehen, wie es funktioniert.

Welche Browser unterstützen die Funktion derzeit?

Browser Support

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

Source

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 No-Name-Marken 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

Dies ist ein Foto von einigen Controllern, die wir zum Testen unseres Doodles verwendet haben – „Ja, Mama, das ist wirklich das, was ich bei der Arbeit mache.“ Wenn dein Controller nicht funktioniert oder die Steuerung falsch zugeordnet ist, melde bitte einen Fehler für Chrome oder Firefox . (Bitte testen Sie die neueste Version des jeweiligen Browsers, um sicherzugehen, dass das Problem nicht bereits behoben wurde.)

Feature-Erkennung der Gamepad API<

In Chrome:

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

In Firefox scheint dies noch nicht möglich zu sein. Alles ist ereignisbasiert und alle Event-Handler müssen an das Fenster angehängt werden, was verhindert, dass eine typische Technik zur Erkennung von Event-Handlern funktioniert.

Wir sind uns aber sicher, dass dies nur vorübergehend ist. Das großartige Modernizr informiert Sie bereits über die Gamepad API. Wir empfehlen es daher für alle aktuellen und zukünftigen Erkennungsanforderungen:

var gamepadSupportAvailable = Modernizr.gamepads;

Informationen zu verbundenen Gamepads abrufen

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

Wenn du diese Hürde genommen hast, warten weitere auf dich.

Umfragen

Die Chrome-Implementierung der API stellt eine Funktion bereit – navigator.webkitGetGamepads() –, mit der Sie eine Liste aller derzeit mit dem System verbundenen Gamepads und deren aktuellen Status (Tasten und 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 war der Zugriff auf dieses Array in Chrome 21 noch erforderlich, während der Funktionsaufruf in Chrome 22 und höher funktioniert. In Zukunft wird der Funktionsaufruf die empfohlene Methode zur Verwendung der API sein und nach und nach in allen installierten Chrome-Browsern eingeführt.

Der bisher implementierte Teil der Spezifikation erfordert, dass Sie den Status verbundener Gamepads kontinuierlich prüfen und bei Bedarf mit dem vorherigen Status vergleichen, anstatt Ereignisse auszulösen, wenn sich etwas ändert. Wir haben requestAnimationFrame() verwendet, um die Abfrage so effizient und akkuschonend wie möglich zu gestalten. 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. Das war einfacher zu programmieren und sollte die Leistung in keiner Weise beeinträchtigen.

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 du nur ein Gamepad hast, kannst du die Daten so abrufen:

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

Wenn du etwas cleverer vorgehen oder mehr als einen Spieler gleichzeitig unterstützen möchtest, musst du ein paar weitere Codezeilen hinzufügen, um auf komplexere Szenarien zu reagieren (z. B. zwei oder mehr Gamepads verbunden, einige davon werden während des Spiels getrennt usw.). Eine Möglichkeit, dieses Problem zu lösen, finden Sie im Quellcode unseres Testers, Funktion pollGamepads().

Ereignisse

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

Aus dem Tester-Quellcode:

/**
 * 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 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 ersten Felder sind einfache Metadaten:

  • id: eine Textbeschreibung des Gamepads
  • index: eine Ganzzahl, mit der verschiedene Gamepads, die an einen Computer angeschlossen sind, voneinander unterschieden werden können
  • timestamp: der Zeitstempel der letzten Aktualisierung des Status der Tasten/Achsen (derzeit nur in Chrome unterstützt)

Tasten und Sticks

Die heutigen Gamepads sind nicht mehr mit denen vergleichbar, die dein Opa vielleicht verwendet hat, um die Prinzessin im falschen Schloss zu retten. Sie haben in der Regel mindestens 16 separate Tasten (einige diskret, einige analog) sowie zwei Analogsticks. Die Gamepad API informiert Sie über alle Tasten und Analogsticks, die vom Betriebssystem gemeldet werden.

Sobald du den aktuellen Status im Gamepad-Objekt hast, kannst du über die Arrays .buttons[] auf die Tasten und über die Arrays .axes[] auf die Sticks zugreifen. Hier ist eine visuelle Zusammenfassung der entsprechenden Elemente:

Gamepad-Diagramm
Gamepad-Diagramm

In der Spezifikation wird der Browser aufgefordert, die ersten 16 Tasten und vier Achsen folgenden Elementen 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 Tasten und Achsen werden an die oben genannten angehängt. Beachte jedoch, dass weder 16 Tasten noch 4 Achsen garantiert sind. Einige davon sind möglicherweise einfach nicht definiert.

Die Tasten können Werte von 0,0 (nicht gedrückt) bis 1,0 (vollständig gedrückt) annehmen. 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 zum Beispiel bei Schultertasten recht üblich. Daher ist es am besten, einen Schwellenwert festzulegen, anstatt ihn einfach mit 1,00 zu vergleichen (was ist, wenn eine analoge Taste etwas verschmutzt ist? Der Wert 1,00 wird möglicherweise nie erreicht. In unserem Doodle machen wir das so:

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 kannst du Analogsticks in digitale Joysticks umwandeln. Natürlich gibt es immer das Steuerkreuz, aber dein Gamepad hat möglicherweise keines. Hier ist der Code, um das zu bewerkstelligen:

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 manchen Fällen, z. B. bei einem Flugsimulator, ist es sinnvoller, die Positionen der Sticks oder das Drücken von Tasten kontinuierlich zu prüfen und darauf zu reagieren. Aber für Dinge wie das Doodle „Hürdenlauf 2012“? Sie fragen sich vielleicht: Warum muss ich in jedem einzelnen Frame nach Schaltflächen suchen? Warum kann ich keine Ereignisse wie für Tastatur- oder Maus-Up/Down-Ereignisse erhalten?

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

Umfragen

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-Ansatz im Doodle „Hürdenlauf 2012“

Da die bevorzugte Eingabemethode für das heutige Doodle ohne Gamepad die Tastatur ist, haben wir uns entschieden, sie mit dem Gamepad möglichst genau zu emulieren. Das bedeutete drei Entscheidungen:

  1. Für das Doodle sind nur drei Tasten erforderlich – zwei zum Laufen und eine zum Springen. Das Gamepad hat aber wahrscheinlich viel mehr Tasten. Wir haben daher alle 16 bekannten Tasten und zwei bekannten Sticks so auf diese drei logischen Funktionen abgebildet, wie es unserer Meinung nach am sinnvollsten ist. So können die Spieler durch abwechselndes Drücken der A/B-Tasten, abwechselndes Drücken der Schultertasten, Drücken der linken/rechten Taste auf dem Steuerkreuz oder heftiges Hin- und Herbewegen eines der Sticks laufen (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 die zuvor beschriebenen Schwellenwertfunktionen verwendet.

  3. Wir haben die Gamepad-Eingabe sogar an das Doodle angehängt, anstatt sie einzubinden. Unsere Abfrageschleife synthetisiert die erforderlichen Keydown- und Keyup-Ereignisse (mit einem geeigneten keyCode) und sendet sie zurück an das 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 war's auch schon!

Tipps und Tricks

  • Das Gamepad wird erst im Browser angezeigt, wenn eine Taste gedrückt wird.
  • Wenn du das Gamepad in mehreren Browsern gleichzeitig testest, kann nur einer davon den Controller erkennen. Wenn Sie keine Ereignisse erhalten, schließen Sie die anderen Seiten, auf denen die Funktion möglicherweise verwendet wird. Außerdem kann es unserer Erfahrung nach vorkommen, dass ein Browser das Gamepad „behält“, auch wenn Sie den Tab schließen oder den Browser beenden. Manchmal ist ein Neustart des Systems die einzige Möglichkeit, Probleme zu beheben.
  • Verwenden Sie wie immer Chrome Canary und die entsprechenden Versionen für andere Browser, um die beste Unterstützung zu erhalten. Wenn Sie feststellen, dass sich ältere Versionen anders verhalten, sollten Sie entsprechend reagieren.

Die Zukunft

Wir hoffen, dass wir euch diese neue API etwas näherbringen konnten. Sie ist zwar noch etwas unbeständig, macht aber schon viel Spaß.

Neben den fehlenden Teilen der API (z. B. Ereignisse) und einer breiteren Browserunterstützung hoffen wir, dass wir irgendwann auch Dinge wie Rumble-Steuerung, Zugriff auf integrierte Gyroskope usw. sehen werden. Und mehr Unterstützung für verschiedene Arten von Gamepads – bitte melden Sie einen Fehler für Chrome und/oder melden Sie einen Fehler für Firefox, wenn Sie einen finden, der nicht richtig oder gar nicht funktioniert.

Aber vorher probieren Sie doch einmal unser Hürdenlauf-Doodle von 2012 aus und sehen Sie, wie viel mehr Spaß es mit dem Gamepad macht. Oh, hast du gerade gesagt, dass du schneller als 10,7 Sekunden sein kannst? Los gehts.

Weitere Informationen