Curta o jogo do dinossauro do Chrome com seu gamepad

Aprenda a usar a API Gamepad para levar seus jogos da Web a um nível mais alto.

O easter egg da página off-line do Chrome é um dos segredos mais mal guardados da história ([citation needed], mas a alegação foi feita para o efeito dramático). Se você pressionar a tecla espaço ou, em dispositivos móveis, tocar no dinossauro, a página off-line vai se tornar um jogo de fliperama. Você pode não precisar ficar off-line quando quiser jogar: no Chrome, basta navegar para about://dino ou, para os mais geeks, about://network-error/-106. Mas você sabia que são 270 milhões de jogos do dinossauro do Chrome jogados todo mês?

Página off-line do Chrome com o jogo do dinossauro.
Aperte a barra de espaço para jogar.

Outro fato que pode ser mais útil e que você talvez não saiba é que no modo arcade, é possível jogar com um gamepad. O suporte a gamepad foi adicionado há cerca de um ano, no momento em que este artigo foi escrito, em um commit de Reilly Grant. Como você pode ver, o jogo, assim como o restante do projeto Chromium, é totalmente de código aberto. Neste post, vou mostrar como usar a API Gamepad.

Usar a API Gamepad

Detecção de recursos e suporte a navegadores

A API Gamepad tem um ótimo suporte a navegadores em computadores e dispositivos móveis. É possível detectar se a API Gamepad tem suporte usando o snippet abaixo:

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

Como o navegador representa um gamepad

O navegador representa gamepads como objetos Gamepad. Um Gamepad tem as seguintes propriedades:

  • id: uma string de identificação para o gamepad. Essa string identifica a marca ou o estilo do dispositivo de gamepad conectado.
  • displayId: o VRDisplay.displayId de um VRDisplay associado (se relevante).
  • index: o índice do gamepad no navegador.
  • connected: indica se o gamepad ainda está conectado ao sistema.
  • hand: um tipo enumerado que define em qual mão o controle está sendo segurado ou em qual é mais provável que seja segurado.
  • timestamp: a última vez que os dados do gamepad foram atualizados.
  • mapping: o mapeamento de botões e eixos em uso para esse dispositivo, "standard" ou "xr-standard".
  • pose: um objeto GamepadPose que representa as informações de pose associadas a um controlador do WebVR.
  • axes: uma matriz de valores para todos os eixos do gamepad, linearmente normalizada para o intervalo de -1.0 a 1.0.
  • buttons: uma matriz de estados de botões para todos os botões do gamepad.

Os botões podem ser digitais (pressionados ou não pressionados) ou analógicos (por exemplo, 78% pressionados). Por isso, os botões são informados como objetos GamepadButton, com os seguintes atributos:

  • pressed: o estado pressionado do botão (true se o botão for pressionado e false se não for.
  • touched: o estado tocado do botão. Se o botão for capaz de detectar toques, essa propriedade será true se o botão estiver sendo tocado e false caso contrário.
  • value: para botões com um sensor analógico, essa propriedade representa a quantidade de vezes que o botão foi pressionado, normalizado linearmente para o intervalo de 0.0 a 1.0.
  • hapticActuators: uma matriz que contém objetos GamepadHapticActuator, cada um representando um hardware de feedback tátil disponível no controle.

Dependendo do navegador e do gamepad, você pode encontrar uma propriedade vibrationActuator. Ele permite dois tipos de efeitos de vibração:

  • Dual-Rumble: o efeito de retorno tátil gerado por dois atuadores de massa rotativa excêntricos, um em cada empunhadura do gamepad.
  • Trigger-Rumble: o efeito de retorno tátil gerado por dois motores independentes, com um motor localizado em cada gatilho do gamepad.

A visão geral esquemática a seguir, tirada diretamente da especificação, mostra o mapeamento e a disposição dos botões e eixos em um gamepad genérico.

Visão geral esquemática dos mapeamentos de botões e eixos de um gamepad comum.
Representação visual de um layout de gamepad padrão (Fonte).

Notificação quando um gamepad é conectado

Para saber quando um gamepad está conectado, detecte o evento gamepadconnected que é acionado no objeto window. Quando o usuário conecta um gamepad, o que pode acontecer usando USB ou Bluetooth, é acionado um GamepadEvent que tem os detalhes do gamepad em uma propriedade gamepad com o nome adequado. Confira abaixo um exemplo de um controle do Xbox 360 que eu tinha (sim, eu gosto de jogos retrô).

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

Notificação quando um gamepad é desconectado

A notificação de desconexão do gamepad acontece de forma análoga à forma como as conexões são detectadas. Dessa vez, o app detecta o evento gamepaddisconnected. Observe como, no exemplo a seguir, connected agora é false quando eu desconecto o controle do 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
  */
});

