Cobrinha Cobrinha - Cobrinha kuhli peixe: origem, características, reprodução, alimentação ...
Cobrinha kuhli peixe: origem, características, reprodução, alimentação ...

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.