Supera los obstáculos con la API de Gamepad

Introducción

Deja que los novatos conserven sus teclados para juegos de aventuras, sus preciados dedos multitáctiles para cortar frutas y sus sofisticados sensores de movimiento novedosos para fingir que pueden bailar como Michael Jackson. (No pueden). Pero tú eres diferente. Te sientes mejor. Eres un profesional. Para ti, los juegos comienzan y terminan con un gamepad en tus manos.

Pero espera. ¿No tienes suerte si quieres admitir un gamepad en tu app web? Ya no. La nueva API de Gamepad te permite usar JavaScript para leer el estado de cualquier controlador de gamepad conectado a tu computadora. Es tan reciente que solo llegó a Chrome 21 la semana pasada y también está a punto de ser compatible con Firefox (actualmente disponible en una compilación especial).

Resultó ser un buen momento, porque tuvimos la oportunidad de usarlo recientemente en el doodle de Google de 2012 sobre obstáculos. En este artículo, explicaremos brevemente cómo agregamos la API de Gamepad al garabato y lo que aprendimos durante el proceso.

Doodle de Google de 2012 sobre obstáculos
Doodle de Google de 2012 sobre obstáculos

Verificador de controles de juegos

A pesar de ser efímeros, los garabatos interactivos suelen ser bastante complejos en su funcionamiento. Para que sea más fácil demostrar de qué estamos hablando, tomamos el código del mando de juegos del garabato y armamos un probador de mandos de juegos simple. Puedes usarlo para ver si el mando de juegos USB funciona correctamente y también para ver cómo funciona.

¿Qué navegadores son compatibles actualmente?

Navegadores compatibles

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

Origen

¿Qué gamepads se pueden usar?

Por lo general, cualquier control de juegos moderno que sea compatible con tu sistema de forma nativa debería funcionar. Probamos varios gamepads, desde controles USB de otras marcas en una PC, hasta gamepads de PlayStation 2 conectados a una Mac con un dongle, y controles Bluetooth vinculados a una notebook con ChromeOS.

Controles de juegos
Controles de juegos

Esta es una foto de algunos controladores que usamos para probar nuestro garabato: "Sí, mamá, eso es lo que hago en el trabajo". Si el controlador no funciona o los controles están asignados de forma incorrecta, informa un error en Chrome o Firefox . (Realiza la prueba en la versión más reciente de cada navegador para asegurarte de que ya no haya un problema).

Detección de funciones de la API de Gamepad<

Es muy fácil en Chrome:

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

Por el momento, no parece posible detectar esto en Firefox, ya que todo se basa en eventos y todos los controladores de eventos deben adjuntarse a la ventana, lo que impide que funcione una técnica típica de detección de controladores de eventos.

Pero estamos seguros de que es temporal. El increíble Modernizr ya te informa sobre la API de Gamepad, por lo que te recomendamos que lo uses para todas tus necesidades de detección actuales y futuras:

var gamepadSupportAvailable = Modernizr.gamepads;

Más información sobre los controles de juegos conectados

Incluso si conectas el gamepad, no se manifestará de ninguna manera, a menos que el usuario presione primero alguno de sus botones. Esto se hace para evitar la creación de huellas digitales, aunque resulta un poco desafiante para la experiencia del usuario: no puedes pedirle al usuario que presione el botón ni proporcionar instrucciones específicas para el mando de juegos porque no tienes idea de si conectó el controlador.

Sin embargo, una vez que superes esa dificultad (lo siento…), te esperan más desafíos.

Sondeo

La implementación de la API de Chrome expone una función (navigator.webkitGetGamepads()) que puedes usar para obtener una lista de todos los gamepads conectados actualmente al sistema, junto con su estado actual (botones y joysticks). El primer gamepad conectado se mostrará como la primera entrada del array, y así sucesivamente.

