Superare gli ostacoli con l'API Gamepad

Marcin Wichary
Marcin Wichary

Introduzione

Lascia che i principianti tengano le loro tastiere per i giochi di avventura, i loro preziosi polpastrelli multi-touch per tagliare la frutta e i loro sofisticati sensori di movimento per fingere di ballare come Michael Jackson. (Notizia bomba: non possono.) Ma tu sei diverso. Stai meglio. Sei un professionista. Per te, i giochi iniziano e finiscono con un gamepad tra le mani.

Ma aspetta. Non hai più speranze se vuoi supportare un gamepad nella tua app web? Non più. La nuovissima API Gamepad viene in soccorso e ti consente di utilizzare JavaScript per leggere lo stato di qualsiasi controller gamepad collegato al computer. È così recente che è stata introdotta in Chrome 21 solo la scorsa settimana ed è anche sul punto di essere supportata in Firefox (attualmente disponibile in una build speciale).

Il momento è stato perfetto, perché di recente abbiamo avuto la possibilità di utilizzarlo nel doodle di Google per le Olimpiadi di Londra 2012. Questo articolo spiega brevemente come abbiamo aggiunto l'API Gamepad al doodle e cosa abbiamo imparato durante la procedura.

Doodle di Google per le Olimpiadi di Londra 2012
Doodle di Google per le Olimpiadi di Londra 2012

Tester di gamepad

Essendo effimeri, i doodle interattivi tendono ad essere piuttosto complessi sotto il cofano. Per dimostrare più facilmente di cosa stiamo parlando, abbiamo preso il codice del gamepad dal doodle e abbiamo creato un semplice tester per gamepad. Puoi utilizzarlo per verificare se il gamepad USB funziona correttamente e anche per dare un'occhiata sotto il cofano per capire come funziona.

Quali browser lo supportano attualmente?

Supporto dei browser

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

Origine

Quali gamepad possono essere utilizzati?

In genere, qualsiasi gamepad moderno supportato nativamente dal sistema dovrebbe funzionare. Abbiamo testato vari gamepad da controller USB non di marca su un PC, gamepad PlayStation 2 collegati tramite un dongle a un Mac e controller Bluetooth accoppiati a un notebook Chrome OS.

Gamepad
Gamepad

Questa è una foto di alcuni controller che abbiamo utilizzato per testare il nostro doodle: "Sì, mamma, è proprio quello che faccio al lavoro". Se il controller non funziona o se i controlli non sono mappati correttamente, segnala un bug per Chrome o Firefox . Esegui il test con la versione più recente di ogni browser per assicurarti che il problema non sia già stato risolto.

Rilevamento della funzionalità dell'API Gamepad<

È abbastanza facile in Chrome:

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

Non sembra ancora possibile rilevarlo in Firefox: tutto è basato su eventi e tutti i gestori di eventi devono essere collegati alla finestra, il che impedisce il funzionamento di una tecnica tipica di rilevamento dei gestori di eventi.

ma siamo sicuri che sia temporaneo. L'ottimo Modernizr ti parla già dell'API Gamepad, quindi ti consigliamo di utilizzarlo per tutte le tue esigenze di rilevamento attuali e future:

var gamepadSupportAvailable = Modernizr.gamepads;

Informazioni sui gamepad connessi

Anche se colleghi il gamepad, non si manifesterà in alcun modo, a meno che l'utente non prema prima uno dei suoi pulsanti. Questo serve a evitare il fingerprinting, anche se rappresenta una sfida per l'esperienza utente: non puoi chiedere all'utente di premere il pulsante o fornire istruzioni specifiche per il gamepad perché non hai idea se ha collegato il controller.

Una volta superato questo ostacolo (scusa…), però, ce ne sono altri.

Sondaggi

L'implementazione dell'API di Chrome espone una funzione (navigator.webkitGetGamepads()) che puoi utilizzare per ottenere un elenco di tutti i gamepad attualmente collegati al sistema, insieme al loro stato attuale (tasti + levette). Il primo gamepad connesso verrà restituito come prima voce nell'array e così via.

