Fiz 1v1 online num arquivo HTML de 220KB. Cada bug de sincronização me ensinou física de rede na prática.
No meu último post, jogadores de doze cidades brasileiras apareceram no Vibe Tanks e o jogo ganhou português. Hoje ele ganhou 1v1 online: VS ONLINE acha um oponente aleatório em até 60 segundos (com CPU de reserva se ninguém aparecer) e DESAFIE UM AMIGO gera um link privado que joga quem abrir direto na sua partida.
O post é s0bre os cinco bugs que separaram "conectado" de "jogável". Todos vieram da mesma raiz: as duas máquinas decidindo, sozinhas, algo que precisava ser compartilhado.
- A janela virou a arena. O jogo faz canvas.width = innerWidth. Desktop 1920, celular 390. Coordenada (400,300) eram dois lugares diferentes. Os dois jogos rodavam perfeitos, em mundos de tamanhos distintos. Solução: arena lógica fixa de 1280x720 nos dois lados, escalada e com letterbox na hora de desenhar. E ela adota a proporção do aparelho mais restrito, o celular; o desktop ganha barras laterais. Justiça vence estética.
- Um isMobile dentro da geração com seed. O mapa vinha de um seed compartilhado, então devia ser idêntico. Mas o número de obstáculos era isMobile ? 7+rnd(4) : 8+rnd(6). Contagens diferentes = número diferente de chamadas ao RNG = o fluxo de aleatórios saía de fase, e tudo depois divergia. O seed estava certo e inútil. Regra que ficou: nenhum galho dependente de aparelho dentro de código com seed.
- Determinismo entre aparelhos é fantasia. Um celular com throttle e um desktop não rodam o mesmo número de passos de simulação. Lockstep acumula erro e nunca recupera (a lição do Age of Empires). Então o host é autoridade do mundo; o convidado não simula em paralelo, ele interpola snapshots com timestamp. Cada snapshot carrega x/y/vx/vy e rotação/velocidade-angular, porque qualquer coisa que integra no tempo precisa estar no pacote ou diverge pra sempre. Esqueci rot na primeira versão e os obstáculos giravam desalinhados nas duas telas.
- O convidado precisa deslizar, não congelar. Host-autoritativo não quer dizer que o convidado para entre snapshots. A 200ms de intervalo isso vira movimento a 5fps. Entre snapshots ele faz dead reckoning na última velocidade conhecida, sem RNG, sem colisão. Velocidade idêntica + rotação idêntica = 0.0000 de deriva no intervalo, então a correção quando o snapshot chega é invisível.
- A arena dá a volta, e a interpolação não sabia. O campo faz wrap nas bordas: um tanque que cruza salta de x=1279 pra x=0. Um lerp ingênuo arrasta ele de volta pela tela inteira, um risco visível pro outro jogador. A correção é interpolar pelo caminho mais curto com wrap e re-aplicar o wrap, com um snap acima de um limiar grande pra teleporte não virar deslize. O jogo já tinha essa matemática (wrappedDelta, a CPU sempre entendeu o wrap); a rede só não estava usando.
Arquitetura: Cloudflare Durable Objects, WebSocket nativo (cliente segue arquivo único, sem lib). Acerto de hit é do atirador, não da vítima, senão um celular em segundo plano nunca reporta o próprio dano. Fim de partida é mútuo: notifica, derruba a conexão, ignora tráfego atrasado.
Continua um HTML só, ~220KB, sem anúncio, sem conta, sem pay-to-win. E eu não escrevo o código: dirijo uma IA, testo cada build (334 até agora, todas verificadas com um harness headless antes de subir) e mando consertar o que quebra. Os cinco bugs acima o harness não pegou; só dois aparelhos de verdade pegaram. Anotei cada um como teste de regressão permanente.
https://www.hammeranvilbrew.com/vibetanks/
Ou jogue no itch.io: https://mdawg74.itch.io/vibe-tanks
Recorde no HARD segue 1:55. Se o matchmaking falhar com você, comenta o aparelho e a rede que eu conserto.