Nach einer weiteren langen Auszeit möchte ich euch diese Woche die HTML5 Gamepad API näher bringen. Diese lässt sich ähnlich einfach ansprechen wie die Maus und Tastatur, allerdings nicht über Events… Man benötigt eine kleine Schleife zur stetigen Abfrage der Knöpfe und Achsen. Das ist zwar „etwas anders“ als wir es kennen, bringt aber auch ein paar Vorteile mit sich – aber lest selbst 🙂

Interaktion mit Hilfe der HTML5 Gamepad API

Während sich die Eingabe von Maus und Tastatur komfortabel mit Javascript Events abfangen und weiter verarbeiten lässt arbeitet die HTML5 Gamepad API etwas anders. Das Gamepad bzw. die Eingabe muss zur Laufzeit ständig abgefragt werden. Dadurch können wir sehr einfach auf gleichzeitige Tastendrücke reagieren anstatt nur auf einzelne!
Für eine Schleife verwenden wir am besten requestAnimationFrame 🙂

Zum abkürzen des Codes an einigen Stellen (Filter, Observer, Events, …) verwende ich MooTools.

Beispiel Maus und Tastatur

An dieser Stelle noch mal ein kurzes recap, wie wir den Tastendruck unserer Tastatur abfragen können – schaut euch folgendes Snippet an:

$(document.body).addEvent('keydown', function (ev) {
    // The pressed button:
    console.log(ev.key);
});

Durch das Event auf dem body erhalten wir sämtliche Tastatureingaben, egal in welchem Kontext oder Fokus wir uns befinden.

Zum abfangen von Mausklicks sieht der Code beinahe identisch aus:

$(document.body).addEvent('click', function (ev) {
    // The clicked element:
    console.log(ev.target);
});

Wir tauschen hier lediglich keydown gegen click und schon wird jeder Klick auf der Seite den Callback durchlaufen und uns das angeklickt Element nennen.

Doch kommen wir zum eigentlichen Teil:

Abfragen des Gamepad Status

Die HTML5 Gamepad API bietet im Gegensatz zu Maus und Tastatur keine expliziten Events an um einen Tastendruck zu ermitteln, stattdessen müssen wir den Gamepad Status zur Laufzeit abgefragen.
Doch zu aller erst sollte geprüft werden ob der Browser die nötige funktionalität bereistellt – das geht mit Hilfe der folgenden Abfrage:

if (typeOf(navigator.getGamepads) === 'function') {
    // ...
}

Natürlich können wir auch Tools wie Modernizr verwenden, das sieht dann etwa so aus:

if (Modernizr.gamepads) {
    // ...
}

Sobald diese Abfrage im Code steht benötigen wir eigentlich nur noch navigator.getGamepads() zum abholen aller aktiven Gamepads und eine requestAnimationFrame Schleife in welcher wir den Status der Gamepads abfragen.

Ich verwende dazu die folgende gamepadLoop Funktion:

function gamepadLoop() {
    var gamepad = navigator.getGamepads()[0],
        buttonA = false,
        buttonB = false;

    if (gamepad) {
        // Query the gamepad buttons and axis:
        buttonA = gamepad.buttons[0].pressed;
        gamepad.buttons[0].pressed;
    }

    window.requestAnimationFrame(gamepadLoop);
}

gamepadLoop();

Dieser Code schreibt zwei Button-Status des ersten Gamepads in die lokalen Variablen buttonA und buttonB – aber das geht schöner!

Gamepad Mapping

Ich kann euch nur empfehlen ein „Mapping“ anzulegen damit ihr im späteren Verlauf des Codes auf einfache Art und Weise auf die gedrückten Knöpfe zugreifen könnt. Ich persönlich nutze das folgende Mapping für ein USB Gamepad im Nintendo (NES) Layout:

mapping = {
    'A': gamepad.buttons[1].pressed,
    'B': gamepad.buttons[0].pressed,
    'start': gamepad.buttons[9].pressed,
    'select': gamepad.buttons[8].pressed,
    'up': (gamepad.axes[1] == -1),
    'right': (gamepad.axes[0] == 1),
    'down': (gamepad.axes[1] == 1),
    'left': (gamepad.axes[0] == -1)
};

Soweit, so gut. In dieser mapping Variable stehen nun sämtliche Status der gedrückten Knöpfe drin… Doch wir wollen uns erst mal auf die tatsächlich gedrückten Knöpfe beschränken – das machen wir mit Hilf eines Filters. MooTools liefert uns hierzu eine Funktion deren Quellcode in etwa so aussehen dürfte:

Object.filter = function (obj, callback) {
    var result = {}, key;

    for (key in obj) {
        if (obj.hasOwnProperty(key) && callback(obj[key])) {
            result[key] = obj[key];
        }
    }

    return result;
};

Damit können wir den Code folgendermaßen umstellen:

window.pressedGamepadButtons = {};

function gamepadLoop() {
    var gamepad = navigator.getGamepads()[0],
        mapping;

    if (gamepad) {
        // Mapping:
        mapping = { /* ... */ };

        // Create a mapping copy with only the pressed buttons.
        window.pressedGamepadButtons = Object.filter(mapping, function (val) {
            return val;
        }) || {};
    }
    window.requestAnimationFrame(gamepadLoop);
}

gamepadLoop();

Wir schreiben also nun die tatsächlich gedrückten Buttons in eine globale Variable window.pressedGamepadButtons – funktioniert prima, aber das können wir besser 🙂

