Como fazer um jogo da cobrina que funciona de verdade
A versão de navegador que a maioria das pessoas tenta construir travando quando precisa lidar com o loop principal. O jogo da cobrina parece simples porque é um exercício clássico de iniciante, mas existem alguns pontos que quebram quem não pensa com antecedência sobre timing e input buffering.
Entendendo o jogo da cobrina
O conceito é direto: uma cobra se move em um grid, come maçãs e cresce, e perde se bater nas paredes ou no próprio corpo. A implementação que costuma funcionar bem usa requestAnimationFrame como motor de renderização com um contador de frames para controlar a velocidade da cobra de forma independente do refresh rate do monitor. Em telas de 60Hz você vai querer atualizar a lógica da cobra a cada 100ms ou algo próximo disso, o que dá cerca de 6 frames por segundo de movimento. Se deixar a cobra se mover a cada frame sem throttle, ela atravessa o grid numa velocidade impossível de jogar. O grid em si pode ser simples mesmo, tipo 20 por 20 células, cada uma com 20 pixels. Canvas 2D resolve tudo sem precisar de bibliotecas externas. Vou mostrar o esqueleto funcional abaixo.
Código base
Isso aqui é o mínimo que funciona e escala até um jogo jogável: const canvas = document.getElementById('snake');
const ctx = canvas.getContext('2d');
const GRID = 20;
const CELL = 20;
let snake = [{x: 10, y: 10}];
let food = {x: 15, y: 15};
let dx = 0;
let dy = 0;
let score = 0;
let lastTime = 0;
let accumulator = 0;
const TICK_RATE = 100; // ms entre movimentos
O detalhe crítico que todo mundo erra na primeira vez é o sistema de input. Se você simplesmente vincula a seta esquerda e depois imediatamente a seta cima no mesmo frame, a cobra vai tentar virar duas vezes antes de se mover e pode colidir consigo mesma instantaneamente. A solução é manter uma variável nextDirection que só é aplicada uma vez por tick de movimento: let nextDx = 0;
let nextDy = 0;
function applyInput() {
if (nextDx !== 0 || nextDy !== 0) {
dx = nextDx;
dy = nextDy;
}
}
No event listener de keydown, você só atualiza nextDirection e nunca dx/dy diretamente. Também precisa bloquear reversão, ou seja, se a cobra está indo para a direita, a tecla esquerda deve ser ignorada.
Logística de crescimento e colisão
Quando a cobra come, você adiciona um novo segmento na posição que ela estava no tick anterior. A maneira mais limpa é calcular a nova cabeça primeiro, verificar colisão, e se não tiver colisão, empurrar a nova cabeça no array da cobra. Se comeu a maçã, não remove a cauda. Se não comeu, remove o último elemento. Isso evita o bug clássico de um frame em que a cobra parece crescer mas na verdade o segmento extra some no mesmo frame. Colisão com paredes: depende do que você quer. Modo clássico com paredes matadoras é mais difícil e mais previsível. Modo com wrap-around (a cobra sai de um lado e aparece no outro) é mais indulgente. Eu prefiro paredes matadoras para jogos de pontuação, porque dá um limite claro de risco.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Colisão com o próprio corpo: verifica se a nova posição da cabeça coincide com qualquer segmento do array snake exceto a cauda atual (porque a cauda vai se mover). Comparação direta de coordenadas x e y é suficiente, não precisa de distância euclidiana.
Problema real que encontrei
Num projeto meu, a maçã aparecia em cima do corpo da cobra durante a geração. A verificação ingênua de randomização simplesmente não considerava as posições ocupadas. A correção foi fazer um loop de retry: gerar posições aleatórias até encontrar uma que não esteja no array snake, mas também limitar a 200 tentativas para não travar em casos extremos de tabuleiro quase cheio. Em jogo da cobrina, isso raramente passa de 3 ou 4 iterações na prática.
Speed up progressivo
Para manter o jogo interessante, reduza o TICK_RATE gradualmente conforme a pontuação sobe. Comece com 100ms e diminua 2ms a cada 5 maçãs, com um piso de 50ms. Isso mantém o jogo jogável até pelo menos 30 pontos sem ficar insuportavelmente rápido.
Download e versão pronta
Se quiser testar uma implementação completa sem montar do zero, o código pode ser baixado como um arquivo HTML único, basta salvar e abrir no navegador. Não precisa de servidor, não precisa de build step, funciona offline. Procure por "snake game html5 github" que encontra várias versões em repositórios abertos.
Pegadinhas que ninguém conta
Primeiro: não use setInterval para o loop principal. requestAnimationFrame com accumulator é infinitamente superior porque se sincroniza com a taxa de atualização do monitor e não drena bateria em laptops. Segundo: o controle de input buffer que mostrei acima também é o que separa um jogo que parece travado de um que responde na hora. Teclas trocadas no mesmo frame são o maior motivo de frustração em implementações caseiras de jogo da cobrina. Terceiro: se for adicionar multiplayer, pense desde o início em como representar o estado. Cada jogador precisa de seu próprio snake array e seu próprio direction. O colisor entre cobras é apenas uma verificação se a cabeça de A está em qualquer posição do corpo de B e vice-versa. O problema é que essa versão com paredes matadoras é mais simples, mas em alguns cenários como torneios ou modos competitivos, o wrap-around pode ser preferível porque reduz a frustração de morte por colisão acidental com parede. A escolha depende do público. Se for para uso casual, paredes matadoras e velocidade progressiva funcionam bem. Se for para alguém que vai jogar todo dia, considere adicionar um modo fácil com velocidade fixa e paredes que teleportam.
O código completo com essas modificações fica em torno de 150 linhas. Mais do que isso já é overengineering para o que o jogo é. Menos do que isso e você vai enfrentar os bugs que descrevi.