Supera los obstáculos con la API de Gamepad

Introducción

Deja que los novatos conserven sus teclados para los juegos de aventura, sus preciosas puntas de los dedos multitáctiles para cortar frutas y sus atractivos sensores de movimiento nuevos para simular que saben bailar como Michael Jackson. (Noticias Flash: No pueden). Pero eres diferente. Eres mejor. Eres un profesional. Para ti, los juegos comienzan y terminan con un control de juegos en tus manos.

Pero espera. ¿No tienes suerte si quisieras admitir un control de juegos en tu aplicación web? Ya no. Ayudamos con la nueva API de Gamepad, que permite usar JavaScript para leer el estado de cualquier control de juegos conectado a la computadora. Está tan actualizado que solo llegó a Chrome 21 la semana pasada, y está a punto de ser compatible con Firefox (actualmente disponible en una compilación especial).

Ese resultó ser un buen momento para hacerlo, ya que tuvimos la oportunidad de usarlo recientemente en el doodle de Hurdles 2012 de Google. Este artículo explicará brevemente cómo agregamos la API de Gamepad al doodle y lo que aprendimos durante el proceso.

Doodle de Google Hurdles 2012
Doodle de Hurdles de Google 2012

Verificador de control de juegos

Aunque son efímeros, los doodles interactivos suelen ser bastante complejos en su interior. Para que sea más fácil demostrar de qué estamos hablando, tomamos el código del control de mando del doodle y creamos un verificador de control de mando simple. Puedes usarlo para verificar si el control de juegos USB funciona correctamente y para examinar cómo funciona.

¿Qué navegadores lo admiten actualmente?

Navegadores compatibles

  • 21
  • 12
  • 29
  • 10.1

Origen

¿Qué controles de juegos se pueden usar?

En general, debería funcionar cualquier control de juegos moderno que sea compatible de forma nativa con tu sistema. Probamos varios controles para juegos, desde controles USB de terceros en una PC, hasta controles de juegos PlayStation 2 conectados a través de una llave a una Mac, hasta controles Bluetooth vinculados a una computadora portátil con ChromeOS.

Controles de juegos
Controladores de juegos

Esta es una foto de algunos controles que utilizamos para probar nuestro doodle: "Sí, mamá, eso es lo que hago en el trabajo". Si el control no funciona o si los controles no están asignados correctamente, informa un error en Chrome o Firefox . (Prueba en la versión más reciente de cada navegador para asegurarte de que no se haya corregido).

Detección de funciones en la API de Gamepad<

Es bastante fácil en Chrome:

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

Todavía no parece posible detectar esto en Firefox; 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 eso es temporal. El increíble Modernizr ya te brinda información sobre la API de Gamepad, así que te recomendamos esto para todas tus necesidades de detección actuales y futuras:

var gamepadSupportAvailable = Modernizr.gamepads;

Información sobre los controles de juegos conectados

Incluso si conectas el control de juegos, no se manifestará de ninguna manera, a menos que el usuario primero presione cualquiera de los botones. Esto es para evitar la creación de huellas digitales, aunque resulta ser un poco desafiante para la experiencia del usuario: no puedes pedirle al usuario que presione el botón ni brindarle instrucciones específicas para el control de juegos porque no tienes idea de si conectó el control.

Una vez que elimines ese obstáculo (perdón...), te esperan más.

Sondeo

La implementación de la API que hace Chrome expone una función, navigator.webkitGetGamepads(), que puedes usar para obtener una lista de todos los controles de juegos conectados actualmente al sistema, junto con su estado actual (botones y sticks). El primer control de mando conectado se devolverá como la primera entrada del conjunto y así sucesivamente.

(Esta llamada a función acaba de reemplazar un array al que podías acceder directamente: navigator.webkitGamepads[]. Desde principios de agosto de 2012, es necesario acceder a este array en Chrome 21, mientras que la llamada a función funciona en Chrome 22 y versiones posteriores. De ahora en adelante, la llamada a función es la forma recomendada de usar la API y se filtrará lentamente a todos los navegadores Chrome instalados).

La parte implementada hasta el momento de la especificación requiere que verifiques continuamente el estado de los controles de juegos conectados (y, si es necesario, los compares con los anteriores), en lugar de activar eventos cuando la situación cambie. Nos basamos en requestAnimationFrame() para configurar el sondeo de la manera más eficiente y que respete la batería. Para nuestro doodle, aunque ya teníamos un bucle requestAnimationFrame() para admitir animaciones, creamos otro segundo completamente independiente: 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 control de mando, obtener sus datos podría ser tan simple como se indica a continuación:

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

Si quieres ser un poco más inteligente o apoyar a más de un jugador a la vez, deberás agregar unas líneas de código más para reaccionar a situaciones más complejas (dos o más controles de juegos conectados, algunos de ellos se desconectan a mitad de camino, etcétera). Puedes consultar el código fuente de nuestro verificador, la función pollGamepads(), para ver un enfoque sobre cómo resolver esto.

Eventos

