O Navistron é um jogo de nave construído inteiramente com JavaScript vanilla e a API Canvas 2D do HTML5 — sem nenhuma biblioteca de jogos externa como Phaser, PixiJS ou Three.js. Todo o código de física, colisão, renderização, partículas e animação reside em uma única função initGame() com cerca de 1.200 linhas, executada uma única vez dentro de um useEffect do React. Neste artigo, vamos desmontar cada engrenagem deste motor para entender como um jogo de arcade moderno funciona diretamente no navegador.

Arquitetura Geral: Closures em Vez de Classes

A arquitetura do Navistron segue um padrão pouco convencional mas extremamente eficiente: toda a lógica do jogo vive dentro de closures da função initGame(). Não há classes, módulos ou importações além do React. Todas as variáveis de estado — score, powerTier, gameRunning, ship, arrays de entidades (bullets[], meteors[], particles[], missiles[], debris[], boosts[], stars[]) — são declaradas como let no escopo da closure e compartilhadas por todas as funções internas.

O componente React GamePage é um componente funcional 'use client' que renderiza todo o DOM (canvas, overlays, HUD) em JSX e usa useEffect com um useRef guard para chamar initGame() exatamente uma vez. Dentro de initGame(), temos: adaptadores de API para banco de dados (DB, AnonymousDB, SponsorsDB), setup do canvas e input, constantes de jogo (TIER_DEFS[], TIER_DIFF[]), funções de criação/atualização/desenho para cada tipo de entidade, e o ciclo de vida completo do jogo (startGame(), beginDeathSequence(), endGame(), update(dt), draw(), loop(ts)).

Canvas 2D: Resolução Interna e Escalonamento CSS

O canvas do Navistron opera com uma resolução interna fixa de 540×720 pixels (proporção 3:4, formato retrato ideal para mobile). O buffer do canvas nunca muda — o que muda é o tamanho CSS. A função resizeCanvas() ajusta canvas.style.width e canvas.style.height para caber no container #canvas-wrap, mantendo a proporção 3:4. Um fator SCALE é calculado como a razão entre a largura CSS exibida e os 540px internos, usado exclusivamente para converter coordenadas do ponteiro via clientToCanvas().

Essa abordagem de resolução fixa + escalonamento CSS é significativamente mais performática do que redimensionar o buffer a cada resize, pois todas as operações de desenho sempre acontecem no mesmo grid de 540×720, independente do tamanho da tela. O devicePixelRatio não é utilizado — uma escolha deliberada para manter a estética pixel-art e maximizar performance em dispositivos móveis.

O Game Loop: requestAnimationFrame e Delta Time

O coração do motor é o game loop, implementado com requestAnimationFrame. A sequência de inicialização é: startGame() reseta todo o estado, depois um primeiro requestAnimationFrame captura o timestamp inicial em lastTime, e um segundo requestAnimationFrame inicia o loop principal loop(ts).

A cada frame, o delta time é calculado como dt = (ts - lastTime) / 1000, convertido para segundos. Um cap de 50ms (Math.min(..., 0.05)) garante que, mesmo durante picos de lag ou mudanças de aba, a simulação nunca avança mais de 50ms por frame — equivalente a um piso de 20 FPS. Sem esse cap, uma pausa de 2 segundos faria a nave teleportar e meteoros atravessarem a tela inteira em um único frame.

A cada iteração, o loop executa update(dt) para avançar a simulação, depois draw() para renderizar, e finalmente agenda o próximo frame. O loop é incondicional — ele nunca para. Mesmo quando gameRunning === false, o update continua movendo meteoros, debris e processando a animação de morte, apenas pulando a lógica da nave e tiros.

Pipeline de Renderização: 16 Camadas de Desenho

A função draw() do Navistron implementa um pipeline de renderização com 16 camadas desenhadas em ordem precisa (de trás para frente):

  1. Screen shakectx.save() + ctx.translate(sx, sy) quando o shake está ativo
  2. Fundo sólido — cor dinâmica rgb(tr, 1, max(14-powerTier*2, 1)), escurecendo a cada tier
  3. Nebulosas — 3 gradientes radiais (createRadialGradient) com matiz que muda conforme o tier
  4. Estrelas — campo estelar de 180 estrelas com parallax
  5. Meteoros — polígonos com gradientes radiais para aura e superfície
  6. Boosts — itens coletáveis com efeito de brilho
  7. Tiros — projéteis com gradiente radial
  8. Mísseis — mísseis guiados com efeito de brilho
  9. Partículas — centenas de partículas de explosão com alpha e tamanho decrescentes
  10. Debris — fragmentos da nave com trilha e gravidade
  11. Shockwave — onda de choque circular durante a morte
  12. Nave — sprite vetorial com gradientes lineares e radiais
  13. Dots de spread — indicadores abaixo da nave quando spread > 1
  14. Cursor crosshair — apenas em dispositivos não-touch com ponteiro ativo
  15. HUD de dificuldade — texto "DIFFICULTY ×N.N" no canto inferior direito
  16. Restauração do shakectx.restore() desempilha a transformação

