Superare gli ostacoli con l'API Gamepad

Marcin Wichary
Marcin Wichary

Introduzione

Lasciamo che i principianti usino le tastiere per i giochi di avventura, i loro preziosi polpastrelli per tagliare la frutta e i loro nuovi sensori di movimento per fingere di saper ballare come Michael Jackson. (Notizia flash: non possono farlo). Ma tu sei diversa. Sei migliore. Sei un professionista. Per te, i giochi iniziano e finiscono con un gamepad tra le mani.

Ma aspetta. Non sei sfortunato se vuoi supportare un gamepad nella tua app web? Non più. La nuovissima API Gamepad viene in aiuto, consentendoti di utilizzare JavaScript per leggere lo stato di qualsiasi controller gamepad collegato al computer. È così recente che è stato introdotto in Chrome 21 solo la scorsa settimana ed è anche sul punto di essere supportato in Firefox (attualmente disponibile in una build speciale).

È stato un tempismo perfetto, perché abbiamo avuto l'opportunità di utilizzarlo di recente nel doodle di Google Corsa a ostacoli 2012. Questo articolo spiega brevemente come abbiamo aggiunto l'API Gamepad al doodle e cosa abbiamo imparato durante il processo.

Doodle di Google Corsa a ostacoli 2012
Doodle di Google Corsa a ostacoli 2012

Tester del gamepad

Per quanto siano effimeri, i doodle interattivi tendono a essere piuttosto complessi dietro le quinte. Per semplificare la dimostrazione di ciò di cui stiamo parlando, abbiamo preso il codice del gamepad dal doodle e abbiamo creato un semplice tester per gamepad. Puoi utilizzarlo per verificare se il tuo gamepad USB funziona correttamente e anche per esaminare come è stato realizzato.

Quali browser lo supportano oggi?

Browser Support

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

Source

Quali gamepad possono essere utilizzati?

In genere, qualsiasi gamepad moderno supportato in modo nativo dal tuo sistema dovrebbe funzionare. Abbiamo testato vari gamepad, dai controller USB di marche sconosciute su un PC ai gamepad PlayStation 2 collegati tramite un dongle a un Mac, fino ai controller Bluetooth accoppiati con un notebook ChromeOS.

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 nella versione più recente di ogni browser per assicurarti che il problema non sia già stato risolto).

Rilevamento delle funzionalità dell'API Gamepad<

In Chrome:

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

Non sembra ancora possibile rilevare questo aspetto in Firefox: tutto è basato sugli 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 certi che si tratti di una situazione temporanea. Il fantastico Modernizr ti fornisce già informazioni sull'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 verrà visualizzato in alcun modo, a meno che l'utente non prema prima uno dei suoi pulsanti. Questo serve a impedire il fingerprinting, anche se si rivela un po' una sfida 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 superato questo ostacolo (scusa…), però, ne troverai altri.

Sondaggi

L'implementazione dell'API da parte 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 (pulsanti + levette). Il primo gamepad connesso verrà restituito come prima voce dell'array e così via.

(Questa chiamata di funzione ha appena sostituito un array a cui potevi accedere direttamente: navigator.webkitGamepads[]. A partire 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à gradualmente estesa a tutti i browser Chrome installati.)

La parte della specifica implementata finora richiede di controllare continuamente lo stato dei gamepad collegati (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 con il minor consumo di batteria possibile. Per il nostro doodle, anche se abbiamo già un loop requestAnimationFrame() per supportare le animazioni, ne abbiamo creato un secondo completamente separato: è stato più semplice da programmare 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 un solo gamepad, ottenere i suoi dati potrebbe essere semplice come:

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

Se vuoi essere un po' più intelligente o supportare più di un giocatore contemporaneamente, dovrai aggiungere qualche riga di codice in più per reagire a scenari più complessi (due o più gamepad collegati, 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. Invece di 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 collegato al sistema sarà rappresentato da un oggetto simile a questo:

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/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 levette analogiche. L'API Gamepad ti fornirà informazioni su tutti i pulsanti e le levette analogiche segnalati dal sistema operativo.

Una volta ottenuto lo stato attuale nell'oggetto gamepad, puoi accedere ai pulsanti tramite gli array .buttons[] e alle levette 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 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 sopra. Tieni presente, tuttavia, che non è garantito che siano presenti 16 pulsanti o quattro assi, quindi preparati al fatto che alcuni di questi potrebbero semplicemente non essere definiti.

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

Analogico o discreto?

In teoria, ogni pulsante potrebbe essere analogico, come accade spesso, ad esempio, per i pulsanti laterali. Pertanto, è meglio impostare una soglia anziché limitarsi a confrontarla con 1,00 (cosa succede se un pulsante analogico è leggermente sporco? Potrebbe non raggiungere mai 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 D-pad, ma il tuo gamepad potrebbe non averne uno. Ecco il nostro codice per gestire questa situazione:

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

Pressioni dei pulsanti e movimenti della levetta

Eventi

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

La buona notizia è che puoi farlo. La cattiva notizia è che, in futuro, È presente nella specifica, ma non è ancora implementata in nessun browser.

Sondaggi

Nel frattempo, la soluzione è 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 Corsa a ostacoli 2012

Poiché senza un gamepad, il metodo di input preferito del doodle di oggi è la tastiera, abbiamo deciso che il gamepad lo emulasse in modo molto simile. Ciò ha comportato tre decisioni:

  1. Il doodle richiede solo 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 le due levette note su queste tre funzioni logiche in un modo che ritenevamo più sensato, in modo che le persone potessero correre: alternando i pulsanti A/B, alternando i pulsanti dorsali, premendo sinistra/destra sul D-pad o muovendo violentemente una delle levette a sinistra e a destra (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 trattato ogni input analogico come un input discreto, utilizzando le funzioni di soglia descritte in precedenza.

  3. Siamo arrivati al punto di collegare l'input del gamepad al doodle, anziché integrarlo: il nostro ciclo di polling sintetizza gli eventi keydown e keyup necessari (con un keyCode appropriato) e li invia nuovamente 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 sarà visibile nel browser finché non viene premuto un pulsante.
  • Se stai testando il gamepad in diversi browser 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, in base alla nostra esperienza, a volte un browser può "mantenere" il gamepad anche se chiudi la scheda o esci dal browser stesso. A volte il riavvio del sistema è l'unico modo per risolvere i problemi.
  • Come sempre, utilizza Chrome Canary e gli equivalenti per altri browser per assicurarti di usufruire del miglior supporto, quindi agisci di conseguenza se noti che le versioni precedenti si comportano in modo diverso.

Futuro =

Ci auguriamo che questo articolo contribuisca a fare un po' di chiarezza su questa nuova API, ancora un po' precaria, ma già molto divertente.

Oltre alle parti mancanti dell'API (ad es. gli eventi) e a un supporto più ampio dei browser, speriamo anche di vedere in futuro funzionalità come il controllo della vibrazione, l'accesso ai giroscopi integrati e così via, oltre a un maggiore supporto per diversi tipi di gamepad. Se ne trovi uno che non funziona correttamente o non funziona affatto, segnala un bug per Chrome e/o segnala un bug per Firefox.

Ma prima, gioca con il nostro Doodle Corsa a ostacoli 2012 e scopri quanto è più divertente con il gamepad. Oh, hai appena detto che potresti fare meglio di 10,7 secondi? Diamoci dentro!

Per approfondire