Superare gli ostacoli con l'API Gamepad

Marcin Wichary
Marcin Wichary

Introduzione

Lasciate che i novizi tengano le tastiere per i giochi d'avventura, le loro preziose punte da taglio multi-touch per tagliare la frutta e i loro fantasiosi sensori di movimento inediti per far finta di ballare come Michael Jackson. (NewsFlash: non possono). Ma tu sei diverso. Stai meglio. Sei un professionista. Per te, i giochi iniziano e finiscono con un gamepad nelle mani.

Ma aspetta. Non sei sfortunato se vuoi supportare un gamepad nella tua app web? Ora non più. Ti viene in aiuto la nuovissima API Gamepad, che ti consente di utilizzare JavaScript per leggere lo stato di qualsiasi controller gamepad collegato al computer. La versione è così recente dalle macchine che è approdata su Chrome 21 solo la scorsa settimana e sta per essere supportata anche in Firefox (attualmente disponibile in una versione speciale).

Si è rivelato un momento davvero eccezionale, perché abbiamo la possibilità di usarlo di recente nel doodle di Google sugli ostacoli 2012. Questo articolo spiegherà brevemente come abbiamo aggiunto l'API Gamepad al doodle e cosa abbiamo imparato durante il processo.

Doodle di Google sugli ostacoli 2012
Doodle di Google Hurdles 2012

Tester per Gamepad

Per quanto siano temporanei, i doodle interattivi tendono a essere piuttosto complessi. Per semplificare la dimostrazione di ciò di cui stiamo parlando, abbiamo estratto il codice del gamepad dal doodle e creato un semplice tester per gamepad. Potete utilizzarlo per vedere se il vostro gamepad USB funziona correttamente e anche per esaminare come funziona.

Quali browser lo supportano attualmente?

Supporto dei browser

  • 21
  • 12
  • 29
  • 10.1

Fonte

Quali gamepad possono essere utilizzati?

In genere, qualsiasi gamepad moderno supportato dal tuo sistema in modo nativo dovrebbe funzionare. Abbiamo testato diversi gamepad, da controller USB non di marca su PC, attraverso i gamepad di PlayStation 2 collegati tramite un dongle a un Mac, fino ai controller Bluetooth abbinati a un notebook Chrome OS.

Gamepad
Gamepad

Questa è una foto di alcuni controller che abbiamo usato per testare il nostro doodle: "Sì, mamma, è davvero 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 un test nella versione più recente di ogni browser per assicurarti che il problema non sia già stato risolto.

Funzionalità di rilevamento dell'API Gamepad<

Abbastanza facile in Chrome:

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

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

Ma siamo sicuri che si tratti di una situazione temporanea. L'incredibile funzionalità di Modernizr ti fornisce già informazioni sull'API Gamepad, pertanto ti consigliamo questa opzione per tutte le tue esigenze di rilevamento attuali e future:

var gamepadSupportAvailable = Modernizr.gamepads;

Informazioni sui gamepad collegati

Anche se colleghi il gamepad, non si manifesta in alcun modo a meno che l'utente non preme prima uno dei suoi pulsanti. Questo serve a evitare il fingerprinting, anche se si rivela un po' difficile per l'esperienza utente: non puoi chiedere all'utente di premere il pulsante o fornire istruzioni specifiche per il gamepad perché non sai se ha collegato il controller.

Una volta risolto l'ostacolo (scusate...), ma aspettati altri.

Sondaggi

L'implementazione dell'API in Chrome espone una funzione, navigator.webkitGetGamepads(), che puoi utilizzare per ottenere un elenco di tutti i gamepad attualmente collegati al sistema, oltre al loro stato corrente (pulsanti e levette). Verrà restituito il primo gamepad collegato come prima voce nell'array e così via.

(Questa chiamata di funzione ha appena sostituito un array a cui puoi accedere direttamente: navigator.webkitGamepads[]. All'inizio di agosto 2012, è ancora necessario accedere a questo array in Chrome 21, mentre la chiamata di funzione funziona in Chrome 22 e versioni successive. In futuro, la chiamata funzione è il modo consigliato per utilizzare l'API e verrà applicata lentamente a tutti i browser Chrome installati.)

La parte delle specifiche implementata finora richiede di controllare continuamente lo stato dei gamepad connessi (e confrontarlo con quello precedente se necessario), invece di attivare gli eventi quando le cose cambiano. Ci siamo affidati a requestAnimationFrame() per configurare il polling nel modo più efficiente e rispettoso della batteria. Per il nostro doodle, anche se abbiamo già un loop requestAnimationFrame() per supportare le animazioni, ne abbiamo creato un secondo completamente separato: era più semplice programmare e non dovrebbe influire in alcun modo sulle prestazioni.

Ecco il codice che ha ricevuto dal 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 un solo gamepad, recuperarne i dati potrebbe essere semplice come:

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

Se vuoi essere più intelligente o supportare più di un player contemporaneamente, dovrai aggiungere qualche altra riga di codice per reagire a scenari più complessi (due o più gamepad collegati, alcuni di loro disconnessi a metà e così via). Per scoprire come risolvere questo problema, puoi consultare il codice sorgente del nostro tester, la funzione pollGamepads().