Questa chiamata di funzione ha sostituito di recente un array a cui potevi accedere direttamente: navigator.webkitGamepads[]. Dall'inizio di agosto 2012, l'accesso a questo array è ancora necessario in Chrome 21, mentre la chiamata di funzione funziona in Chrome 22 e versioni successive. In futuro, la chiamata di funzione sarà il modo consigliato per utilizzare l'API e verrà implementata gradualmente in tutti i browser Chrome installati.

La parte della specifica implementata finora richiede di controllare continuamente lo stato dei gamepad connessi (e di confrontarlo con quello precedente, se necessario), anziché attivare eventi quando le cose cambiano. Abbiamo utilizzato requestAnimationFrame() per configurare il polling nel modo più efficiente e incentrato sulla batteria. Per il nostro doodle, anche se abbiamo già un ciclo requestAnimationFrame() per supportare le animazioni, ne abbiamo creato un secondo completamente separato: era più semplice da codificare e non dovrebbe influire in alcun modo sulle prestazioni.

Ecco il codice del tester:

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

Se ti interessa solo un gamepad, ottenerne i dati potrebbe essere semplice come:

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

Se vuoi essere un po' più furbo o supportare più di un giocatore contemporaneamente, dovrai aggiungere altre righe di codice per reagire a scenari più complessi (due o più gamepad connessi, alcuni dei quali si disconnettono a metà, ecc.). Puoi esaminare il codice sorgente del nostro tester, la funzione pollGamepads(), per un approccio su come risolvere il problema.

Eventi

Firefox utilizza un modo alternativo e migliore descritto nella specifica dell'API Gamepad. Anziché chiederti di eseguire il polling, espone due eventi, MozGamepadConnected e MozGamepadDisconnected, che vengono attivati ogni volta che un gamepad viene collegato (o, più precisamente, collegato e "annunciato" premendo uno dei suoi pulsanti) o scollegato. L'oggetto gamepad che continuerà a riflettere il suo stato futuro viene passato come parametro .gamepad dell'oggetto evento.

Dal codice sorgente del tester:

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

Riepilogo

Alla fine, la nostra funzione di inizializzazione nel tester, che supporta entrambi gli approcci, ha il seguente aspetto:

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

Informazioni sul gamepad

Ogni gamepad connesso al sistema sarà rappresentato da un oggetto simile al seguente:

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

Informazioni di base

I primi campi sono metadati semplici:

  • id: una descrizione testuale del gamepad
  • index: un numero intero utile per distinguere i diversi gamepad collegati a un computer
  • timestamp: il timestamp dell'ultimo aggiornamento dello stato dei pulsanti/degli assi (attualmente supportato solo in Chrome)

Pulsanti e levette

I gamepad di oggi non sono esattamente quelli che tuo nonno potrebbe aver usato per salvare la principessa nel castello sbagliato: in genere hanno almeno sedici pulsanti separati (alcuni discreti, altri analogici), oltre a due stick analogici. L'API Gamepad ti informerà su tutti i pulsanti e i joystick analogici segnalati dal sistema operativo.

Una volta ottenuto lo stato corrente nell'oggetto gamepad, puoi accedere ai pulsanti tramite .buttons[] e ai joystick tramite gli array .axes[]. Ecco un riepilogo visivo a cosa corrispondono:

Diagramma del gamepad
Diagramma del gamepad

La specifica chiede al browser di mappare prima i primi sedici pulsanti e quattro assi su:

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

I pulsanti e gli assi aggiuntivi verranno aggiunti a quelli precedenti. Tieni presente che né i sedici pulsanti né i quattro assi sono garantiti, quindi alcuni potrebbero non essere definiti.

