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 peores guardados en la historia ([citation needed], pero se reclamó el efecto dramático). Si presionas la tecla de barra espaciadora o, en dispositivos móviles, presionas el dinosaurio, la página sin conexión se convertirá en un juego de arcade reproducible. Quizás sepas que, en realidad, no tienes que desconectarte cuando tienes ganas de jugar: en Chrome, puedes navegar a about://dino o, si eres geek, navegar a about://network-error/-106. Pero ¿sabías que se juegan 270 millones de juegos del dinosaurio de Chrome todos los meses?

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

Otro hecho que es más útil saber y que quizás no sepas es que en el modo arcade puedes jugar con un control de juegos. La compatibilidad con Gamepad se agregó hace aproximadamente un año al momento de escribir este documento en una confirmación 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 ofrece una excelente compatibilidad con navegadores tanto para computadoras de escritorio como para dispositivos móviles. Puedes usar el siguiente fragmento para detectar si la API de Gamepad es compatible:

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

Cómo el navegador representa un control de juegos

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

  • id: Es una cadena de identificación para el control de juegos. Esta cadena identifica la marca o el estilo del dispositivo de control de juegos conectado.
  • displayId: Es el VRDisplay.displayId de un VRDisplay asociado (si es relevante).
  • index: Es el índice del control de juegos en el navegador.
  • connected: Indica si el control de juegos todavía está conectado al sistema.
  • hand: Es una enumeración que define en qué mano se sostiene el control o en qué mano es más probable que se retenga.
  • timestamp: Es la última vez que se actualizaron los datos de este control de juegos.
  • mapping: Es la asignación de botones y ejes que se usa para este dispositivo, ya sea "standard" o "xr-standard".
  • pose: Es un objeto GamepadPose que representa la información de poses asociada con un controlador de WebVR.
  • axes: Es un array de valores para todos los ejes del control 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 control de juegos.

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

  • pressed: Es el estado presionado del botón (true si se presiona el botón y false si no se presiona.
  • touched: Es el estado en el que se tocó el botón. Si el botón es capaz de detectar el tacto, esta propiedad es true si lo tocas y false en el caso contrario.
  • value: En el caso de los botones que tienen un sensor analógico, esta propiedad representa la cantidad de pulsación del botón, normalizada linealmente 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 control.

Algo adicional que puedes encontrar, según el navegador y el control de juegos que tengas, es una propiedad vibrationActuator. Permite dos tipos de efectos de rumble:

  • Rumbo doble: El efecto de la respuesta táctil que generan dos accionadores de masa excéntricos y rotativos, uno en cada control del control de juegos.
  • Trigger-Rumble: El efecto de la respuesta táctil que generan dos motores independientes, con uno ubicado en cada uno de los gatillos del control de juegos.

En la siguiente descripción general del esquema, tomada directamente de la especificación, se muestra la asignación y la disposición de los botones y ejes en un control de juegos genérico.

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

Notificación que aparece cuando se conecta un control de juegos

Para saber si un control de juegos está conectado, busca el evento gamepadconnected que se activa en el objeto window. Cuando el usuario conecta un control de juegos, lo que puede suceder a través de USB o Bluetooth, se activa un GamepadEvent que tiene los detalles del control de juegos en una propiedad gamepad con el mismo nombre. A continuación, puedes ver un ejemplo de un control de Xbox 360 que tenía tirado (sí, me encantan 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 que aparece cuando se desconecta un control de juegos

Las notificaciones de desconexiones del control de juegos suceden de forma análoga a la forma en que se detectan las conexiones. Esta vez, la app detecta el evento gamepaddisconnected. Observa que, en el siguiente ejemplo, connected ahora es false cuando desenchufo el control 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 mando en el bucle de juego

La conexión de un control de juegos 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 hay cero o menos de cuatro controles de juegos conectados, un elemento puede ser solo null. Asegúrate siempre de verificar todos los elementos del array y de que los controles de juegos "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 uno o varios controles de juegos están conectados, pero navigator.getGamepads() aún informa elementos null, es posible que debas "activar" cada control de juegos presionando cualquiera de los botones. Luego, puedes sondear los estados del control de juegos en el 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();

Accionador de vibración

La propiedad vibrationActuator muestra un objeto GamepadHapticActuator, que corresponde a una configuración de motores y otros accionadores que pueden aplicar una fuerza para fines de respuesta táctil. Para reproducir los efectos táctiles, llama a Gamepad.vibrationActuator.playEffect(). El único tipo de efecto válido es 'dual-rumble'. La función Dual-rumble describe una configuración táctil con un excéntrico motor de vibración de masa giratoria en cada control de un control de juegos estándar. En esta configuración, cualquiera de los motores puede hacer vibrar todo el control de juegos. Las dos masas no son iguales, por lo que los efectos de cada una se pueden combinar y crear efectos táctiles más complejos. Los efectos de sonido 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: Establecen los niveles de intensidad de vibración de los motores de masa rotativa excéntricos, más pesados y más livianos, normalizados al rango de 0.0 a 1.0.

Efectos de rumble 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.
}

Rumbo doble

// 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 ruido

// 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

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

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

Demostración

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

Contenido adicional: Juega al dinosaurio de Chrome en web.dev

En este sitio, puedes jugar al dinosaurio de Chrome con tu control de juegos. El código fuente está disponible en GitHub. Consulta la implementación del sondeo de control de juegos en trex-runner.js y observa cómo emula las pulsaciones de teclas.

Para que funcione la demostración del control de juegos del dinosaurio de Chrome, saqué el juego del dinosaurio de Chrome del proyecto principal de Chromium (que actualizó un esfuerzo anterior de Arnelle Ballane), lo coloqué en un sitio independiente, amplió la implementación existente de la API de control de juegos mediante efectos de atenuación y vibración, creé un modo de pantalla completa y Mehul Satardekar contribuyó con una implementación del modo oscuro. ¡Que disfrutes de los videojuegos!

Agradecimientos

François Beaufort y Joe Medley revisaron este documento. Steve Agoston, James Hollyer y Matt Reynolds editaron las especificaciones de la API de Gamepad. Los editores de especificaciones anteriores son Brandon Jones, Scott Graham y Ted Mielczarek. Brandon Jones editó la especificación de extensiones para Gamepad. Hero image de Laura Torrent Puig.