Eventi

Firefox utilizza un metodo alternativo descritto meglio nelle specifiche dell'API Gamepad. Invece di chiederti di fare un sondaggio, 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 collegato 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 del pulsante o degli assi (attualmente supportato solo in Chrome)

Bottoni e bastoncini

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, alcuni analogici), oltre a due levette analogiche. L'API Gamepad ti indicherà tutti i pulsanti e le levette analogiche segnalate dal sistema operativo.

Una volta ottenuto lo stato corrente nell'oggetto Gamepad, puoi accedere ai pulsanti tramite .buttons[] e stick tramite gli array .axes[]. Ecco un riepilogo visivo di ciò a cui corrispondono:

Diagramma del Gamepad
Diagramma del Gamepad

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

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 extra verranno aggiunti a quelli sopra indicati. Tieni presente, tuttavia, che non sono garantiti né sedici pulsanti né quattro assi: tieni presente che alcuni di essi non saranno semplicemente definiti.

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

Analogico o discreto?

Apparentemente, ogni pulsante può essere analogico, ad esempio per i pulsanti a spalla. Di conseguenza, è meglio impostare una soglia piuttosto che confrontarla in modo brusco con 1,00 (cosa succede se un pulsante analogico è leggermente sporco? È possibile che non raggiunga 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 le levette analogiche in joystick digitali. Certo, c'è sempre il pad digitale (D-pad), ma il tuo gamepad potrebbe non averne uno. Ecco il nostro codice per risolvere 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;
    }
};

Pressione dei pulsanti e movimenti delle leve

Eventi

In alcuni casi, come nel caso di un simulatore di volo, controllare e reagire continuamente alle posizioni di ferma o alla pressione dei pulsanti ha più senso... ma per cose come il doodle di Hurdles 2012? Forse ti chiederai: perché devo cercare i pulsanti ogni singolo frame? Perché non riesco a ricevere eventi come per la tastiera o per il passaggio del mouse in alto o in basso?

La buona notizia è che puoi farlo. Le cattive notizie sono le future. È conforme alle specifiche, ma non è ancora implementato in nessun browser.

Sondaggi

Nel frattempo, puoi provare a confrontare lo stato attuale con quello precedente e chiamare le funzioni se noti delle 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 incentrato sulla tastiera nel doodle degli ostacoli 2012

Dal momento che senza un gamepad, il metodo di immissione preferito del nostro doodle di oggi è la tastiera, abbiamo deciso che il gamepad possa emularlo piuttosto da vicino. Questo ha comportato tre decisioni:

  1. Per il doodle occorrono solo tre pulsanti, due per correre e uno per saltare, ma il gamepad probabilmente ne avrà molti di più. Abbiamo quindi mappato tutti i sedici pulsanti noti e i due stick noti su queste tre funzioni logiche in un modo che pensavamo fosse più logico, in modo che le persone potessero funzionare: alternando pulsanti A/B, alternando pulsanti sulle spalle, premendo sinistra/destra sul D-pad o facendo oscillare violentemente a destra e a sinistra (alcuni di questi saranno, ovviamente, più efficienti degli 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 considerato ogni ingresso analogico come discreto, utilizzando le funzioni di soglia descritte in precedenza.

  3. Ci siamo spinti fino ad aggiungere l'input del gamepad al doodle, invece di integrarlo: il nostro loop di polling in realtà sintetizza gli eventi di keydown e keyup necessari (con un keyCode appropriato) e li rimanda 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);

Questo è tutto.

Suggerimenti utili

  • Ricorda che il gamepad non è visibile nel browser prima della pressione di un pulsante.
  • Se stai testando il gamepad in diversi browser contemporaneamente, tieni presente che solo uno di questi riesce a rilevare il controller. Se non ricevi eventi, assicurati di chiudere le altre pagine che potrebbero utilizzarlo. Inoltre, in base alla nostra esperienza, a volte un browser può "afferrare" 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 di altri browser per assicurarti di ricevere un'assistenza ottimale, dopodiché prendi i provvedimenti necessari se noti un comportamento diverso delle versioni precedenti.

Il futuro

Ci auguriamo che queste informazioni ti siano di aiuto per far luce su questa nuova API, che è comunque un po' precaria, ma già molto divertente.

Oltre ai componenti mancanti dell'API (ad es. gli eventi) e al supporto più ampio del browser, speriamo anche di vedere elementi come il controllo del rombo, l'accesso a giroscopi integrati e così via. Inoltre, maggiore supporto per diversi tipi di gamepad: segnala un bug per Chrome e/o segnala un bug su Firefox se ne trovi uno che funziona in modo errato o non funziona affatto.

Prima però, prova a giocare con il nostro doodle Hurtles 2012 per scoprire quanto è più divertente usare il gamepad. Oh, hai appena detto che potresti fare meglio di 10,7 secondi? Diamoci dentro!

Per approfondire