Curta o jogo do dinossauro do Chrome com seu gamepad

Saiba como usar a API Gamepad para turbinar seus jogos de navegador.

O easter egg da página off-line do Chrome é um dos segredos mais mal guardados da história ([citation needed], mas a afirmação foi feita para dar um 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 fliperama. Você já deve saber que não precisa ficar off-line para jogar: no Chrome, basta acessar about://dino ou, se você for mais geek, about://network-error/-106. Mas você sabia que o jogo do dinossauro do Chrome é jogado 270 milhões de vezes por mês?

Página off-line do Chrome com o jogo Dinossauro do Chrome.
Pressione 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 ao 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. Nesta postagem, quero mostrar como usar a API Gamepad.

Usar a API Gamepad

Detecção de recursos e suporte do navegador

A API Gamepad tem um ótimo suporte de navegador universal em computadores e dispositivos móveis. É possível detectar se a API Gamepad é compatível usando o seguinte snippet:

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

Como o navegador representa um gamepad

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

  • id: uma string de identificação do 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 é mais provável que esteja.
  • timestamp: a última vez que os dados desse gamepad foram atualizados.
  • mapping: o mapeamento de botões e eixos em uso para esse dispositivo, que pode ser "standard" ou "xr-standard".
  • pose: um objeto GamepadPose que representa as informações de pose associadas a um controlador WebVR.
  • axes: uma matriz de valores para todos os eixos do gamepad, normalizados linearmente para o intervalo de -1.0–1.0.
  • buttons: uma matriz de estados de 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, 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 estiver pressionado e false se não estiver).
  • touched: o estado tocado do botão. Se o botão for capaz de detectar toque, essa propriedade será true se o botão estiver sendo tocado e false caso contrário.
  • value: para botões que têm um sensor analógico, essa propriedade representa o quanto 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 o hardware de retorno tátil disponível no controle.

Dependendo do navegador e do gamepad que você tem, pode ser que você encontre 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êntrica, 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 um dos gatilhos do gamepad.

A visão geral esquemática a seguir, retirada 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 por USB ou Bluetooth, um GamepadEvent é disparado com os detalhes do gamepad em uma propriedade gamepad com um nome apropriado. A seguir, você pode ver um exemplo de um controle do Xbox 360 que eu tinha por aí (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ões de gamepad acontece de forma análoga à detecção de conexões. Desta vez, o app detecta o evento gamepaddisconnected. No exemplo a seguir, connected agora é false quando 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

Para acessar um gamepad, comece com uma chamada para navigator.getGamepads(), que retorna uma matriz com itens Gamepad. A matriz no Chrome sempre tem um tamanho fixo de quatro itens. Se zero ou menos de quatro gamepads estiverem conectados, um item poderá ser apenas null. Sempre verifique todos os itens da matriz e lembre-se de que os gamepads "memorizam" o 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 navigator.getGamepads() ainda informar null itens, talvez seja necessário "ativar" cada gamepad pressionando qualquer um dos botões. Em seguida, você pode consultar os estados do gamepad no loop de jogo, conforme mostrado no código a seguir.

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 resposta 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 termo "vibração dupla" descreve uma configuração tátil com um motor de vibração de massa rotativa excêntrica em cada alça de um gamepad padrão. Nessa configuração, qualquer um dos motores pode 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 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 de vibração para os motores de massa rotativa excêntrica mais pesados e mais leves, normalizados para o intervalo 0.0–1.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,
  });
};

Vibração do gatilho

A vibração do gatilho é o efeito de retorno tátil gerado por dois motores independentes, com um motor localizado em cada um dos gatilhos 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 qualquer conteúdo nele pode acessar navigator.getGamepads(). Se desativado em qualquer documento, nenhum conteúdo no documento poderá usar navigator.getGamepads(), e os eventos gamepadconnected e gamepaddisconnected não 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 GitHub. Teste a demonstração conectando um gamepad por USB ou Bluetooth e pressionando qualquer um dos botões ou movendo qualquer um dos eixos.

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 da sondagem do gamepad em trex-runner.js e observe como ela emula as teclas pressionadas.

Para que a demonstração do gamepad do Dinossauro do Chrome funcione, extraí o jogo do Dinossauro do Chrome do projeto principal do Chromium (atualizando um esforço anterior de Arnelle Ballane), o coloquei em um site independente, estendi a implementação atual da API Gamepad adicionando efeitos de agachamento e vibração, criei um modo de tela cheia e Mehul Satardekar contribuiu com uma implementação do modo escuro. Bom jogo!

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