Superando obstáculos com a API Gamepad

Marcin wichary
Marcin Wichary

Introdução

Deixe os novatos manterem seus teclados para jogos de aventura, seus preciosos dedos multitoques para cortar frutas e seus sofisticados sensores de movimento modernos para fingir que sabem dançar como Michael Jackson. (Newsflash: eles não podem.) Mas você é diferente. Você é melhor. Você é incrível. Para você, os jogos começam e terminam com um gamepad nas suas mãos.

Opa, peraí. Você não está sem sorte se quiser oferecer suporte a um gamepad em seu aplicativo da web? Não teria. A nova API Gamepad é a salvação, permitindo que você use o JavaScript para ler o estado de qualquer controlador de gamepad conectado ao seu computador. Ele é tão recente que só foi lançado no Chrome 21 na semana passada. Além disso, está prestes a ser compatível com o Firefox (atualmente disponível em uma versão especial).

Esse momento acabou sendo um ótimo momento, porque tivemos a chance de usá-lo recentemente no doodle do Google Hurdles 2012 (link em inglês). Este artigo explicará brevemente como adicionamos a API Gamepad ao doodle e o que aprendemos durante o processo.

Doodle do Google com Barreiras 2012
Doodle do Google Hurdles 2012

Testador de gamepad

Por mais efêmeros, os doodles interativos tendem a ser bastante complexos internamente. Para facilitar a demonstração do que estamos falando, pegamos o código do gamepad do doodle e montamos um testador de gamepad simples. Ele pode ser usado para conferir se o gamepad USB funciona corretamente e também para conferir como ele é feito.

Quais navegadores são compatíveis atualmente?

Compatibilidade com navegadores

  • 21
  • 12
  • 29
  • 10.1

Origem

Quais gamepads podem ser usados?

Geralmente, qualquer gamepad moderno com suporte ao seu sistema deve funcionar de forma nativa. Testamos vários gamepads em um PC, desde controles USB fora da marca, até PlayStation 2 conectados por um dongle a um Mac, até controles Bluetooth pareados com um notebook Chrome OS.

Gamepads
Gamepads

Esta é a foto de alguns controles que usamos para testar nosso doodle: "Sim, mãe, é isso que eu faço no trabalho". Se seu controlador não funcionar ou se os controles estiverem mapeados incorretamente, registre um bug no Chrome ou no Firefox . Teste na versão mais recente de cada navegador para ter certeza de que isso ainda não está corrigido.

Detecção de recurso da API Gamepad<

Fácil no Chrome:

var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;

Ainda não é possível detectar isso no Firefox. Tudo é baseado em eventos e todos os manipuladores de eventos precisam estar conectados à janela, o que impede o funcionamento de uma técnica típica de detecção de manipuladores de eventos.

Mas temos certeza de que isso é temporário. O incrível Modernizr já informa você sobre a API Gamepad, então recomendamos esse recurso para todas as suas necessidades de detecção atuais e futuras:

var gamepadSupportAvailable = Modernizr.gamepads;

Como descobrir gamepads conectados

Mesmo se você conectar o gamepad, ele não se manifestará de forma alguma, a menos que o usuário pressione qualquer um dos botões primeiro. Isso é para evitar o uso de técnicas de impressão digital, embora isso seja um pouco desafiador para a experiência do usuário: você não pode pedir para o usuário pressionar o botão ou fornecer instruções específicas de gamepad porque não tem ideia se ele conectou o controle.

Depois de superar esse obstáculo (desculpe...), no entanto, mais um pouco esperará.

Frequência de coleta

A implementação da API pelo Chrome expõe a função navigator.webkitGetGamepads(), que pode ser usada para consultar uma lista de todos os gamepads conectados ao sistema, além do estado atual (botões e sticks). O primeiro gamepad conectado será retornado como a primeira entrada na matriz, e assim por diante.

Essa chamada de função acabou de substituir uma matriz que você pode acessar diretamente: navigator.webkitGamepads[]. Desde o início de agosto de 2012, o acesso a essa matriz ainda é necessário no Chrome 21, enquanto a chamada de função funciona no Chrome 22 e em versões mais recentes. Daqui para frente, a chamada de função é a maneira recomendada de usar a API e, lentamente, ela será transmitida aos poucos a todos os navegadores Chrome instalados.