Eigene Events

Damit wir keine unschönen globalen Variablen benutzen, möchten wir eigene Events einführen – auch hier wieder: Ich nutze MooTools. Das Problem an der Sache: wir müssen erst mal „erkennen“ wann ein Knopf gedrückt wird – und genau dafür haben wir vorhin die „nicht-gedrückten“ Knöpfe gefiltert. Wir benötigen darüber hinaus lediglich eine weitere lokale Variable, die den Status der letzten Iteration gespeichert hat. Hier das gesamte Beispiel:

var lastButtons = {};

if (typeOf(navigator.getGamepads) === 'function') {
    function gamepadLoop() {
        var gamepad = navigator.getGamepads()[0], 
            mapping, 
            pressed;

        if (gamepad) {
            // Mapping:
            mapping = { /* ... */ };

            // Create a mapping copy with only the pressed buttons.
            pressed = Object.filter(mapping, function (val) {
                return val;
            }) || {};

            // Only register "new" button presses.
            if (Object.toQueryString(lastButtons) != Object.toQueryString(pressed)) {
                if (Object.getLength(pressed)) {
                    document.fireEvent('gamepadButtonDown', pressed);
                } else {
                    document.fireEvent('gamepadButtonUp');
                }

                lastButtons = Object.clone(pressed);
            }
        }

        window.requestAnimationFrame(gamepadLoop);
    }

    gamepadLoop();
}

Uuuund das war es auch schon 🙂 In unserem übrigen Code müssen wir nun nur noch auf die eigenen Events gamepadButtonDown und gamepadButtonUp reagieren – sauber! Der Vorteil dieser eigenen Events bzw. der generellen Art und Weise wie Knöpfe und Achsen abgefragt werden ist ganz einfach: Wir können hiermit problemlos abfragen ob mehrere Tasten gleichzeitig gedrückt werden – je nach Anwendungsfall ist das sehr Sinnvoll!

Der HTML5 Gamepad Tester kann euch dabei helfen das Mapping eures Gamepads herauszufinden!

Goodie: XBox Controller Mapping

Als kleines Extra spendiere ich euch hier ein Mapping für den weit verbreiteten Xbox Controller:

mapping = {
	'A': gamepad.buttons[0].pressed,
	'B': gamepad.buttons[1].pressed,
	'X': gamepad.buttons[2].pressed,
	'Y': gamepad.buttons[3].pressed,
	'LB': gamepad.buttons[4].pressed,
	'RB': gamepad.buttons[5].pressed,
	'LT': gamepad.buttons[6].pressed, // Alternative: gamepad.buttons[6].value
	'RT': gamepad.buttons[7].pressed, // Alternative: gamepad.buttons[7].value
	'select': gamepad.buttons[8].pressed,
	'start': gamepad.buttons[9].pressed,
	'leftStick': gamepad.buttons[10].pressed,
	'rightStick': gamepad.buttons[11].pressed,
	'up': gamepad.buttons[12].pressed,
	'down': gamepad.buttons[13].pressed,
	'left': gamepad.buttons[14].pressed,
	'right': gamepad.buttons[15].pressed,
	'home': gamepad.buttons[16].pressed,
	'leftStickX': gamepad.axes[0],
	'leftStickY': gamepad.axes[1],
	'rightStickX': gamepad.axes[2],
	'rightStickY': gamepad.axes[3],
};

Für die buttons lässt sich, alternativ zu pressed, noch das value Attribute auslesen – das ist vor allem bei analogen Knöpfen (z.B. Trigger) sinnvoll. Diese beinhalten im Gegensatz zu digitalen Knöpfen nicht nur Werte wie 0 oder 1, sondern auch alle Werte dazwischen (je nachdem wie stark der Knopf gedrückt wird).

Ich hoffe ich konnte euch nach dieser längeren Auszeit etwas interessantes neues zeigen! Vielleicht schaffe ich es ja demnächst wieder etwas öfters neue Beiträge zu schreiben 😉 Mit Hilfe von Gamepads können wir übrigens nicht nur Spiele im Browser realisieren sondern auch eine neue Form der Navigation!
Ich habe zum Beispiel ein Dashboard und einige Easter-Eggs (Konami-Code, anyone?) mit Hilfe dieses Codes zum Leben erweckt! Das schöne dabei ist: Ein Gamepad macht an einem Dashboard eine wesentlich bessere Figur als eine komplette Tastatur 😉
Vielleicht fällt euch ja auch ein interessanter Einsatzzweck ein? Lasst es mich wissen!

Bis zum nächsten Mal wünsche ich euch, wie immer, Happy Coding!

2 comments on “HTML5 Gamepad API

  • Hey Leonard,
    wir sind Leankoala, ein junges Hamburger Software-Startup, das sich mit Web Monitoring & Testing beschäftigt.
    Bei der Optimierung von Leankoala wollen wir jetzt vermehrt die Expertise von Tech-Kollegen und Kolleginnen mit einbeziehen, sodass wir aktuell auf der Suche nach 30 Tech & Web -Bloggern sind, die unser Tool ausprobieren.
    Solltest du Lust darauf haben, würden wir dir unser Tool kostenlos für mindestens ein Jahr zur Verfügung stellen.
    Das einzige was wir uns von dir wünschen ist Feedback!
    Falls du Interesse hast, kann ich dir gerne mehr über uns und unser Tool erzählen.

    Viele Grüße
    Christian

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.