Skip to content

Manipulação de Entrada em Jogos ​

Uma boa manipulação de entrada pode determinar o sucesso ou o fracasso de um jogo. Controles com atraso, entradas não detectadas ou mapeamentos de botões confusos frustram os jogadores antes mesmo que eles vejam o conteúdo do seu jogo. Este tutorial mostra como lidar com teclado, mouse, toque e controle de forma unificada.

Experimente primeiro:

O Padrão de Estado de Entrada ​

A abordagem ingênua é reagir diretamente aos eventos: quando o usuário pressiona uma tecla, faça algo. Isso causa problemas. Os eventos são disparados em momentos imprevisíveis, enquanto o loop do jogo é executado em uma frequência fixa. Se você mover o jogador no manipulador de keydown, a velocidade do movimento dependerá da taxa de repetição das teclas, que varia conforme o sistema operacional.

Em vez disso, mantenha um objeto de estado de entrada que seja lido pelo loop do jogo:

js
const input = {
  left: false,
  right: false,
  up: false,
  down: false,
  action: false,
  pointer: { x: 0, y: 0, down: false },
}

Os manipuladores de eventos definem esses indicadores. O loop do jogo os lê. Esse desacoplamento simplifica tudo: sua lógica de atualização não precisa saber se a entrada veio do teclado, do toque ou do controle.

Teclado ​

Use e.code em vez de e.key, pois ele se refere à posição física da tecla. Assim, o WASD funciona corretamente em layouts diferentes do QWERTY.

js
const keyMap = {
  ArrowLeft: 'left',
  ArrowRight: 'right',
  ArrowUp: 'up',
  ArrowDown: 'down',
  KeyA: 'left',
  KeyD: 'right',
  KeyW: 'up',
  KeyS: 'down',
  Space: 'action',
}

window.addEventListener('keydown', (e) => {
  const action = keyMap[e.code]
  if (action) {
    input[action] = true
    e.preventDefault()
  }
})

window.addEventListener('keyup', (e) => {
  const action = keyMap[e.code]
  if (action) input[action] = false
})

O e.preventDefault() impede que as teclas de seta rolem a página. Chame-o apenas para as teclas que você está tratando.

Mouse ​

As coordenadas do mouse são fornecidas no espaço da tela. Você precisa convertê-las para o espaço do canvas:

js
const canvas = document.getElementById('game')

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = e.clientX - rect.left
  input.pointer.y = e.clientY - rect.top
})

canvas.addEventListener('mousedown', () => {
  input.pointer.down = true
  input.action = true
})

canvas.addEventListener('mouseup', () => {
  input.pointer.down = false
  input.action = false
})

Se o seu canvas estiver redimensionado (consulte o tutorial sobre canvas responsivo), você precisará dividir pelo fator de escala para obter as coordenadas do jogo.

Toque ​

O toque funciona de forma semelhante ao mouse, mas você precisa usar e.preventDefault() para impedir que o navegador role a página ou aplique zoom:

js
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
  input.pointer.down = true
  input.action = true
}, { passive: false })

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
}, { passive: false })

canvas.addEventListener('touchend', () => {
  input.pointer.down = false
  input.action = false
})

O { passive: false } é necessário porque, por padrão, os navegadores modernos usam listeners passivos para eventos de toque por motivos de desempenho. Sem isso, preventDefault não funcionará.

Direcional Virtual para Dispositivos Móveis ​

Para jogos que precisam de controles direcionais em dispositivos móveis, adicione botões à tela:

js
function createVirtualDpad(container) {
  const dpad = document.createElement('div')
  dpad.className = 'virtual-dpad'
  dpad.innerHTML = `
    <button data-dir="up">↑</button>
    <button data-dir="left">←</button>
    <button data-dir="right">→</button>
    <button data-dir="down">↓</button>
  `
  
  dpad.querySelectorAll('button').forEach(btn => {
    const dir = btn.dataset.dir
    btn.addEventListener('touchstart', (e) => {
      e.preventDefault()
      input[dir] = true
    })
    btn.addEventListener('touchend', () => input[dir] = false)
  })
  
  container.appendChild(dpad)
}