Cada entidade (nave, tiro, meteoro, boost, míssil, partícula, debris) usa seu próprio par ctx.save()/ctx.restore(), tornando cada função de desenho completamente autocontida. Os gradientes são usados extensivamente: createRadialGradient para nebulosas, brilho do motor, cockpit, tiros, boosts, aura dos meteoros, brilho dos mísseis e shockwave; createLinearGradient para o casco e chama do motor da nave.

Sistema de Partículas e Explosões

O sistema de partículas do Navistron é responsável por criar os efeitos visuais espetaculares de explosões. A função spawnExplosion(x, y, big, hueOverride) cria dois tipos de explosões, diferenciados pelo tamanho do meteoro destruído:

  • Explosão grande (meteoro grande): 30 partículas, velocidade de 80–280 px/s, raio de 2–7px, matiz alaranjado (20–60°), vida máxima de 0.5–1.2 segundos
  • Explosão pequena (meteoro pequeno): 14 partículas, velocidade de 50–160 px/s, raio de 1.5–4.5px, matiz azulado (190–250°), mesma distribuição de vida

Cada partícula é um objeto simples com { x, y, vx, vy, r, life, maxLife, color }. A cada frame, a posição é atualizada pela velocidade multiplicada por dt, e a vida é decrementada por dt. Partículas com life <= 0 são removidas via filter(). Na renderização, o alpha é calculado como min(life/maxLife, 1) e o raio visual diminui proporcionalmente, criando o efeito de desvanecimento natural. Um shadowBlur=8 adiciona o brilho característico.

Com explosões frequentes a partir do Tier III, é comum ter 200+ partículas simultâneas na tela, todas processadas sem queda perceptível de framerate graças à simplicidade do cálculo por partícula.

Campo Estelar com Parallax Dinâmico

O fundo do Navistron apresenta um campo de 180 estrelas com parallax que se intensifica conforme o tier. Cada estrela tem posição aleatória, raio entre 0.2–1.7px, velocidade base entre 0.1–0.7, e um valor de brilho aleatório que determina sua cor e opacidade.

O multiplicador de velocidade é 1 + powerTier × 0.45, então no Tier I as estrelas se movem a velocidade normal, mas no Tier VII elas se movem 4.15× mais rápido, criando uma sensação visceral de velocidade crescente. As estrelas rolam para baixo e, ao sair da tela, reaparecem no topo com nova posição X aleatória. A cor é um branco-azulado (rgba(180+br×75, 200+br×55, 255, 0.3+br×0.7)), com estrelas mais brilhantes sendo mais opacas e mais brancas.

Input Handling: Mouse e Touch Unificados

O Navistron suporta tanto mouse quanto touch com um sistema unificado via o objeto pointer = { x, y, active }. A função clientToCanvas(cx, cy) converte coordenadas de tela para coordenadas do canvas usando o fator SCALE.

Para mouse, os eventos mousemove e mouseleave atualizam o ponteiro. Para touch, um offset vertical de -140px (TOUCH_OFFSET_Y) é aplicado, posicionando a nave acima do dedo para que o jogador possa ver a ação. Todos os eventos touch usam { passive: false } com e.preventDefault() para evitar scroll acidental.

A nave não segue o ponteiro instantaneamente — ela usa um lerp exponencial (1 - Math.exp(-12 × dt)) que cria um movimento suave com aceleração e desaceleração naturais. O fator de suavização 12 garante que a nave responda rapidamente sem parecer robótica.

Death Sequence: Animação Cinematográfica de Morte

