Como fazer um jogo cobrinha cobrinha do zero
A cobra clássica que todo mundo conhece. Se você quer construir uma versão sua, o processo é mais direto do que parece. Eu peguei esse projeto no início de uma semana livre e levei umas três horas pra ter algo jogável no navegador. O desafio real não é fazer a cobra se mover, é lidar com as bordas, colisões e manter a resposta rápida.
cobrinha cobrinha — o básico que funciona
O jogo precisa de três coisas: um canvas pra desenhar, um loop que atualize a tela em intervalos fixos, e uma lista de coordenadas que representa o corpo da cobra. Comece com um array de pares x,y. A cabeça é o primeiro elemento. Quando a cobra anda, você adiciona uma nova posição na frente baseado na direção atual e remove o último segmento. Simples. A movimentação funciona assim. Você lê as teclas, atualiza a direção, e a cada tick recalcula onde a cabeça vai parar. Se cair na comida, a cauda não some. Se não cair, ela some. Aí é só checar colisão: a cabeça bateu nela mesma ou na parede? Se sim, jogo encerra.
Ia usar setInterval mas migrei pra requestAnimationFrame com controle de delta time. setInterval tem um problema chato: ele não respeita a taxa de refresh do monitor. Você pode acabar com quadros pulados ou animação travando em telas 120Hz. Com requestAnimationFrame você sincroniza com o display e controla a velocidade da cobra manualmente com um timer acumulador.
Detalhes que ninguém conta
O primeiro problema que eu encontrei foi a cobra entrando em si mesma instantaneamente. Se você aperta para cima e imediatamente para baixo num mesmo frame, a direção inverte e a cabeça colide com o segundo segmento. A solução é simples mas fácil de esquecer: bloqueie reversão. Antes de aplicar uma nova direção, verifique se ela é oposta à atual. Se for, ignore. Outra coisa: o tamanho do grid. Eu começo sempre com células de 20 pixels. Canvas de 400x400 dá 20x20 células. É um tamanho razoável. Menor que isso fica apertado demais e maior que isso o jogo fica lento visualmente sem ganho real. A velocidade inicial eu deixei em 150ms por tick. Vai diminuindo conforme a cobra cresce, mas nunca abaixo de 60ms. Ficar mais rápido que isso é praticamente impossível de jogar e o cérebro humano já não consegue reagir direito em velocidades acima de 80ms.
👉 Clique no botão abaixo para saber mais sobre o assunto!
A comida também merece atenção. Se você gerar a posição aleatoriamente sem verificar se ela caiu em cima do corpo da cobra, o jogo fica bugado. A cobra pode comer uma fruta que está dentro dela mesma. Meu workaround foi criar uma função que gera uma posição e faz um loop até encontrar um espaço vazio. Se o grid estiver quase cheio, isso pode demorar, mas num jogo normal a cobra começa pequena e a chance de ficar preso é mínima.
Código funcional
Aqui está uma versão completa que eu uso como base. Copie, cole num arquivo .html e abra no navegador. Não precisa de biblioteca nenhuma.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Cobrinha</title>
<style>
body { background: #1a1a1a; display: flex;
flex-direction: column; align-items: center;
justify-content: center; height: 100vh;
margin: 0; font-family: monospace; color: #eee; }
canvas { border: 2px solid #444; background: #0a0a0a; }
.info { margin-top: 10px; font-size: 14px; color: #888; }
</style>
</head>
<body>
<canvas id="c" width="400" height="400"></canvas>
<div class="info">Setas pra mover · R pra reiniciar</div>
<div class="info" id="score">Pontos: 0</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const GRID = 20;
const CELL = canvas.width / GRID;
let snake, dir, nextDir, food, score, lastTime, accumulator;
const TICK_START = 150;
const TICK_MIN = 60;
function init() {
snake = [{x:10,y:10},{x:9,y:10},{x:8,y:10}];
dir = {x:1,y:0};
nextDir = {x:1,y:0};
score = 0;
accumulator = 0;
lastTime = performance.now();
placeFood();
document.getElementById('score').textContent = 'Pontos: ' + score;
}
function placeFood() {
let pos;
do {
pos = {x: Math.floor(Math.random()*GRID),
y: Math.floor(Math.random()*GRID)};
} while (snake.some(s => s.x===pos.x && s.y===pos.y));
food = pos;
}
function tick() {
dir = nextDir;
const head = {x: snake[0].x + dir.x, y: snake[0].y + dir.y};
if (head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID) {
return gameOver();
}
for (let s of snake) {
if (s.x === head.x && s.y === head.y) return gameOver();
}
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
score++;
document.getElementById('score').textContent = 'Pontos: ' + score;
placeFood();
} else {
snake.pop();
}
}
function draw() {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = '#2d8a4e';
for (let s of snake) {
ctx.fillRect(s.x*CELL+1, s.y*CELL+1, CELL-2, CELL-2);
}
ctx.fillStyle = '#c0392b';
ctx.fillRect(food.x*CELL+1, food.y*CELL+1, CELL-2, CELL-2);
}
function gameOver() {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = '#eee';
ctx.font = '24px monospace';
ctx.textAlign = 'center';
ctx.fillText('Fim de jogo', canvas.width/2, canvas.height/2-10);
ctx.font = '16px monospace';
ctx.fillText('Pressione R', canvas.width/2, canvas.height/2+15);
}
function loop(now) {
const dt = now - lastTime;
lastTime = now;
accumulator += dt;
const speed = Math.max(TICK_MIN, TICK_START - score * 3);
while (accumulator >= speed) {
tick();
accumulator -= speed;
}
draw();
requestAnimationFrame(loop);
}
document.addEventListener('keydown', e => {
if (e.key === 'r' || e.key === 'R') { init(); return; }
const map = {ArrowUp:{x:0,y:-1},ArrowDown:{x:0,y:1},
ArrowLeft:{x:-1,y:0},ArrowRight:{x:1,y:0}};
const d = map[e.key];
if (!d) return;
if (d.x !== -dir.x || d.y !== -dir.y) nextDir = d;
e.preventDefault();
});
init();
requestAnimationFrame(loop);
</script>
</html>
O código inteiro cabe num único arquivo. Se quiser transformar num download, basta salvar como index.html e pointear o link. A lógica é autocontida.
O que esse jogo não faz bem
Não tem som. Não tem high score salvo. Não tem modo noturno/claro. Não tem versão mobile com touch. Pra tudo isso você precisaria adicionar camadas extras que fogem do escopo do básico. Se o seu objetivo é um jogo polido pra publicar, considere usar uma engine como Phaser ou Godot. O canvas puro é bom pra aprender, mas vira manutenção dolorosa rápido quando o jogo cresce. O outro problema é responsividade. O canvas de 400x400 fixo não se adapta a telas diferentes. Em um celular pequeno ele ocupa a tela toda e os botões de toque precisam ser implementados do zero. Eu resolvi isso numa iteração posterior adicionando um scale no CSS e detectando window.innerWidth, mas isso quebra a lógica de grid se você não ajustar a proporção manualmente.
Também vale notar que essa versão usa paredes sólidas. Algumas pessoas preferem o modo onde a cobra atravessa e aparece do outro lado. É fácil de implementar — basta mudar a verificação de colisão com bordas pra fazer wrap-around — mas muda completamente a estratégia do jogo. Com paredes, o espaço disponível diminui proporcionalmente ao crescimento da cobra. Sem paredes, o jogo tende a durar mais, mas fica mais previsível porque você pode usar as bordas como rota de fuga. Se a ideia é transformar isso num projeto portfólio, capriche na parte visual. O verde e vermelho padrão funcionam, mas adicionar gradientes, animação suave na comida pulsando, e um efeito de morte na cobra faz diferença. Não precisa ser complexo. Três cores com boa contrastura e pronto. O resto é polimento.