A parte implementada da especificação até agora requer a verificação contínua do estado dos gamepads conectados (e a comparação com o anterior, se necessário), em vez de disparar eventos quando as coisas mudarem. confiamos em requestAnimationFrame() para configurar a pesquisa da maneira mais eficiente e de baixo consumo de bateria. Para nosso doodle, embora já exista uma repetição requestAnimationFrame() para oferecer suporte a animações, criamos uma segunda repetição completamente separada: era mais simples de codificar e não afetava o desempenho de forma alguma.

Aqui está o código do testador:

/**
 * Starts a polling loop to check for gamepad state.
 */
startPolling: function() {
    // Don't accidentally start a second loop, man.
    if (!gamepadSupport.ticking) {
    gamepadSupport.ticking = true;
    gamepadSupport.tick();
    }
},

/**
 * Stops a polling loop by setting a flag which will prevent the next
 * requestAnimationFrame() from being scheduled.
 */
stopPolling: function() {
    gamepadSupport.ticking = false;
},

/**
 * A function called with each requestAnimationFrame(). Polls the gamepad
 * status and schedules another poll.
 */
tick: function() {
    gamepadSupport.pollStatus();
    gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
    // Only schedule the next frame if we haven't decided to stop via
    // stopPolling() before.
    if (gamepadSupport.ticking) {
    if (window.requestAnimationFrame) {
        window.requestAnimationFrame(gamepadSupport.tick);
    } else if (window.mozRequestAnimationFrame) {
        window.mozRequestAnimationFrame(gamepadSupport.tick);
    } else if (window.webkitRequestAnimationFrame) {
        window.webkitRequestAnimationFrame(gamepadSupport.tick);
    }
    // Note lack of setTimeout since all the browsers that support
    // Gamepad API are already supporting requestAnimationFrame().
    }
},

/**
 * Checks for the gamepad status. Monitors the necessary data and notices
 * the differences from previous state (buttons for Chrome/Firefox,
 * new connects/disconnects for Chrome). If differences are noticed, asks
 * to update the display accordingly. Should run as close to 60 frames per
 * second as possible.
 */
pollStatus: function() {
    // (Code goes here.)
},

Se você só se importa com um gamepad, conseguir os dados pode ser tão simples quanto:

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

Se você quiser ser mais inteligente ou oferecer suporte a mais de um jogador ao mesmo tempo, vai precisar adicionar mais algumas linhas de código para reagir a cenários mais complexos (dois ou mais gamepads conectados, alguns sendo desconectados no meio etc.). Consulte o código-fonte do testador, a função pollGamepads(), para ver uma abordagem de como resolver isso.

Eventos

O Firefox usa uma forma alternativa e melhor descrita na especificação da API Gamepad. Em vez de solicitar uma pesquisa, ele expõe dois eventos, MozGamepadConnected e MozGamepadDisconnected, que são disparados sempre que um gamepad é conectado (ou, mais precisamente, conectado e "anunciado" ao pressionar qualquer um dos botões) ou desconectado. O objeto de gamepad que continuará a refletir o estado futuro é transmitido como o parâmetro .gamepad do objeto de evento.

No código-fonte do testador:

/**
 * React to the gamepad being connected. Today, this will only be executed
 * on Firefox.
 */
onGamepadConnect: function(event) {
    // Add the new gamepad on the list of gamepads to look after.
    gamepadSupport.gamepads.push(event.gamepad);

    // Start the polling loop to monitor button changes.
    gamepadSupport.startPolling();

    // Ask the tester to update the screen to show more gamepads.
    tester.updateGamepads(gamepadSupport.gamepads);
},

Resumo

No final, nossa função de inicialização no testador, com suporte para as duas abordagens, tem esta aparência:

/**
 * Initialize support for Gamepad API.
 */
init: function() {
    // As of writing, it seems impossible to detect Gamepad API support
    // in Firefox, hence we need to hardcode it in the third clause.
    // (The preceding two clauses are for Chrome.)
    var gamepadSupportAvailable = !!navigator.webkitGetGamepads ||
        !!navigator.webkitGamepads ||
        (navigator.userAgent.indexOf('Firefox/') != -1);

    if (!gamepadSupportAvailable) {
    // It doesn't seem Gamepad API is available – show a message telling
    // the visitor about it.
    tester.showNotSupported();
    } else {
    // Firefox supports the connect/disconnect event, so we attach event
    // handlers to those.
    window.addEventListener('MozGamepadConnected',
                            gamepadSupport.onGamepadConnect, false);
    window.addEventListener('MozGamepadDisconnected',
                            gamepadSupport.onGamepadDisconnect, false);

    // Since Chrome only supports polling, we initiate polling loop straight
    // away. For Firefox, we will only do it if we get a connect event.
    if (!!navigator.webkitGamepads || !!navigator.webkitGetGamepads) {
        gamepadSupport.startPolling();
    }
    }
},

