Introdução
Deixe os novatos usarem os teclados para jogos de aventura, os preciosos sensores de toque para cortar frutas e os sensores de movimento modernos para fingir que estão dançando como Michael Jackson. (Notícia de última hora: não podem.) Mas você é diferente. Você está melhor. Você é incrível. Para você, os jogos começam e terminam com um gamepad nas mãos.
Mas espere. Você não tem sorte se quiser oferecer suporte a um gamepad no seu app da Web? Isso já não é mais necessário. A nova API Gamepad vem para ajudar, permitindo que você use JavaScript para ler o estado de qualquer controlador de gamepad conectado ao computador. Ele é tão recente que só foi lançado no Chrome 21 na semana passada e está prestes a ser compatível com o Firefox (atualmente disponível em um build especial).
O timing foi ótimo, porque recentemente tivemos a chance de usá-lo no desafio de obstáculos de 2012 do Google. Neste artigo, explicamos brevemente como adicionamos a API Gamepad ao doodle e o que aprendemos durante o processo.
Testador de gamepad
Por mais efêmeros que sejam, os Doodles interativos tendem a ser bastante complexos. Para facilitar a demonstração, pegamos o código do gamepad do doodle e criamos um teste simples. Você pode usá-lo para conferir se o gamepad USB funciona corretamente e também para examinar como isso é feito.
Quais navegadores oferecem suporte a ele atualmente?
Quais gamepads podem ser usados?
Geralmente, qualquer gamepad moderno com suporte nativo do sistema funciona. Testamos vários gamepads de controladores USB de outras marcas em um PC, gamepads PlayStation 2 conectados a um dongle em um Mac e controladores Bluetooth pareados com um notebook Chrome OS.
Esta é uma foto de alguns controles que usamos para testar nosso doodle: "Sim, mãe, é isso mesmo que eu faço no trabalho". Se o controle não funcionar ou se os controles estiverem mapeados incorretamente, envie um bug para o Chrome ou o Firefox . Teste na versão mais recente de cada navegador para garantir que o problema não tenha sido corrigido.
Recurso de detecção 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 ser anexados à janela, o que impede que uma técnica típica de detecção de manipuladores de eventos funcione.
Mas temos certeza de que isso é temporário. O incrível Modernizr já informa sobre a API Gamepad. Recomendamos o uso dele para todas as necessidades de detecção atuais e futuras:
var gamepadSupportAvailable = Modernizr.gamepads;
Como descobrir gamepads conectados
Mesmo que você conecte o gamepad, ele não vai se manifestar de nenhuma forma, a menos que o usuário pressione um dos botões primeiro. Isso é para evitar a impressão digital, embora seja um pouco desafiador para a experiência do usuário: não é possível pedir ao usuário para pressionar o botão ou fornecer instruções específicas para o gamepad porque você não sabe se ele conectou o controle.
Depois de superar essa barreira (desculpe…), mais desafios estão à espera.
Enquetes
A implementação da API do Chrome expõe uma função, navigator.webkitGetGamepads()
, que pode ser usada para receber uma lista de todos os gamepads conectados ao sistema no momento, além do estado atual deles (botões + joysticks). O primeiro gamepad conectado será retornado como a primeira entrada na matriz e assim por diante.
Essa chamada de função substituiu recentemente uma matriz que você podia 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 versões mais recentes. A partir de agora, a chamada de função é a maneira recomendada de usar a API e será implementada aos poucos em todos os navegadores Chrome instalados.)
A parte implementada até o momento da especificação exige que você verifique continuamente o estado dos gamepads conectados (e compare com o anterior, se necessário), em vez de acionar eventos quando as coisas mudam. Usamos requestAnimationFrame() para configurar a pesquisa da maneira mais eficiente e econômica. Para o doodle, mesmo que já tivéssemos um loop requestAnimationFrame()
para oferecer suporte a animações, criamos um segundo loop completamente separado. Ele foi mais fácil de programar e não afetou o desempenho de forma alguma.
Confira 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 interessar por um gamepad, a extração dos dados pode ser tão simples quanto:
var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];
Se você quiser ser um pouco mais esperto ou oferecer suporte a mais de um jogador ao mesmo tempo, adicione mais algumas linhas de código para reagir a cenários mais complexos (dois ou mais gamepads conectados, alguns deles desconectados no meio do caminho etc.). Você pode consultar o código-fonte do nosso testador, função pollGamepads()
, para saber como resolver esse problema.
Eventos
O Firefox usa uma maneira alternativa e melhor descrita na especificação da API Gamepad. Em vez de pedir que você faça uma pesquisa, ele expõe dois eventos, MozGamepadConnected
e MozGamepadDisconnected
, que são acionados sempre que um gamepad é conectado (ou, mais precisamente, conectado e "anunciado" ao pressionar qualquer um dos botões) ou desconectado. O objeto do gamepad que vai continuar refletindo o estado futuro é transmitido como o parâmetro .gamepad
do objeto do 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, que oferece suporte às duas abordagens, fica assim:
/**
* 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 parecido com 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 primeiros campos são metadados simples:
id
: uma descrição textual do gamepadindex
: um número inteiro útil para diferenciar diferentes gamepads conectados a um computadortimestamp
: o carimbo de data/hora da última atualização do estado do botão/eixos (atualmente, só é compatível com o Chrome)
Botões e botões de controle
Os gamepads de hoje não são exatamente o que seu avô usava para salvar a princesa no castelo errado. Eles geralmente têm pelo menos 16 botões separados (alguns discretos, outros analógicos), além de dois joysticks analógicos. A API Gamepad informa todos os botões e sticks analógicos informados pelo sistema operacional.
Depois de receber o estado atual no objeto do gamepad, você pode acessar os botões por .buttons[]
e as alavancas por matrizes .axes[]
. Confira um resumo visual do que eles correspondem:
A especificação pede que o navegador mapeie os primeiros 16 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 acima. No entanto, nem 16 botões nem 4 eixos são garantidos. Alguns deles podem ser indefinidos.
Os botões podem ter valores de 0,0 (não pressionado) a 1,0 (completamente pressionado). Os eixos vão de -1,0 (completamente à esquerda ou para cima) a 0,0 (centro) e 1,0 (completamente à direita ou para baixo).
Analógico ou discreto?
Aparentemente, todos os botões podem ser analógicos, o que é comum para botões de ombro, por exemplo. Portanto, é melhor definir um limite em vez de comparar diretamente com 1,00 (e se um botão analógico estiver um pouco sujo? Ela pode nunca chegar a 1,00). No nosso doodle, fazemos isso assim:
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 sticks analógicos em joysticks digitais. Claro, sempre há o botão digital (D-pad), mas talvez seu gamepad não tenha um. Confira o código para processar 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;
}
};
Pressões de botão e movimentos do manche
Eventos
Em alguns casos, como em um jogo de simulador de voo, faz mais sentido verificar e reagir continuamente às posições do joystick ou pressionamentos de botão. Mas para coisas como o Doodle de 2012 do jogo "Hurdles"? Você pode estar se perguntando: por que preciso verificar os botões em cada frame? Por que não consigo receber eventos como faço com o teclado ou o mouse para cima/para baixo?
A boa notícia é que você pode. Mas há uma má notícia: no futuro. Ele está na especificação, mas ainda não foi implementado em nenhum navegador.
Enquetes
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 de teclado primeiro no doodle de 2012
Como o método de entrada preferido do Doodle de hoje é o teclado, decidimos que o gamepad precisaria emular isso. Isso significa três decisões:
O Doodle precisa apenas de três botões: dois para correr e um para pular, mas o gamepad provavelmente terá muitos mais. Portanto, mapeamos todos os 16 botões e duas alavancas conhecidos para essas três funções lógicas de uma maneira que achamos que fazia mais sentido, para que as pessoas pudessem usar: alternar os botões A/B, alternar os botões de ombro, pressionar esquerda/direita no direcional ou balançar as alavancas violentamente para a esquerda e para a direita. É claro que alguns deles serão mais eficientes do que 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),
Tratamos cada entrada analógica como uma entrada discreta, usando as funções de limite descritas anteriormente.
Nós fomos além e fixamos a entrada do gamepad no doodle, em vez de incorporá-la. Nosso loop de pesquisa sintetiza os eventos keydown e keyup necessários (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
- O gamepad não vai aparecer no navegador antes que um botão seja pressionado.
- Se você estiver testando o gamepad em diferentes navegadores ao mesmo tempo, saiba que apenas um deles pode detectar o controle. Se você não estiver recebendo eventos, feche as outras páginas que podem estar usando. Além disso, pela nossa experiência, às vezes um navegador pode "segurar" o gamepad mesmo que você feche a guia ou saia do navegador. Às vezes, reiniciar o sistema é a única maneira de corrigir o problema.
- Como sempre, use o Chrome Canary e os equivalentes para outros navegadores para garantir o melhor suporte. Em seguida, aja de forma adequada se notar que as versões mais antigas se comportam de maneira diferente.
O futuro
Esperamos que isso ajude a esclarecer essa nova API, que ainda é um pouco precária, mas já é muito divertida.
Além das partes ausentes da API (por exemplo, eventos) e do suporte mais amplo ao navegador, também esperamos ver recursos como controle de vibração, acesso a giroscópios integrados etc. E mais suporte a diferentes tipos de gamepads. Registre um bug no Chrome e/ou registre um bug no Firefox se encontrar um que funcione incorretamente ou não funcione.
Mas antes disso, jogue com nosso desafio de obstáculos de 2012 e veja como é mais divertido com o gamepad. Você disse que poderia fazer melhor do que 10, 7 segundos? Vamos lá.