Desenvolvimento30 de setembro de 20267 min de leitura

Como fizemos OLYMPUS: 300 sem Canvas

Um arcade de ação inteiro com elementos HTML, CSS e um laço de JavaScript. O que funcionou e onde tivemos cuidado.

Zeus no pico do Olimpo cercado por titãs

Quando começamos o OLYMPUS: 300, a escolha óbvia era desenhar tudo em um <canvas>. Decidimos fazer o contrário: cada titã, cada raio e cada número na tela é um elemento HTML comum. Este texto explica o porquê e mostra as partes principais.

Por que DOM

O jogo nunca tem mais do que algumas dezenas de objetos na tela. Nessa escala, o navegador move elementos com folga, desde que você anime apenas transform e opacity. Em troca, ganhamos de graça texto nítido em qualquer densidade de tela, layout responsivo com CSS e botões que são botões de verdade, acessíveis e fáceis de tocar.

O laço

O estado do jogo vive em um único objeto. Um requestAnimationFrame avança a simulação e escreve a posição de cada titã direto no estilo do elemento:

function tick(now) {
  requestAnimationFrame(tick);
  const dt = Math.min(0.05, (now - last) / 1000);
  last = now;
  for (const e of state.ents) {
    const d = Math.hypot(e.x, e.y);
    const step = (e.speed * dt) / d;
    e.x -= e.x * step;
    e.y -= e.y * step;
    e.el.style.transform =
      `translate3d(${e.x}px, ${e.y}px, 0)`;
  }
}

As coordenadas têm origem no centro da arena, onde Zeus fica. Assim, andar em direção ao Olimpo é só encolher o vetor de posição. O limite de 50 ms no dt evita saltos quando a aba volta do segundo plano.

Um toque, um alvo

Em vez de registrar um evento em cada titã, a arena inteira escuta pointerdown e procura o alvo mais próximo do dedo, com uma margem generosa. Isso resolve dois problemas do celular de uma vez: alvos pequenos ficam fáceis de acertar e um único manipulador cuida de toque, caneta e mouse. Segurar o dedo por quase meio segundo transforma o golpe em raio carregado.

Personagens em SVG

Zeus e os titãs são desenhos vetoriais comuns, carregados em elementos <img>. O passo dos titãs e o arremesso de Zeus são animações CSS curtas. Quando a vida cai, o JavaScript só escreve um atributo data-ruin de 0 a 4, e o CSS deixa o céu cada vez mais vermelho.

[data-ruin="3"] .o3-sky::after { opacity: 0.75; }

O raio é um retângulo girado

Cada raio é um <div> com recorte em zigue-zague. O código calcula a distância e o ângulo entre a mão de Zeus e o alvo, define a largura e gira o elemento. Um quarto de segundo depois, ele some.

Efeitos descartáveis

Raios, clarões e números de dano são elementos criados na hora, com uma animação CSS curta. Quando a animação termina, o evento animationend remove o elemento. Não existe lista de partículas para gerenciar.

Onde tivemos cuidado

  • Nada de animar layout. Barras de vida usam scaleX, nunca width.
  • Rolagem desligada na arena. touch-action: none impede que um deslize mova a página no meio da luta.
  • Altura estável. A área de jogo usa 100dvh, que acompanha a barra de endereço do navegador móvel.
  • Pausa automática. O evento visibilitychange pausa a partida quando você troca de aba.

Vale a pena?

Para um jogo com centenas de objetos, não: aí um motor com renderização em WebGL é o caminho, e comparamos as opções em Phaser ou DOM puro?. Para um arcade de arena com interface forte, o DOM entregou um jogo leve, nítido e sem dependências.