Criar um jogo 2D em JavaScript é mais acessível do que parece — e você não precisa de nenhuma engine como Unity, Godot ou Phaser para começar. Com o elemento <canvas> do HTML5 e JavaScript puro, é possível construir jogos completos com renderização, colisão, partículas, input e game loop. O Navistron, por exemplo, é 100% vanilla JavaScript — zero bibliotecas externas, apenas Canvas 2D e o DOM.

Neste guia, vamos percorrer todos os pilares de um jogo 2D funcional, usando padrões reais extraídos do código-fonte do Navistron como referência prática.

1. Configurando o Canvas 2D

O canvas é o elemento HTML que funciona como a "tela" do seu jogo. Toda a renderização acontece nele. A configuração básica envolve criar o elemento, obter o contexto 2D e definir as dimensões internas.

O Navistron usa um canvas de 540×720 pixels (proporção 3:4, orientação retrato) com escala dinâmica via CSS. A resolução interna é fixa — sempre 540×720 — mas o canvas é redimensionado visualmente para caber na tela do dispositivo, mantendo a proporção. Isso garante que a lógica do jogo funcione com coordenadas consistentes independentemente do tamanho da tela.

A fórmula de escala calcula se a tela disponível é mais estreita ou mais alta que a proporção 3:4. Se for mais estreita, usa a largura como referência; se for mais alta, usa a altura. O resultado é aplicado como canvas.style.width e canvas.style.height, sem alterar a resolução interna do canvas de 540×720.

Dica prática: sempre use Math.floor() nas dimensões CSS do canvas para evitar sub-pixels que causam borramento (blurriness) nos gráficos.

2. O Game Loop — O Coração do Jogo

Todo jogo precisa de um loop que executa continuamente: atualizar o estado do jogo e renderizar a tela. Em JavaScript, usamos requestAnimationFrame() para sincronizar com a taxa de atualização do monitor (geralmente 60fps).

O padrão usado no Navistron é elegante em sua simplicidade:

  1. Na inicialização, o primeiro frame define o timestamp base (lastTime)
  2. A cada frame seguinte, calcula-se o delta time (tempo entre frames em segundos): (timestamp - lastTime) / 1000
  3. O delta time é limitado a 0.05s (equivalente a 20fps mínimo) para evitar saltos de física quando o jogador troca de aba e volta
  4. Chama update(dt) para atualizar posições, colisões e lógica
  5. Chama draw() para renderizar tudo
  6. Agenda o próximo frame com requestAnimationFrame(loop)

Esse modelo é chamado de variable timestep — cada frame recebe o tempo real que se passou, garantindo que o jogo rode na mesma velocidade em qualquer dispositivo, seja 30fps ou 144fps.

3. Input: Mouse e Touch

Capturar a posição do jogador é essencial. O Navistron suporta mouse (desktop) e touch (celular) com o mesmo sistema de coordenadas.

O truque é converter coordenadas do cliente para coordenadas do canvas. Como o canvas pode estar escalado via CSS, a posição do clique/toque no viewport precisa ser dividida pelo fator de escala:

x = (clientX - rect.left) / SCALE e y = (clientY - rect.top) / SCALE

No touch, o Navistron aplica um offset vertical de −140 pixels para que o dedo do jogador não cubra a nave. A nave fica 140 pixels acima do ponto de toque — uma solução simples que melhora dramaticamente a jogabilidade mobile.

O movimento da nave usa interpolação exponencial (exponential lerp):

fator = 1 - e^(-12 × dt)

A nave move 12 unidades por segundo em direção ao alvo, com desaceleração natural. Essa fórmula é independente de framerate — produz o mesmo resultado visual a 30fps ou 144fps. É um padrão superior ao lerp linear (x += (alvo - x) * 0.1) que depende do framerate.

4. Renderização em Camadas

A ordem de desenho (draw order) define o que aparece na frente e o que fica atrás. No Canvas 2D, não existe z-index — o último item desenhado fica por cima.

A pipeline de renderização do Navistron desenha 15 camadas em sequência:

  1. Screen shake — desloca a origem do canvas (ctx.translate) com offsets aleatórios
  2. Fundo — retângulo sólido que muda de cor com o tier
  3. Nebulosas — 3 gradientes radiais (createRadialGradient) com matizes que rodam por tier
  4. Estrelas — 180 pontos animados que rolam verticalmente
  5. Meteoros — polígonos irregulares com gradientes, brilho de calor e barras de HP
  6. Boosts — orbes dourados pulsantes com símbolo ★ ou ⬆
  7. Tiros — orbes luminosos com gradiente radial (núcleo branco → cor do tier)
  8. Mísseis — triângulos com aura e trilhas de 12 pontos
  9. Partículas — explosões, brilhos, debris
  10. Debris — fragmentos da nave com gravidade e trilhas
  11. Onda de choque — anel expandindo após morte
  12. Nave — nave detalhada com motor, chama, cockpit e asas
  13. Spread dots — indicadores visuais do nível de spread
  14. Crosshair — retículo ciano no cursor (desktop)
  15. HUD de dificuldade — texto com multiplicador no canto inferior

