Disfruta del juego del dinosaurio de Chrome con tu control de mando

Aprende a usar la API de Gamepad para llevar tus juegos web al siguiente nivel.

El huevo de pascua de la página sin conexión de Chrome es uno de los secretos peor guardados de la historia ([citation needed], pero la afirmación se hizo por el efecto dramático). Si presionas la tecla espacio o, en dispositivos móviles, presionas el dinosaurio, la página sin conexión se convierte en un juego de arcade. Es posible que sepas que, en realidad, no tienes que jugar sin conexión cuando te apetezca: en Chrome, puedes navegar a about://dino o, si eres un geek, ir a about://network-error/-106. Sin embargo, ¿sabías que se juegan 270 millones de juegos del dinosaurio de Chrome todos los meses?

Página sin conexión de Chrome con el juego del dinosaurio de Chrome.
Presiona la barra espaciadora para jugar.

Otro dato que podría ser más útil y que quizás no conozcas es que, en el modo arcade, puedes jugar con un gamepad. La compatibilidad con gamepads se agregó hace aproximadamente un año, en el momento de la escritura de este artículo, en un commit de Reilly Grant. Como puedes ver, el juego, al igual que el resto del proyecto de Chromium, es completamente de código abierto. En esta publicación, quiero mostrarte cómo usar la API de Gamepad.

Cómo usar la API de Gamepad

Detección de funciones y compatibilidad con navegadores

La API de Gamepad tiene una compatibilidad universal con navegadores en computadoras de escritorio y dispositivos móviles. Puedes detectar si la API de Gamepad es compatible con el siguiente fragmento:

if ('getGamepads' in navigator) {
  // The API is supported!
}

Cómo representa el navegador un control de juegos

El navegador representa los mandos de juegos como objetos Gamepad. Un Gamepad tiene las siguientes propiedades:

  • id: Es una cadena de identificación para el mando de juegos. Esta cadena identifica la marca o el estilo del dispositivo de gamepad conectado.
  • displayId: Es el VRDisplay.displayId de un VRDisplay asociado (si corresponde).
  • index: Es el índice del gamepad en el navegador.
  • connected: Indica si el gamepad sigue conectado al sistema.
  • hand: Es una enumeración que define en qué mano se sostiene el controlador o en qué mano es más probable que se sostenga.
  • timestamp: Es la última vez que se actualizaron los datos de este gamepad.
  • mapping: Es la asignación de botones y ejes en uso para este dispositivo, ya sea "standard" o "xr-standard".
  • pose: Un objeto GamepadPose que representa la información de pose asociada con un controlador de WebVR.
  • axes: Un array de valores para todos los ejes del mando de juegos, normalizado de forma lineal al rango de -1.0 a 1.0.
  • buttons: Es un array de estados de botones para todos los botones del mando de juegos.

Ten en cuenta que los botones pueden ser digitales (presionados o no) o analógicos (por ejemplo, presionados en un 78%). Por este motivo, los botones se informan como objetos GamepadButton, con los siguientes atributos:

  • pressed: Es el estado presionado del botón (true si está presionado y false si no lo está).
  • touched: Es el estado tocado del botón. Si el botón puede detectar toques, esta propiedad es true si se está tocando el botón y false de lo contrario.
  • value: En el caso de los botones que tienen un sensor analógico, esta propiedad representa la cantidad en la que se presionó el botón, normalizada de forma lineal al rango de 0.0 a 1.0.
  • hapticActuators: Es un array que contiene objetos GamepadHapticActuator, cada uno de los cuales representa el hardware de respuesta táctil disponible en el controlador.

Según el navegador y el gamepad que tengas, es posible que encuentres una propiedad vibrationActuator. Permite dos tipos de efectos de vibración:

  • Dual-Rumble: El efecto de respuesta táctil que generan dos actuadores de masa rotativa excéntricos, uno en cada empuñadura del mando de juego.
  • Trigger-Rumble: El efecto de respuesta táctil que generan dos motores independientes, con un motor ubicado en cada uno de los gatillos del mando de juegos.

En la siguiente descripción general esquemática, que se toma directamente de las especificaciones, se muestra la asignación y la disposición de los botones y los ejes en un gamepad genérico.

Descripción general esquemática de las asignaciones de botones y ejes de un mando de juegos común.
Representación visual de un diseño de gamepad estándar (Fuente).

Notificación cuando se conecta un control de juegos

Para saber cuándo se conecta un gamepad, escucha el evento gamepadconnected que se activa en el objeto window. Cuando el usuario conecta un gamepad, lo que puede ocurrir a través de USB o Bluetooth, se activa un GamepadEvent que tiene los detalles del gamepad en una propiedad gamepad con un nombre adecuado. A continuación, puedes ver un ejemplo de un control de Xbox 360 que tenía por ahí (sí, me gustan los juegos retro).

window.addEventListener('gamepadconnected', (event) => {
  console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: true
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
  */
});

Notificación cuando se desconecta un gamepad

Recibir notificaciones sobre desconexiones del mando de juegos ocurre de manera análoga a la forma en que se detectan las conexiones. Esta vez, la app escucha el evento gamepaddisconnected. Observa cómo, en el siguiente ejemplo, connected ahora es false cuando desconecto el controlador de Xbox 360.

window.addEventListener('gamepaddisconnected', (event) => {
  console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: false
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: null
  */
});

