Curta o jogo do dinossauro do Chrome com seu gamepad

Aprenda a usar a API Gamepad para aprimorar seus jogos da Web.

O easter egg de páginas off-line do Chrome é um dos segredos mais mal guardados da história ([citation needed], mas alegação feita para o efeito dramático). Se você pressionar a tecla de espaço ou, em um dispositivo móvel, toque no dinossauro, a página off-line vira um jogo de arcade. Talvez você saiba que você não precisa ficar off-line quando quiser jogar: no Chrome, basta navegar para about://dino ou, para os geeks, navegue até about://network-error/-106. Mas você sabia que existem 270 milhões de jogos de dinossauros do Chrome todos os meses?

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

Outro fato que, sem dúvida, é mais útil de conhecer e do qual talvez não esteja ciente é que, modo arcade, você pode jogar com um gamepad. O suporte para Gamepad foi adicionado há cerca de um ano, como da época em que este artigo foi escrito commit para Reilly Grant. Como você pode ver, o jogo, assim como os demais do Chromium, está totalmente código aberto. Em quero mostrar a você como usar a API Gamepad.

Usar a API Gamepad

Detecção de recursos e compatibilidade com navegadores

A API Gamepad oferece um suporte a navegadores universalmente excelente em ambos computadores e dispositivos móveis. Você pode 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 Gamepad. objetos. 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: um tipo enumerado que define em que mão o controle é segurado ou que tem mais probabilidade de ser segurado.
  • timestamp: a última vez que os dados desse gamepad foram atualizados.
  • mapping: o mapeamento de botão e eixos em uso neste dispositivo, "standard" ou "xr-standard"
  • pose: um objeto GamepadPose. que representam as informações de pose associadas a um controlador da WebVR.
  • axes: uma matriz de valores para todos os eixos do gamepad, linearmente normalizados no 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). Isso é por isso que os botões são informados como objetos GamepadButton, com estes atributos:

  • pressed: o estado do botão pressionado (true se o botão for pressionado, e false) se ele não estiver pressionado.
  • touched: o estado de toque do botão. Se o botão for capaz de detectar toque, este será true se o botão estiver sendo tocado e false se não for.
  • value: para botões com sensor analógico, esta propriedade representa o valor em que o o botão for pressionado, normalizado de maneira linear para o intervalo de 0.0 a 1.0.
  • hapticActuators: uma matriz contendo GamepadHapticActuator cada um representando um hardware de retorno tátil disponível no controle.

Outra coisa que você pode encontrar, dependendo do seu navegador e do gamepad que você possui, é uma propriedade vibrationActuator. Ele permite dois tipos de efeitos de ruído:

  • Dual-Rumble: o efeito de retorno tátil gerado por dois atuadores de massa giratórios excêntricos, um em cada alça do gamepad.
  • Trigger-Rumble: o efeito de retorno tátil gerado por dois motores independentes, um em cada gatilho do gamepad.

A visão geral esquemática a seguir, direto da especificação, 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 padrão de gamepad (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 é acionado com os detalhes do gamepad em uma propriedade gamepad devidamente nomeada. A seguir, você pode ver um exemplo de um controle Xbox 360 que eu tinha parado (sim, eu gosto 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 do gamepad acontece de maneira semelhante à forma como as conexões são detectadas. Desta vez, o app detectará 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 seu loop de jogo

A ação de controlar um gamepad começa com uma chamada para navigator.getGamepads(), que retorna uma matriz com Gamepad itens. A matriz no Chrome sempre tem um tamanho fixo de quatro itens. Se for zero ou menos mais de quatro gamepads estiverem conectados, um item poderá ser null. Sempre verifique todos os itens a matriz e esteja ciente de que os gamepads “lembram” seu espaço e nem sempre estão presentes no primeiro espaço 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 null itens, você pode precisar "acordar" cada gamepad pressionando qualquer um dos botões dele. Depois, você pode fazer pesquisas no 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 o retorno tátil feedback. Para reproduzir efeitos táteis, chame Gamepad.vibrationActuator.playEffect(). O único os tipos de efeito válidos são 'dual-rumble' e 'trigger-rumble'.

Efeitos de ruído 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.
}

Som duplo

O Dual-rumble descreve uma configuração tátil com um motor de vibração de massa rotativo excêntrico em cada alça de um gamepad padrão. Nessa configuração, qualquer motor é capaz de vibrar todo o gamepad. As duas massas são desiguais, de modo que os efeitos de cada um deles podem ser combinados para criar efeitos táteis mais complexos. Os efeitos de Dual-rumble são 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: definem os níveis de intensidade de vibração para as frequências mais intensas e motores de massa giratórios excêntricos 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,
  });
};

Balanço do gatilho

O ruído de gatilho é o efeito de retorno tátil gerado por dois motores independentes, com um deles 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íticas identificado pelo 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 desativada em qualquer documento, nenhum conteúdo do documento terá permissão para usar o navigator.getGamepads(), nem os eventos gamepadconnected e gamepaddisconnected são disparados.

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

Demonstração

Uma demonstração do gamepad testador está incorporada no exemplo a seguir. O código-fonte está disponível no Glitch. Experimente a demonstração conectando um um gamepad usando USB ou Bluetooth e pressionando qualquer um de seus botões ou movendo qualquer um de seu eixo.

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

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

Para que a demonstração do gamepad do dinossauro do Chrome funcione, tenho o jogo do dinossauro do Chrome do projeto principal do Chromium (atualizando um esforço anterior do Arnelle Ballane), o colocou em um site independente, estendeu a implementação existente da API de gamepad adicionando efeitos de redução de som e vibração, criou uma e Mehul Satardekar contribuíram com um modo escuro implementação. Aproveite!

Agradecimentos

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