A sequência de morte do Navistron é uma das implementações mais elaboradas do engine, com múltiplas fases coordenadas:

  1. Fase inicial: gameRunning = false, nave removida, deathAnimPhase = 1, timer de 2.5 segundos. Screen shake de magnitude 20 por 1.5s. 60 partículas disparam em explosão primária com velocidades de 30–310 px/s, raios de 2–8px e vida de 0.6–1.8s em tons de branco e azul.
  2. Debris: 12 fragmentos são criados com velocidade inicial para cima (bias de -20), gravidade de 60–100 px/s² puxando para baixo, rotação de até 6 rad/s, e uma trilha de 8 posições anteriores. As cores variam entre azuis, brancos e laranjas.
  3. Explosões secundárias: 3 setTimeout escalonados (300ms, 700ms, 1100ms) disparam clusters adicionais de 54 partículas cada, com screen shakes decrescentes (magnitude 10, depois 8).
  4. Shockwave: uma onda de choque circular expande de 0 a 180px de raio em 0.8 segundos, com dois anéis concêntricos e shadowBlur=20 para efeito de brilho.
  5. Flash branco: nos primeiros 0.15 segundos, um overlay branco com alpha decrescente de 0.5 a 0 cobre toda a tela.

Quando o timer de 2.5s expira, endGame() é chamado com um delay de 700ms antes de exibir o overlay de game over — tempo suficiente para os últimos debris desaparecerem.

Screen Shake e Tier Flash

O screen shake funciona com ctx.translate(sx, sy) aplicado antes de toda a renderização. Os deslocamentos sx e sy são aleatórios a cada frame, com magnitude que decai linearmente ao longo do tempo. A magnitude é calculada como shakeMag × (shakeTimer / 0.7), normalizando o decaimento para 0.7 segundos.

O shake é ativado em dois momentos: tier up (magnitude 12, duração 0.55s) e morte da nave (magnitude 20, duração 1.5s, com duas sacudidas adicionais de magnitude 10 e 8 durante as explosões secundárias).

O tier flash é um efeito DOM separado: um <div> overlay com a cor do novo tier, que aparece a 35% de opacidade e faz fade linear para 0 em 0.5 segundos. Ele é gerenciado via tierFlashTimer no loop de update, atualizando style.opacity a cada frame.

HUD: Híbrido DOM + Canvas

O Navistron usa uma abordagem híbrida para o HUD, combinando elementos DOM e desenhos no canvas. Os elementos DOM incluem: pontuação (#score), badge de tier (#tier-badge) com cor e sombra dinâmicas, pips de boost (#boost-pips) com 5 indicadores preenchidos conforme boosts coletados, e nível de spread (#spread). Esses elementos são atualizados pela função updateHUD() chamada a cada frame durante gameplay.

No canvas, são desenhados: o texto de dificuldade ("DIFFICULTY ×N.N") em Courier New 11px com 28% de opacidade no canto inferior direito, os dots de spread coloridos abaixo da nave quando o spread é maior que 1, e o crosshair customizado em dispositivos com mouse.

FAQ — Perguntas Frequentes sobre o Motor do Navistron

O Navistron usa WebGL ou Canvas 2D?

O Navistron usa exclusivamente Canvas 2D. Não há WebGL, Three.js, PixiJS ou qualquer biblioteca gráfica externa. Toda a renderização — gradientes, sombras, partículas, polígonos — é feita com a API nativa do Canvas 2D, garantindo compatibilidade universal com navegadores modernos.

Por que o delta time é limitado a 50ms?

O cap de 50ms (Math.min(dt, 0.05)) previne "saltos" na simulação. Sem ele, se o jogador trocar de aba por 3 segundos e voltar, o dt seria 3.0 — a nave teleportaria, tiros atravessariam a tela, e meteoros passariam pelo jogador sem colisão ser detectada. Com o cap, o pior cenário é equivalente a jogar a 20 FPS.

Quantas entidades o engine suporta simultaneamente?

Não há limite rígido. Em momentos de ação intensa no Tier VII, é comum ter 15+ meteoros, 10+ tiros, 3+ mísseis, 200+ partículas, debris com trilhas, 180 estrelas e 3 gradientes de nebulosa renderizados simultaneamente — tudo a 60 FPS na maioria dos dispositivos. A colisão é brute-force O(tiros × meteoros), mas com números pequenos isso não é gargalo.

Como funciona o input em dispositivos touch?

O Navistron detecta automaticamente se o dispositivo usa touch ou mouse. Em touch, aplica um offset vertical de -140px para que a nave fique acima do dedo do jogador, permitindo visibilidade total da ação. O lerp exponencial com fator 12 faz a nave seguir o dedo com suavidade natural, sem parecer "grudada" ao toque.

O jogo usa alguma biblioteca ou framework de jogos?

Não. O Navistron é 100% JavaScript vanilla para toda a lógica de jogo. O único framework usado é Next.js / React para a estrutura da página e SSR/SSG — o jogo em si é puramente imperativo, rodando dentro de um useEffect. Isso resulta em zero dependências de runtime para o engine, maximizando performance e eliminando overhead de abstrações.