(Esta llamada a función reemplazó recientemente un array al que podías acceder directamente: navigator.webkitGamepads[]. A partir de principios de agosto de 2012, aún es necesario acceder a este array en Chrome 21, mientras que la llamada a función funciona en Chrome 22 y versiones posteriores. En el futuro, la llamada a función es la forma recomendada de usar la API y se irá implementando lentamente en todos los navegadores Chrome instalados.

La parte de la especificación implementada hasta el momento requiere que verifiques de forma continua el estado de los mandos de juegos conectados (y lo compares con el anterior si es necesario), en lugar de activar eventos cuando cambien los elementos. Usamos requestAnimationFrame() para configurar el sondeo de la manera más eficiente y amigable con la batería. Para nuestro garabato, aunque ya tenemos un bucle requestAnimationFrame() para admitir animaciones, creamos un segundo bucle completamente independiente, ya que era más fácil de codificar y no debería afectar el rendimiento de ninguna manera.

Este es el código del verificador:

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

Si solo te interesa un gamepad, obtener sus datos puede ser tan simple como lo siguiente:

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

Si quieres ser un poco más inteligente o admitir más de un jugador de forma simultánea, deberás agregar algunas líneas de código más para reaccionar a situaciones más complejas (dos o más gamepads conectados, algunos de los cuales se desconectan a mitad de la partida, etcétera). Puedes consultar el código fuente de nuestro verificador, la función pollGamepads(), para ver un enfoque sobre cómo resolver este problema.

Eventos

Firefox usa una alternativa mejor que se describe en las especificaciones de la API de Gamepad. En lugar de pedirte que realices una sondeo, expone dos eventos: MozGamepadConnected y MozGamepadDisconnected, que se activan cada vez que se conecta un control de juegos (o, más precisamente, se conecta y se "anuncia" presionando cualquiera de sus botones) o se desconecta. El objeto del control de juegos que seguirá reflejando su estado futuro se pasa como el parámetro .gamepad del objeto del evento.

Desde el código fuente del verificador:

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

Resumen

Al final, nuestra función de inicialización en el verificador, que admite ambos enfoques, se ve de la siguiente manera:

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

Información del control de juegos

Cada gamepad conectado al sistema estará representado por un objeto que se verá de la siguiente manera:

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

Información básica

Los primeros campos son metadatos simples:

  • id: Una descripción textual del mando de juegos
  • index: Es un número entero útil para diferenciar los diferentes gamepads conectados a una computadora.
  • timestamp: La marca de tiempo de la última actualización del estado del botón o los ejes (actualmente, solo se admite en Chrome).

Botones y palancas

Los gamepads de hoy en día no son exactamente lo que tu abuelo podría haber usado para salvar a la princesa en el castillo equivocado. Por lo general, tienen al menos dieciséis botones separados (algunos discretos y otros analógicos), además de dos joysticks analógicos. La API de Gamepad te informará sobre todos los botones y joysticks analógicos que informa el sistema operativo.

Una vez que obtengas el estado actual en el objeto del gamepad, podrás acceder a los botones a través de .buttons[] y a los joysticks a través de arrays .axes[]. Este es un resumen visual de a qué corresponden:

Diagrama del control de juegos
Diagrama del gamepad

La especificación le pide al navegador que asigne los primeros dieciséis botones y cuatro ejes a lo siguiente:

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

Los botones y ejes adicionales se agregarán a los anteriores. Sin embargo, ten en cuenta que no se garantizan los dieciséis botones ni los cuatro ejes. Prepárate para que algunos de ellos simplemente no estén definidos.

Los botones pueden tener valores de 0.0 (no presionado) a 1.0 (completamente presionado). Los ejes van de -1.0 (completamente a la izquierda o hacia arriba) a través de 0.0 (centro) a 1.0 (completamente a la derecha o hacia abajo).

¿Es analógico o discreto?

En teoría, cada botón podría ser analógico, lo que es algo común en los botones de hombro, por ejemplo. Por lo tanto, es mejor establecer un umbral en lugar de compararlo directamente con 1.00 (¿qué sucede si un botón analógico está ligeramente sucio? Es posible que nunca llegue a 1.00). En nuestro garabato, lo hacemos de esta manera:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

Puedes hacer lo mismo para convertir los joysticks analógicos en joysticks digitales. Por supuesto, siempre está el pad digital (pad direccional), pero es posible que tu mando de juego no tenga uno. Este es nuestro código para controlar eso:

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

Presionar botones y movimientos de la palanca

Eventos

En algunos casos, como en un juego de simulador de vuelo, tiene más sentido verificar y reaccionar de forma continua a las posiciones de los joysticks o a la presión de los botones, pero ¿para elementos como el garabato de obstáculos de 2012? Te preguntarás por qué debo verificar si hay botones en cada fotograma. ¿Por qué no puedo obtener eventos como lo hago para el teclado o el mouse hacia arriba o hacia abajo?

La buena noticia es que puedes hacerlo. La mala noticia es que será en el futuro. Está en la especificación, pero aún no se implementó en ningún navegador.

Sondeo

Mientras tanto, la solución es comparar el estado actual y el anterior, y llamar a las funciones si ves alguna diferencia. Por ejemplo:

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

Enfoque en el teclado en el doodle de obstáculos de 2012

Dado que, sin un control de juegos, el método de entrada preferido de nuestro garabato de hoy es el teclado, decidimos que el control de juegos lo emule de forma bastante cercana. Esto implicó tres decisiones:

  1. El garabato solo necesita tres botones: dos para correr y uno para saltar, pero es probable que el mando tenga muchos más. Por lo tanto, asignamos los dieciséis botones y los dos joysticks conocidos a esas tres funciones lógicas de la manera que consideramos más lógica para que las personas pudieran correr de las siguientes maneras: alternando los botones A/B, alternando los botones de los hombros, presionando izquierda/derecha en el mando de dirección o balanceando cualquiera de los joysticks violentamente hacia la izquierda y la derecha (por supuesto, algunos de ellos serán más eficientes que los otros). Por ejemplo:

    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. Tratamos cada entrada analógica como una discreta, con las funciones de umbral descritas anteriormente.

  3. Fuimos tan lejos como atornillar la entrada del gamepad al garabato, en lugar de incorporarla. Nuestro bucle de sondeo en realidad sintetiza los eventos keydown y keyup necesarios (con un keyCode adecuado) y los envía de vuelta 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);