Informações do gamepad

Cada gamepad conectado ao sistema será representado por um objeto semelhante a este:

id: "PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)"
index: 1
timestamp: 18395424738498
buttons: Array[8]
    0: 0
    1: 0
    2: 1
    3: 0
    4: 0
    5: 0
    6: 0.03291
    7: 0
axes: Array[4]
    0: -0.01176
    1: 0.01961
    2: -0.00392
    3: -0.01176

Informações básicas

Os poucos campos principais são metadados simples:

  • id: uma descrição textual do gamepad.
  • index: um número inteiro útil para diferenciar gamepads conectados a um computador separadamente.
  • timestamp: o carimbo de data/hora da última atualização do estado dos botões/eixos (atualmente compatível apenas no Chrome).

Botões e bastões

Os gamepads de hoje não são exatamente o que seu avô poderia ter usado para salvar a princesa no castelo errado. Eles geralmente têm pelo menos 16 botões separados (alguns discretos, alguns análogos), além de dois bastões análogos. A API Gamepad vai informar você sobre todos os botões e stick analógicos que são informados pelo sistema operacional.

Depois de receber o estado atual no objeto do gamepad, você pode acessar os botões usando .buttons[] e sticks usando as matrizes .axes[]. Veja um resumo visual do que eles correspondem:

Diagrama do gamepad
Diagrama do Gamepad

A especificação solicita que o navegador mapeie os primeiros dezesseis botões e quatro eixos para:

gamepad.BUTTONS = {
    FACE_1: 0, // Face (main) buttons
    FACE_2: 1,
    FACE_3: 2,
    FACE_4: 3,
    LEFT_SHOULDER: 4, // Top shoulder buttons
    RIGHT_SHOULDER: 5,
    LEFT_SHOULDER_BOTTOM: 6, // Bottom shoulder buttons
    RIGHT_SHOULDER_BOTTOM: 7,
    SELECT: 8,
    START: 9,
    LEFT_ANALOGUE_STICK: 10, // Analogue sticks (if depressible)
    RIGHT_ANALOGUE_STICK: 11,
    PAD_TOP: 12, // Directional (discrete) pad
    PAD_BOTTOM: 13,
    PAD_LEFT: 14,
    PAD_RIGHT: 15
};

gamepad.AXES = {
    LEFT_ANALOGUE_HOR: 0,
    LEFT_ANALOGUE_VERT: 1,
    RIGHT_ANALOGUE_HOR: 2,
    RIGHT_ANALOGUE_VERT: 3
};

Os botões e eixos extras serão anexados aos eixos acima. No entanto, nem os 16 botões nem os 4 eixos são garantidos. Alguns deles estarão indefinidos.

Os botões podem assumir valores de 0,0 (não pressionado) a 1,0 (pressionado completamente). Os eixos vão de -1,0 (completamente para a esquerda ou para cima) a 0,0 (centro) até 1,0 (completamente para a direita ou para baixo).

Analógico ou discreto?

É possível que cada botão seja semelhante a um botão análogo. Isso é um pouco comum para botões laterais, por exemplo. Portanto, é melhor definir um limite em vez de apenas compará-lo sem cortes com 1,00.E se um botão analógico estiver um pouco sujo? pode nunca chegar a 1,00). No nosso doodle, fazemos isso desta forma:

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