O gamepad no loop de jogo

A captura de um gamepad começa com uma chamada para navigator.getGamepads(), que retorna uma matriz com itens Gamepad. A matriz no Chrome sempre tem um comprimento fixo de quatro itens. Se zero ou menos de quatro gamepads estiverem conectados, um item poderá ser null. Sempre verifique todos os itens da matriz e saiba que os gamepads "lembram" do slot e nem sempre estão presentes no primeiro slot disponível.

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

Se um ou vários gamepads estiverem conectados, mas o navigator.getGamepads() ainda informar itens null, talvez seja necessário "ativar" cada gamepad pressionando qualquer um dos botões. Em seguida, você pode consultar os estados do gamepad no loop do jogo, conforme mostrado no código abaixo.

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

Atuador de vibração

A propriedade vibrationActuator retorna um objeto GamepadHapticActuator, que corresponde a uma configuração de motores ou outros acionadores que podem aplicar uma força para fins de feedback tátil. Os efeitos táteis podem ser reproduzidos chamando Gamepad.vibrationActuator.playEffect(). Os únicos tipos de efeito válidos são 'dual-rumble' e 'trigger-rumble'.

Efeitos de vibração compatíveis

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

Vibração dupla

O Dual-rumble descreve uma configuração háptica com um motor de vibração de massa rotativa excêntrica em cada alavanca de um gamepad padrão. Nessa configuração, ambos os motores podem vibrar o gamepad inteiro. As duas massas são desiguais para que os efeitos de cada uma possam ser combinados para criar efeitos táteis mais complexos. Os efeitos de vibração dupla são definidos por quatro parâmetros:

  • duration: define a duração do efeito de vibração em milissegundos.
  • startDelay: define a duração do atraso até que a vibração seja iniciada.
  • strongMagnitude e weakMagnitude: defina os níveis de intensidade da vibração para os motores de massa rotativa excêntrica mais pesados e mais leves, normalizados para o intervalo 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,
  });
};

Acionar vibração

O gatilho com vibração é o efeito de retorno tátil gerado por dois motores independentes, com um motor localizado em cada gatilho do gamepad.

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

Integração com a Política de permissões

A especificação da API Gamepad define um recurso controlado por política identificado pela string "gamepad". O allowlist padrão é "self". A política de permissões de um documento determina se algum conteúdo nesse documento tem permissão para acessar navigator.getGamepads(). Se desativado em qualquer documento, nenhum conteúdo no documento poderá usar navigator.getGamepads(), nem os eventos gamepadconnected e gamepaddisconnected serão acionados.

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

Demonstração

Uma demonstração do testador de gamepad está incorporada no exemplo a seguir. O código-fonte está disponível no Glitch. Para testar a demonstração, conecte um gamepad usando USB ou Bluetooth e pressione qualquer botão ou mova qualquer eixo.

Bônus: jogue o dinossauro do Chrome no web.dev

Você pode jogar o Dinossauro do Chrome com seu gamepad neste site. O código-fonte está disponível no GitHub. Confira a implementação de pesquisa de gamepad em trex-runner.js e observe como ela emula pressionamentos de tecla.

Para que a demonstração do gamepad do dinossauro do Chrome funcione, removi o jogo do dinossauro do Chrome do projeto principal do Chromium (atualizei um esforço anterior do Arnelle Ballane), coloquei em um site independente, estendi a implementação da API do gamepad adicionando efeitos de abatimento e vibração, criei um modo de tela cheia e Mehul Satardekar contribuiu com uma implementação do modo escuro. Divirta-se!

Agradecimentos

Este documento foi revisado por François Beaufort e Joe Medley. A especificação da API Gamepad é editada por Steve Agoston, James Hollyer e Matt Reynolds. Os editores de especificações anteriores são Brandon Jones, Scott Graham e Ted Mielczarek. A especificação das extensões do Gamepad é editada por Brandon Jones. Imagem principal de Laura Torrent Puig.