Eso es todo.

Sugerencias y trucos

  • Recuerda que el gamepad no será visible en el navegador hasta que se presione un botón.
  • Si pruebas el gamepad en diferentes navegadores de forma simultánea, ten en cuenta que solo uno de ellos puede detectar el controlador. Si no recibes ningún evento, asegúrate de cerrar las otras páginas que podrían estar usándolo. Además, según nuestra experiencia, a veces un navegador puede “retener” el mando de juegos incluso si cierras la pestaña o sales del navegador. A veces, reiniciar el sistema es la única forma de solucionar el problema.
  • Como siempre, usa Chrome Canary y los equivalentes para otros navegadores para asegurarte de obtener la mejor compatibilidad y, luego, actúa de manera apropiada si ves que las versiones anteriores se comportan de forma diferente.

El futuro

Esperamos que esta información te ayude a comprender mejor esta nueva API, que aún es un poco precaria, pero ya es muy divertida.

Además de las partes faltantes de la API (p. ej., eventos) y la compatibilidad más amplia con navegadores, también esperamos ver, con el tiempo, funciones como el control de vibración, el acceso a giroscopios integrados, etc., y más compatibilidad con diferentes tipos de gamepads. Informa un error en Chrome o en Firefox si encuentras uno que funcione de forma incorrecta o no funcione en absoluto.

Pero antes, juega con nuestro doodle de obstáculos de 2012 y descubre lo mucho más divertido que es en el mando de juegos. ¿Acabas de decir que puedes hacerlo en menos de 10.7 segundos? Adelante.

Lecturas adicionales