I pulsanti possono assumere valori da 0,0 (non premuto) a 1,0 (premuto completamente). Gli assi vanno da -1,0 (completamente a sinistra o verso l'alto) a 0,0 (centro) fino a 1,0 (completamente a destra o verso il basso).

Analogico o discreto?

In teoria, ogni pulsante potrebbe essere analogico, ad esempio è abbastanza comune per i pulsanti laterali. Pertanto, è meglio impostare una soglia anziché limitarti a confrontarla direttamente con 1,00 (cosa succede se un pulsante analogico è leggermente sporco? Potrebbe non raggiungere mai il valore 1,00). Nel nostro doodle lo facciamo in questo modo:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

Puoi fare lo stesso per trasformare i joystick analogici in joystick digitali. Certo, c'è sempre il pad digitale (d-pad), ma il tuo gamepad potrebbe non averlo. Ecco il nostro codice per gestire questo problema:

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

Prenotazioni di pulsanti e movimenti del joystick

Eventi

In alcuni casi, ad esempio in un gioco di simulazione di volo, ha più senso controllare e reagire continuamente alle posizioni del joystick o alla pressione dei pulsanti… ma per cose come il doodle Ostacoli 2012? Potresti chiederti: perché devo controllare la presenza di pulsanti in ogni singolo frame? Perché non riesco a ricevere eventi come per i tasti freccia della tastiera o del mouse?

La buona notizia è che puoi. Le cattive notizie sono che sarà disponibile in futuro. È presente nelle specifiche, ma non è ancora implementata in nessun browser.

Sondaggi

Nel frattempo, la soluzione è confrontare lo stato corrente e quello precedente e chiamare le funzioni se noti differenze. Ad esempio:

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

L'approccio che dà la priorità alla tastiera nel doodle di Ostacoli 2012

Poiché senza un gamepad, il metodo di immissione preferito del nostro doodle di oggi è la tastiera, abbiamo deciso di emularlo abbastanza fedelmente con il gamepad. Ciò ha comportato tre decisioni:

  1. Il doodle ha bisogno solo di tre pulsanti: due per correre e uno per saltare, ma è probabile che il gamepad ne abbia molti di più. Abbiamo quindi mappato tutti i sedici pulsanti e i due stick noti su queste tre funzioni logiche nel modo che ci sembrava più logico, in modo che le persone potessero correre alternando i pulsanti A/B, alternando i pulsanti del braccio, premendo sinistra/destra sul d-pad o muovendo violentemente uno dei due stick a sinistra e a destra (ovviamente alcuni saranno più efficienti di altri). Ad esempio:

    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. Abbiamo trattato ogni input analogico come discreto, utilizzando le funzioni di soglia descritte in precedenza.

  3. Abbiamo persino collegato l'input del gamepad al doodle, anziché integrarlo: il nostro loop di polling sintetizza effettivamente gli eventi keydown e keyup necessari (con un keyCode appropriato) e li invia di nuovo al 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);

Ed è fatta.

Suggerimenti utili

  • Ricorda che il gamepad non sarà visibile nel browser finché non viene premuto un pulsante.
  • Se stai testando il gamepad in browser diversi contemporaneamente, tieni presente che solo uno di questi può rilevare il controller. Se non ricevi eventi, assicurati di chiudere le altre pagine che potrebbero utilizzarlo. Inoltre, dalla nostra esperienza, a volte un browser può "trattenere" il gamepad anche se chiudi la scheda o esci dal browser stesso. A volte, il riavvio del sistema è l'unico modo per risolvere il problema.
  • Come sempre, utilizza Chrome Canary e gli equivalenti per altri browser per assicurarti di ricevere la migliore assistenza e poi agisci di conseguenza se noti che le versioni precedenti si comportano in modo diverso.

Il futuro

Ci auguriamo che queste informazioni ti siano utili per capire meglio questa nuova API, ancora un po' precaria, ma già molto divertente.

Oltre ai componenti mancanti dell'API (ad es.gli eventi) e al supporto più ampio dei browser, speriamo di poter includere in futuro anche il controllo rumble, l'accesso ai giroscopi integrati e così via. Inoltre, prevediamo di supportare un maggior numero di tipi di gamepad. Segnala un bug per Chrome e/o segnala un bug per Firefox se ne trovi uno che non funziona correttamente o non funziona affatto.

Prima, però, prova il nostro doodle Ostacoli 2012 e scopri quanto è più divertente con il gamepad. Oh, hai detto che puoi fare meglio di 10,7 secondi? Diamoci dentro.

Per approfondire