Estilize esses botões para que sejam grandes o suficiente para os polegares (pelo menos 44x44 pixels) e posicione-os nos cantos inferiores.

Controle ​

Ao contrário do teclado e do mouse, o estado do controle não é orientado por eventos. Você precisa consultá-lo a cada frame:

js
function pollGamepads() {
  const gamepads = navigator.getGamepads()
  const gp = gamepads[0]
  
  if (gp) {
    // Direcional ou analógico esquerdo
    input.left = gp.buttons[14]?.pressed || gp.axes[0] < -0.5
    input.right = gp.buttons[15]?.pressed || gp.axes[0] > 0.5
    input.up = gp.buttons[12]?.pressed || gp.axes[1] < -0.5
    input.down = gp.buttons[13]?.pressed || gp.axes[1] > 0.5
    
    // Botão A (mapeamento padrão)
    input.action = gp.buttons[0]?.pressed
  }
}

Chame pollGamepads() no início da sua função de atualização. O limite de 0,5 nos analógicos cria uma zona morta para que pequenos movimentos não sejam registrados como entrada.

Você pode detectar quando controles são conectados e desconectados:

js
window.addEventListener('gamepadconnected', (e) => {
  console.log('Controle conectado:', e.gamepad.id)
})

window.addEventListener('gamepaddisconnected', (e) => {
  console.log('Controle desconectado:', e.gamepad.id)
})

Usando a Entrada no Loop do Jogo ​

Agora, sua função de atualização apenas lê o estado de entrada:

js
function update(dt) {
  pollGamepads()
  
  if (input.left) player.x -= player.speed * dt
  if (input.right) player.x += player.speed * dt
  if (input.up) player.y -= player.speed * dt
  if (input.down) player.y += player.speed * dt
  
  if (input.action && player.canShoot) {
    player.shoot()
  }
}

Isso funciona independentemente de a entrada ter vindo do teclado, do toque ou do controle. A lógica do jogo não precisa saber.

Recém-Pressionado vs. Mantido Pressionado ​

Às vezes, você quer detectar quando um botão acabou de ser pressionado neste frame, não se ele está sendo mantido pressionado. Pular é um bom exemplo: você quer um salto por pressionamento, não saltos contínuos enquanto o botão estiver pressionado.

js
const inputPrev = { ...input }

function wasJustPressed(action) {
  return input[action] && !inputPrev[action]
}

function endFrame() {
  Object.assign(inputPrev, input)
}

Chame endFrame() no final do seu loop de atualização. Depois, use wasJustPressed('action') em vez de input.action para ações que devem acontecer uma única vez.

Perda de Foco ​

Quando a janela do navegador perde o foco, os eventos do teclado deixam de ser disparados. Se o jogador estava segurando uma tecla, ela fica presa no estado ativo:

js
window.addEventListener('blur', () => {
  input.left = input.right = input.up = input.down = input.action = false
})

Sem isso, o jogador continua se movendo quando o usuário alterna para outra janela com Alt+Tab.

Boas Práticas ​

Ofereça suporte tanto a WASD quanto às teclas de seta, pois os jogadores têm preferências diferentes. Permita remapear os controles e armazene a configuração no localStorage. Disponibilize controles por toque em dispositivos móveis. Teste com controles reais, pois os controles de Xbox, PlayStation e modelos genéricos se comportam de maneiras ligeiramente diferentes. E sempre trate a perda de foco adequadamente.

Mais Recursos ​

Análise detalhada da Gamepad API aborda recursos avançados de controles, como vibração e suporte a vários jogadores.

Jogos web adaptados para dispositivos móveis aborda a entrada por toque em mais detalhes.

Pointer Lock para jogos de FPS mostra como capturar o mouse para controles em primeira pessoa.

Loop de jogo com Canvas 2D mostra o padrão de estado de entrada integrado a um loop de jogo completo.

Noções básicas de multijogador com WebSocket aborda a sincronização de entradas entre jogadores conectados pela rede.

Recursos Externos ​