Curta o jogo do dinossauro do Chrome com seu gamepad

Aprenda a usar a API Gamepad para levar seus jogos da Web a um novo patamar.

O easter egg da página off-line do Chrome é um dos segredos mais guardados na história ([citation needed], mas afirma que o efeito é dramático). Se você pressionar a tecla de espaço ou, em dispositivos móveis, tocar no dinossauro, a página off-line se tornará um jogo de arcade. Você pode estar ciente de que não precisa ficar off-line quando quiser jogar: no Chrome, basta navegar até about://dino ou, caso seja um nerd, navegue até about://network-error/-106. Mas você sabia que ocorrem 270 milhões de jogos do dinossauro do Chrome todos os meses?

A página off-line do Chrome com o jogo do dinossauro.
Pressione a tecla de espaço para jogar.

Outro fato que pode ser considerado mais útil e que você talvez não conheça é que, no modo arcade, é possível jogar com um gamepad. O suporte a Gamepad foi adicionado há aproximadamente um ano, no momento em que este artigo foi escrito, em uma confirmação de Reilly Grant (em inglês). Como você pode notar, o jogo, assim como o restante do projeto do Chromium, é totalmente de código aberto. Nesta postagem, quero mostrar como usar a API Gamepad.

Usar a API Gamepad

Detecção de recursos e suporte a navegadores

A API Gamepad tem um suporte a navegadores universalmente excelente em computadores e dispositivos móveis. Você pode detectar se há suporte para a API Gamepad usando o seguinte snippet:

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 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: uma enumeração que define em qual mão o controle está sendo segurado ou provavelmente será segurado.
  • timestamp: a última vez em que os dados deste gamepad foram atualizados.
  • mapping: o mapeamento de botões e eixos em uso no dispositivo, "standard" ou "xr-standard".
  • pose: um objeto GamepadPose que representa as informações de pose associadas a um controle da 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 para todos os botões do gamepad.

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

  • pressed: o estado pressionado do botão (true se o botão for pressionado e false se ele não for pressionado.
  • touched: o estado de toque do botão. Se o botão for capaz de detectar toques, essa propriedade será true se estiver sendo tocado. Caso contrário, será false.
  • value: para botões que têm um sensor analógico, essa propriedade representa a quantidade de pressionamento do botão, que é linearmente normalizada no intervalo de 0.0 a 1.0.
  • hapticActuators: uma matriz contendo objetos GamepadHapticActuator, cada um representando o hardware de retorno tátil disponível no controlador.

Outra coisa que você pode encontrar, dependendo do seu navegador e do gamepad que você tem, é uma propriedade vibrationActuator. Ela permite dois tipos de efeitos de ronco:

  • Dual-Rumble: o efeito de retorno tátil gerado por dois atuadores de massa giratórios excêntricos, um em cada mão 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 (link em inglês), mostra o mapeamento e a organizaçã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 acionado no objeto window. Quando o usuário conecta um gamepad, o que pode acontecer por USB ou Bluetooth, é disparado um GamepadEvent com os detalhes do gamepad em uma propriedade gamepad adequadamente nomeada. A seguir, você pode ver um exemplo de um controle Xbox 360 em que eu estava (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

Receber notificações de desconexões do gamepad acontece de maneira semelhante à forma como as conexões são detectadas. Desta vez, o app detecta o evento gamepaddisconnected. Observe como, no exemplo a seguir, connected agora é false quando desconecta 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 seu 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 que quatro gamepads estiverem conectados, um item poderá ser apenas null. Verifique sempre todos os itens da matriz e esteja ciente de que os gamepads "se lembram" do slot e podem não estar sempre 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 a navigator.getGamepads() ainda informar itens null, talvez seja necessário "ativar" cada gamepad pressionando qualquer um dos botões dele. É possível pesquisar os estados do gamepad no loop de 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();

O atuador de vibração

A propriedade vibrationActuator retorna um objeto GamepadHapticActuator, que corresponde a uma configuração de motores ou outros atuadores que podem aplicar uma força para fins de retorno tátil. Os efeitos táteis podem ser reproduzidos chamando Gamepad.vibrationActuator.playEffect(). O único tipo de efeito válido é 'dual-rumble'. O "Dual Rumble" descreve uma configuração tátil com um motor de vibração de massa giratório excêntrico em cada alça de um gamepad padrão. Nessa configuração, qualquer um dos motores é capaz de vibrar todo o gamepad. 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 Dual Rumble são definidos por quatro parâmetros:

  • duration: define a duração do efeito de vibração em milissegundos.
  • startDelay: define o tempo de espera até que a vibração seja iniciada.
  • strongMagnitude e weakMagnitude: definem os níveis de intensidade de vibração para os motores de massa excêntrica mais pesados e mais leves, normalizados para o intervalo 0.0-1.0.

Efeitos de rumble com suporte

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

Ronco duplo

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

// 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 desse documento tem permissão para acessar o navigator.getGamepads(). Se desativado em qualquer documento, nenhum conteúdo nele poderá usar navigator.getGamepads(), e os eventos gamepadconnected e gamepaddisconnected não serão disparados.

<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 (link em inglês). Teste a demonstração conectando um gamepad por USB ou Bluetooth e pressionando qualquer um dos botões ou movendo qualquer um dos eixos dele.

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 (link em inglês). Confira a implementação da pesquisa do gamepad em trex-runner.js e observe como ela está emulando o pressionamento de teclas.

Para a demonstração do gamepad do dinossauro do Chrome funcionar, removi o jogo do dinossauro do Chrome do projeto principal do Chromium (atualizando uma esforço anterior de Arnelle Ballane), coloquei-o em um site independente, estendemos a implementação da API do gamepad atual adicionando efeitos de redução e vibração, criamos 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 foi editada por Steve Agoston, James Hollyer e Matt Reynolds. Os antigos editores de especificações 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.