gamepad.buttonPressed_ = function(pad, buttonId) {
    return pad.buttons[buttonId] &&
            (pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};

Você pode fazer o mesmo para transformar joysticks analógicos em joysticks digitais. Claro, sempre há um botão digital, mas seu gamepad pode não ter um. Confira nosso código para lidar com isso:

gamepad.AXIS_THRESHOLD = .75;

gamepad.stickMoved_ = function(pad, axisId, negativeDirection) {
    if (typeof pad.axes[axisId] == 'undefined') {
    return false;
    } else if (negativeDirection) {
    return pad.axes[axisId] < -gamepad.AXIS_THRESHOLD;
    } else {
    return pad.axes[axisId] > gamepad.AXIS_THRESHOLD;
    }
};

Pressionamento de botões e movimentos do stick

Eventos

Em alguns casos, como em um jogo de simulação de voo, verificar e reagir continuamente às posições de stick ou ao pressionamento de botões faz mais sentido... mas para coisas como o doodle do Hurdles 2012? Você pode se perguntar: por que preciso verificar os botões em cada frame? Por que não consigo obter eventos como os que acontece para o teclado ou o mouse para cima/baixo?

A boa notícia é que isso é possível. A má notícia é que ela vai estar no futuro. Ele está na especificação, mas ainda não foi implementado em nenhum navegador.

Frequência de coleta

Enquanto isso, a saída é comparar o estado atual e o anterior e chamar funções se você notar alguma diferença. Exemplo:

if (buttonPressed(pad, 0) != buttonPressed(oldPad, 0)) {
    buttonEvent(0, buttonPressed(pad, 0) ? 'down' : 'up');
}
for (var i in gamepadSupport.gamepads) {
    var gamepad = gamepadSupport.gamepads[i];

    // Don't do anything if the current timestamp is the same as previous
    // one, which means that the state of the gamepad hasn't changed.
    // This is only supported by Chrome right now, so the first check
    // makes sure we're not doing anything if the timestamps are empty
    // or undefined.
    if (gamepadSupport.prevTimestamps[i] &&
        (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
    continue;
    }
    gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

    gamepadSupport.updateDisplay(i);
}

A abordagem que prioriza o teclado no doodle do Hurdles 2012

Como o método de entrada preferencial do nosso doodle de hoje é o teclado, decidimos que o gamepad o emule de maneira bem próxima. Isso gerou três decisões:

  1. O doodle só precisa de três botões: dois para correr e um para pular. No entanto, é provável que o gamepad tenha muitos outros. Por isso, mapeamos todos os 16 botões conhecidos e dois sticks conhecidos nessas três funções lógicas, de uma forma que achamos que fazia mais sentido, para que as pessoas pudessem usar: alternando botões A/B, alternando botões de ombro, pressionando esquerda/direita no botão direcional ou balançando um dos sticks violentamente para a esquerda e para a direita (alguns desses botões serão, obviamente, mais eficientes do que os outros). Exemplo:

    newState[gamepad.STATES.LEFT] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.PAD_LEFT) ||
        gamepad.stickMoved_(pad, gamepad.AXES.LEFT_ANALOGUE_HOR, true) ||
        gamepad.stickMoved_(pad, gamepad.AXES.RIGHT_ANALOGUE_HOR, true),
    
    newState[gamepad.STATES.PRIMARY_BUTTON] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.FACE_1) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER_BOTTOM) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.SELECT) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.START) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_ANALOGUE_STICK),
    
  2. Tratamos cada entrada analógica como discreta, usando as funções de limite descritas anteriormente.

  3. Chegamos a incluir a entrada do gamepad no doodle, em vez de colocá-la no rastro. Na verdade, nosso loop de pesquisa sintetiza eventos necessários de keydown e keyup (com um keyCode adequado) e os envia de volta ao DOM:

    // Create and dispatch a corresponding key event.
    var event = document.createEvent('Event');
    var eventName = down ? 'keydown' : 'keyup';
    event.initEvent(eventName, true, true);
    event.keyCode = gamepad.stateToKeyCodeMap_[state];
    gamepad.containerElement_.dispatchEvent(event);

Isso é tudo!

Dicas e sugestões

  • Lembre-se de que o gamepad não fica visível no navegador antes de um botão ser pressionado.
  • Se você estiver testando o gamepad em diferentes navegadores simultaneamente, só um deles detectará o controle. Se você não estiver recebendo eventos, feche as outras páginas que podem estar usando esses eventos. Além disso, pela nossa experiência, às vezes um navegador consegue manter o gamepad mesmo que você feche a guia ou saia do navegador. Às vezes, reiniciar o sistema é a única maneira de corrigir problemas.
  • Como sempre, use o Chrome Canary e os equivalentes de outros navegadores para garantir que você esteja recebendo o melhor suporte. Em seguida, se as versões mais antigas se comportarem de maneira diferente, aja de acordo com elas.

O futuro

Esperamos que isso ajude a esclarecer essa nova API - ainda um pouco precária, mas já muito divertida.

Além das partes ausentes da API (por exemplo, eventos) e um suporte mais amplo ao navegador, também esperamos encontrar coisas como controle de ruído, acesso a giroscópios integrados etc. E mais suporte para diferentes tipos de gamepads: registre um bug no Chrome e/ou registre um bug contra o Firefox se você encontrar um que funcione incorretamente ou não funcione.

Mas, antes disso, brinque com nosso doodle Huurdles 2012 e veja como ele é muito mais divertido no gamepad. Você disse que a duração é melhor que 10, 7 segundos? Vamos lá.

Sugestões de leitura