Firefox utiliza una alternativa mejor descrita en la especificación de la API de Gamepad. En lugar de pedirte que realices un sondeo, expone dos eventos, MozGamepadConnected y MozGamepadDisconnected, que se activan cuando un control de juegos se conecta (o, más precisamente, cuando se conecta y "se anuncia" al presionar cualquiera de los botones) o cuando se desenchufa. El objeto de control de juegos que seguirá reflejando su estado futuro se pasa como parámetro .gamepad del objeto de 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, la 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 control de juegos conectado al sistema estará representado por un objeto que se ve así:

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 pocos campos superiores son metadatos simples:

  • id: Es una descripción textual del control de juegos.
  • index: Es un número entero útil para distinguir diferentes controles de juegos conectados a una computadora.
  • timestamp: Es la marca de tiempo de la última actualización del estado del botón o los ejes (por el momento, solo se admite en Chrome).

Botones y palitos

Los controles de juegos actuales 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, otros analógicos), además de dos sticks analógicos. La API de Gamepad te informará sobre todos los botones y sticks analógicos que informa el sistema operativo.

Una vez que obtengas el estado actual en el objeto del control de juegos, podrás acceder a los botones con .buttons[] y a los sticks a través de arrays .axes[]. A continuación, se incluye un resumen visual de lo que corresponden:

Diagrama de control de juegos
Diagrama de control de juegos

La especificación solicita 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. No obstante, ten en cuenta que no se garantizan dieciséis botones ni cuatro ejes; debes estar listo para que algunos de ellos simplemente queden indefinidos.

Los botones pueden tomar valores de 0.0 (sin presionar) a 1.0 (presionado por completo). Los ejes van de -1.0 (completamente a la izquierda o arriba) a 0.0 (centro) a 1.0 (completamente a la derecha o abajo).

¿Analógico o discreto?

Aparentemente, todos los botones podrían ser análogos, lo que es algo común para los botones superiores, por ejemplo. Por lo tanto, es mejor establecer un umbral en lugar de compararlo directamente con 1.00 (¿qué pasa si un botón analógico está un poco sucio? Es posible que nunca llegue a 1.00). En nuestro doodle, 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 sticks analógicos en joysticks digitales. Claro, siempre está el pad digital (pad direccional), pero es posible que el control de juegos no tenga uno. Este es el código para manejarlo:

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 mover el palillo

Eventos

En algunos casos, como en un juego de simulador de vuelo, comprobar y reaccionar continuamente a las posiciones de los palillos o presionar los botones tiene más sentido... pero para cosas como el doodle de Hurdles 2012? Quizás te preguntes: ¿por qué debo buscar los botones en cada fotograma? ¿Por qué no puedo obtener eventos como los hago con la flecha hacia arriba o abajo del teclado o del mouse?

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

Sondeo

Mientras tanto, la salida es comparar el estado actual y el anterior, y llamar a 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);
}

El enfoque centrado en el teclado en el doodle de Hurdles de 2012

Como sin un control de mando, el método de entrada preferido de nuestro doodle actual es el teclado, por lo que decidimos que el control de mando lo emule bastante cerca. Esto significó tres decisiones:

  1. El doodle solo necesita tres botones (dos para correr y uno para saltar), pero es probable que el control de mando tenga muchos más. Por lo tanto, asignamos todos los dieciséis botones y dos sticks conocidos a esas tres funciones lógicas de la manera que nos pareció más lógica, para que las personas pudieran utilizar: alternando botones A/B, botones de los hombros, presionando hacia la izquierda/derecha en el pad direccional o moviendo cualquiera de ellos violentamente hacia la izquierda y hacia la derecha (algunos de ellos, por supuesto, serán más eficientes que las demás). 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. Llegamos al punto de agregar la entrada del control de juegos al doodle, en lugar de integrarla: nuestro bucle de sondeo sintetiza los eventos de 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 control de mando no estará visible en el navegador antes de que se presione un botón.
  • Si pruebas el control de mando en diferentes navegadores a la vez, ten en cuenta que solo uno de ellos puede detectarlo. Si no recibes ningún evento, asegúrate de cerrar las otras páginas que puedan estar usándolo. Además, según nuestra experiencia, a veces un navegador puede "conservar" el control de juegos, incluso si cierras la pestaña o cierras el navegador. A veces, reiniciar el sistema es la única forma de solucionar problemas.
  • Como siempre, utiliza Chrome Canary y sus equivalentes para otros navegadores a fin de asegurarte de contar con la mejor compatibilidad. Luego, actúa de forma adecuada si ves que las versiones anteriores se comportan de forma diferente.

El futuro

Esperamos que esto aclare tus dudas sobre esta nueva API, que sigue siendo un poco precaria, pero muy divertida.

Además de las partes faltantes de la API (p.ej., eventos) y una mayor compatibilidad con los navegadores, también esperamos ver aspectos como el control de rumble, el acceso a giroscopios integrados, etc. Además, hay más compatibilidad con diferentes tipos de controles de juegos: informa un error en Chrome o informa un error en Firefox si encuentras uno que funciona de manera incorrecta o no funciona.

Pero antes de eso, ve a jugar con nuestro doodle de Hurdles 2012 y descubre cuánto más divertido es en el control de juegos. ¿Acabas de decir que podrías hacerlo mejor que 10.7 segundos? Llévalo.

Lecturas adicionales