Recursos do Canvas 2D usados: save()/restore(), translate(), rotate(), createRadialGradient(), createLinearGradient(), shadowColor, shadowBlur, globalAlpha, arc(), ellipse(), beginPath/closePath/moveTo/lineTo.

5. Detecção de Colisão Circular

Colisão é o que faz tiros acertarem meteoros e meteoros destruírem a nave. Para jogos 2D com entidades arredondadas, a colisão circular é a mais eficiente e natural.

A fórmula clássica verifica se a distância entre dois centros é menor que a soma dos raios. Mas calcular distância exige Math.sqrt(), que é caro. A otimização é comparar as distâncias ao quadrado:

dx² + dy² ≤ (rA + rB)²

Essa é exatamente a fórmula que o Navistron usa — sem raiz quadrada, apenas multiplicações e somas. Em um jogo com dezenas de tiros e meteoros sendo testados a cada frame, essa otimização faz diferença.

Um detalhe importante: os meteoros têm formas irregulares (polígonos rochosos), então o raio de colisão é reduzido em relação ao raio visual: 82% para tiros/mísseis e 72% para a nave. Isso evita que o jogador sinta que morreu injustamente (o hitbox visual é maior que o real).

6. Gerenciamento de Entidades

Todo jogo 2D precisa gerenciar listas de objetos: tiros, inimigos, partículas, power-ups. O Navistron usa arrays simples para todas as entidades:

  • bullets[], meteors[], missiles[], boosts[], particles[], debris[], stars[]

Novas entidades são criadas com push() (objetos literais, sem classes). Entidades mortas ou fora da tela são removidas com filter():

bullets = bullets.filter(b => b.life > 0 && b.y > -40)

Esse padrão é simples e performático para contagens de entidades na casa das centenas. Para jogos com milhares de entidades, considere object pooling (reutilizar objetos em vez de criar/destruir) — mas para jogos arcade como este, push/filter funciona perfeitamente.

7. Sistema de Partículas

Partículas são o que transformam um jogo funcional em um jogo bonito. Explosões satisfatórias, brilhos, trilhas — tudo é partícula.

Cada partícula no Navistron é um objeto com: posição (x, y), velocidade (vx, vy), raio, vida atual, vida máxima e cor. A cada frame, a posição é atualizada pela velocidade, e a vida diminui. O raio visual encolhe proporcionalmente à vida restante, criando o efeito de "desvanecimento".

Eventos que geram partículas:

EventoPartículasVelocidadeDuração
Meteoro pequeno destruído1450–160 px/s0.5–1.2s
Meteoro grande destruído3080–280 px/s0.5–1.2s
Boost coletado1450–160 px/s0.5–1.2s
Morte da nave (explosão inicial)6030–310 px/s0.6–1.8s
Morte (explosões secundárias, ×3)18 cada40–160 px/s0.4–1.0s

O brilho é criado com ctx.shadowColor e ctx.shadowBlur = 8, que adiciona um halo suave sem complexidade adicional de código.

8. Física Básica para Jogos 2D

Física em jogos 2D é mais simples do que parece. O padrão fundamental é: posição += velocidade × dt. Multiplicar pelo delta time garante que o movimento é proporcional ao tempo real, não ao framerate.

Exemplos do Navistron:

  • Tiros — velocidade fixa de 520 px/s, direção calculada pelo ângulo do spread
  • Meteoros — velocidade vertical (60–200 px/s base, escalando até ×3.8 com a dificuldade) + drift horizontal aleatório
  • Mísseis teleguiados — velocidade constante de 320 px/s com sistema de steering: calculam o ângulo desejado até o alvo, limitam a rotação a 5.5 rad/s, e ajustam a direção suavemente. Se o alvo é destruído, re-adquirem o meteoro mais próximo
  • Debris — fragmentos da nave que sofrem gravidade: vy += gravidade × dt (60–100 px/s²), criando arcos parabólicos naturais

Rotação de meteoros é puramente visual: ângulo += velocidade_rotação × dt, com velocidades aleatórias entre −0.5 e +0.5 base.

9. Máquina de Estados do Jogo

Todo jogo tem estados: menu, jogando, morte, game over. A forma mais simples de gerenciar isso é com variáveis booleanas e flags.

O Navistron usa duas variáveis para controlar 4 estados:

EstadogameRunningdeathAnimPhaseO que acontece
Jogandotrue0Toda a lógica roda normalmente
Animação de mortefalse1Explosão 2,5s — estrelas e meteoros ainda flutuam
Game Overfalse2Overlay com stats, input de nome
Leaderboardfalse2Ranking e botão "jogar novamente"

Quando gameRunning é false, o update() faz um early return — só atualiza estrelas, meteoros flutuando, debris e o timer de animação. Isso evita que tiros sejam disparados ou colisões processadas durante a morte.

