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:
- Na inicialização, o primeiro frame define o timestamp base (
lastTime) - A cada frame seguinte, calcula-se o delta time (tempo entre frames em segundos):
(timestamp - lastTime) / 1000 - 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
- Chama
update(dt)para atualizar posições, colisões e lógica - Chama
draw()para renderizar tudo - 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:
- Screen shake — desloca a origem do canvas (ctx.translate) com offsets aleatórios
- Fundo — retângulo sólido que muda de cor com o tier
- Nebulosas — 3 gradientes radiais (createRadialGradient) com matizes que rodam por tier
- Estrelas — 180 pontos animados que rolam verticalmente
- Meteoros — polígonos irregulares com gradientes, brilho de calor e barras de HP
- Boosts — orbes dourados pulsantes com símbolo ★ ou ⬆
- Tiros — orbes luminosos com gradiente radial (núcleo branco → cor do tier)
- Mísseis — triângulos com aura e trilhas de 12 pontos
- Partículas — explosões, brilhos, debris
- Debris — fragmentos da nave com gravidade e trilhas
- Onda de choque — anel expandindo após morte
- Nave — nave detalhada com motor, chama, cockpit e asas
- Spread dots — indicadores visuais do nível de spread
- Crosshair — retículo ciano no cursor (desktop)
- 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:
| Evento | Partículas | Velocidade | Duração |
|---|---|---|---|
| Meteoro pequeno destruído | 14 | 50–160 px/s | 0.5–1.2s |
| Meteoro grande destruído | 30 | 80–280 px/s | 0.5–1.2s |
| Boost coletado | 14 | 50–160 px/s | 0.5–1.2s |
| Morte da nave (explosão inicial) | 60 | 30–310 px/s | 0.6–1.8s |
| Morte (explosões secundárias, ×3) | 18 cada | 40–160 px/s | 0.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:
| Estado | gameRunning | deathAnimPhase | O que acontece |
|---|---|---|---|
| Jogando | true | 0 | Toda a lógica roda normalmente |
| Animação de morte | false | 1 | Explosão 2,5s — estrelas e meteoros ainda flutuam |
| Game Over | false | 2 | Overlay com stats, input de nome |
| Leaderboard | false | 2 | Ranking 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:
- O componente é marcado como
'use client'(componente de cliente no Next.js) - O JSX renderiza toda a estrutura HTML estática: canvas, overlays, botões
- Um
useEffectcom array de dependências vazio roda uma vez na montagem - Um
useRefcom guard boolean previne inicialização dupla (React 18 StrictMode chama effects duas vezes em dev) - Dentro do
useEffect, uma função monolítica de 1.200+ linhas assume o controle viadocument.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-independent —
1 - e^(-k×dt)produz resultado visualmente idêntico em qualquer framerate - Pixel-perfect sizing —
Math.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.