El control de juegos en tu bucle de juego

Para obtener un gamepad, comienza con una llamada a navigator.getGamepads(), que muestra un array con elementos Gamepad. El array en Chrome siempre tiene una longitud fija de cuatro elementos. Si no hay ninguno o menos de cuatro gamepads conectados, un elemento puede ser solo null. Asegúrate de revisar todos los elementos del array y ten en cuenta que los gamepads “recuerdan” su ranura y es posible que no siempre estén presentes en la primera ranura disponible.

// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]

Si hay uno o varios gamepads conectados, pero navigator.getGamepads() aún informa elementos null, es posible que debas presionar cualquiera de sus botones para activar cada gamepad. Luego, puedes sondear los estados del gamepad en tu bucle de juego, como se muestra en el siguiente código.

const pollGamepads = () => {
  // Always call `navigator.getGamepads()` inside of
  // the game loop, not outside.
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    // Disregard empty slots.
    if (!gamepad) {
      continue;
    }
    // Process the gamepad state.
    console.log(gamepad);
  }
  // Call yourself upon the next animation frame.
  // (Typically this happens every 60 times per second.)
  window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();

El actuador de vibración

La propiedad vibrationActuator muestra un objeto GamepadHapticActuator, que corresponde a una configuración de motores o de otros actuadores que pueden aplicar una fuerza para los fines de la respuesta táctil. Para reproducir efectos táctiles, llama a Gamepad.vibrationActuator.playEffect(). Los únicos tipos de efectos válidos son 'dual-rumble' y 'trigger-rumble'.

Efectos de vibración compatibles

if (gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
  // Trigger rumble supported.
} else if (gamepad.vibrationActuator.effects.includes('dual-rumble')) {
  // Dual rumble supported.
} else {
  // Rumble effects aren't supported.
}

Vibración dual

La vibración dual describe una configuración táctil con un motor de vibración de masa rotativa excéntrica en cada mango de un mando de juegos estándar. En esta configuración, cualquier motor puede hacer vibrar todo el mando de juegos. Las dos masas no son iguales, de modo que los efectos de cada una se pueden combinar para crear efectos táctiles más complejos. Los efectos de vibración doble se definen con cuatro parámetros:

  • duration: Establece la duración del efecto de vibración en milisegundos.
  • startDelay: Establece la duración de la demora hasta que se inicia la vibración.
  • strongMagnitude y weakMagnitude: Establece los niveles de intensidad de vibración para los motores de masa rotativa excéntrica más pesados y más ligeros, normalizados al rango 0.01.0.
// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  gamepad.vibrationActuator.playEffect('dual-rumble', {
    // Start delay in ms.
    startDelay: delay,
    // Duration in ms.
    duration: duration,
    // The magnitude of the weak actuator (between 0 and 1).
    weakMagnitude: weak,
    // The magnitude of the strong actuator (between 0 and 1).
    strongMagnitude: strong,
  });
};

Activar vibración

La vibración del gatillo es el efecto de respuesta táctil que generan dos motores independientes, con un motor ubicado en cada uno de los gatillos del mando de juegos.

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  // Feature detection.
  if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
    return;
  }
  gamepad.vibrationActuator.playEffect('trigger-rumble', {
    // Duration in ms.
    duration: duration,
    // The left trigger (between 0 and 1).
    leftTrigger: leftTrigger,
    // The right trigger (between 0 and 1).
    rightTrigger: rightTrigger,
  });
};

Integración con la Política de Permisos

La especificación de la API de Gamepad define una función controlada por políticas que se identifica con la cadena "gamepad". Su allowlist predeterminado es "self". La política de permisos de un documento determina si el contenido de ese documento puede acceder a navigator.getGamepads(). Si se inhabilita en cualquier documento, no se permitirá que ningún contenido del documento use navigator.getGamepads() ni se activarán los eventos gamepadconnected y gamepaddisconnected.

<iframe src="index.html" allow="gamepad"></iframe>

Demostración

En el siguiente ejemplo, se incorpora una demo de probador de gamepad. El código fuente está disponible en Glitch. Para probar la demostración, conecta un gamepad mediante USB o Bluetooth y presiona cualquiera de sus botones o mueve cualquiera de sus ejes.

Bonificación: juega al dinosaurio de Chrome en web.dev

Puedes jugar al Dinosaurio de Chrome con tu mando en este mismo sitio. El código fuente está disponible en GitHub. Consulta la implementación de sondeo del gamepad en trex-runner.js y observa cómo emula las presiones de teclas.

Para que funcione la demostración del Gamepad de Chrome Dino, quité el juego del dinosaurio de Chrome del proyecto principal de Chromium (actualizando un esfuerzo anterior de Arnelle Ballane), lo coloqué en un sitio independiente, extendí la implementación existente de la API de gamepad agregando efectos de atenuación y vibración, creé un modo de pantalla completa y Mehul Satardekar contribuyó con una implementación de modo oscuro. ¡Que disfrutes el juego!

Agradecimientos

François Beaufort y Joe Medley revisaron este documento. Steve Agoston, James Hollyer y Matt Reynolds editan la especificación de la API de Gamepad. Los editores de especificaciones anteriores son Brandon Jones, Scott Graham y Ted Mielczarek. Brandon Jones es el editor de las especificaciones de Gamepad Extensions. Imagen hero de Laura Torrent Puig.