10. HUD e Interface no Canvas

Existem duas abordagens para interfaces em jogos Canvas: renderizar texto/ícones no próprio canvas, ou usar elementos HTML sobrepostos. O Navistron usa ambos:

  • No canvas: multiplicador de dificuldade ("DIFFICULTY ×X.X" no canto inferior direito, fonte 11px Courier New), crosshair do cursor
  • No DOM (HTML): score, badge de tier (com cor e brilho dinâmicos), barra de boost (5 pips), nível de spread, overlay de game over, leaderboard

A vantagem dos elementos DOM é o acesso a CSS para estilização (bordas, sombras, transições), tipografia avançada e input do usuário (campo de nome). A vantagem do canvas é posicionar elementos relativos ao mundo do jogo (como o crosshair que segue o cursor).

11. Integrando com React e Next.js

Se você está usando React ou Next.js, a integração com Canvas pode parecer conflitante — React quer controlar o DOM, mas o jogo precisa de acesso direto. O padrão do Navistron resolve isso elegantemente:

  1. O componente é marcado como 'use client' (componente de cliente no Next.js)
  2. O JSX renderiza toda a estrutura HTML estática: canvas, overlays, botões
  3. Um useEffect com array de dependências vazio roda uma vez na montagem
  4. Um useRef com guard boolean previne inicialização dupla (React 18 StrictMode chama effects duas vezes em dev)
  5. Dentro do useEffect, uma função monolítica de 1.200+ linhas assume o controle via document.getElementById()

Resultado: React cuida da renderização inicial e do roteamento. O jogo em si é JavaScript vanilla puro — sem useState, sem re-renders, sem React state. É o melhor de dois mundos: infraestrutura moderna (SSR, roteamento, SEO) com performance máxima de canvas.

12. Otimizações de Performance

Para manter 60fps suaves, o Navistron aplica várias otimizações que você deve considerar em qualquer jogo 2D:

  • Colisão sem Math.sqrt() — comparar distâncias ao quadrado é matematicamente equivalente e evita a operação mais cara
  • Delta time clampado — limitar a 0.05s evita que entidades "teleportem" quando o jogador volta de outra aba
  • Escalamento via CSS — renderizar em resolução fixa (540×720) e escalar com CSS é muito mais eficiente que renderizar na resolução nativa do dispositivo
  • Cullling offscreen — remover entidades que saíram dos limites da tela impede acúmulo de objetos invisíveis
  • Lifetime em tudo — partículas, debris, tiros e mísseis têm timers de vida. Sem isso, arrays cresceriam indefinidamente
  • Early return condicional — quando o jogo não está rodando, pular a maior parte do update economiza processamento
  • Lerp exponencial frame-independent1 - e^(-k×dt) produz resultado visualmente idêntico em qualquer framerate
  • Pixel-perfect sizingMath.floor() nas dimensões evita renderização sub-pixel

FAQ — Perguntas Frequentes sobre Criar Jogos 2D em JavaScript

Preciso de uma engine como Unity ou Godot para criar jogos 2D?

Não, para jogos 2D no navegador. JavaScript puro com Canvas 2D é suficiente — o Navistron é um jogo completo com 7 tiers, mísseis teleguiados, sistema de partículas, ranking global, e mais de 1.300 linhas de código, tudo sem nenhuma biblioteca externa. Engines são úteis para projetos 3D ou equipes grandes, mas para jogos arcade 2D, vanilla JS oferece controle total e zero overhead.

Canvas 2D ou WebGL para jogos 2D?

Canvas 2D para a maioria dos jogos 2D. É mais simples, tem boa performance para centenas de entidades, e funciona em todos os navegadores. WebGL vale a pena quando você precisa de milhares de sprites simultâneos, shaders customizados, ou efeitos de iluminação complexos.

Como faço o jogo rodar na mesma velocidade em qualquer dispositivo?

Use delta time: meça o tempo entre frames e multiplique todas as velocidades por esse valor. Se um frame demora 16ms (60fps), a nave move X×0.016. Se demora 33ms (30fps), move X×0.033 — o resultado visual é idêntico. Leia sobre como funciona o motor do jogo para mais detalhes.

Como funciona colisão em jogos 2D sem engine?

A forma mais comum é colisão circular: se a distância entre dois centros é menor que a soma dos raios, há colisão. A otimização principal é comparar distâncias ao quadrado (sem calcular raiz quadrada). AABB (retângulos alinhados) é outra opção para entidades retangulares.

O Navistron usa alguma biblioteca ou framework de jogos?

Zero. É 100% JavaScript vanilla — sem Phaser, PixiJS, Three.js, ou qualquer engine. As únicas importações são useEffect e useRef do React (para a integração com Next.js) e o arquivo CSS do jogo. Toda a lógica de renderização, física, colisão, partículas e input é escrita do zero com a API nativa do Canvas 2D.