Pitch: Lagario 3, meu clone de agar.io de 2018 refeito com gravidade de verdade e multiplayer sem servidor
Transparência: este texto foi escrito por um assistente de IA em meu nome, a partir do código-fonte e do changelog do jogo, e conferido contra o código. Os trechos de código são do repositório, levemente enxugados.
Em 2018 fiz um clone de agar.io num fim de semana: um planeta que seguia o mouse com aceleração e atrito quase nulo, três arquivos (planeta.js, vector.js, functions.js) e um comentário perdido no código: F = G * m1 * m2 / r². Na versão 3, esse comentário virou o jogo inteiro.
O Lagario 3: Órbita é um multiplayer de navegador em que você é um planeta com embalo, as estrelas puxam com gravidade que cai com o quadrado da distância, e não existe servidor de jogo. Dá pra jogar aqui, no PC ou no celular, sem cadastro: https://lagario.thomasar.dev

Abaixo, as decisões técnicas que mais me ensinaram alguma coisa.
Por que não tem servidor
O site fica na Vercel, e funções serverless (inclusive os WebSockets de lá) não garantem que todos os jogadores de uma sala caiam na mesma instância. Em vez de brigar com isso, tirei o servidor da equação: quem cria a sala hospeda a partida no próprio navegador. A simulação autoritativa roda num Web Worker a 30 ticks por segundo, e os outros jogadores se conectam por WebRTC. A sinalização usa relays públicos do Nostr via Trystero. Tirando um endpoint pequeno e opcional que entrega credenciais TURN, o site é estático.
O Worker tem um motivo além de liberar a thread principal: o navegador segura os timers dela em abas de segundo plano, mas o timer do Worker segue rodando. O host pode trocar de aba sem congelar a sala.
A rede manda snapshots binários filtrados por área de interesse: cada cliente só recebe o que está na tela dele, e a poeira vai por diferença. Salas pequenas recebem 30 snapshots/s (uns 4–5 KB/s por jogador numa sala com bots), salas grandes, 15/s.
Quando o host sai
"Um jogador é o servidor" funciona até esse jogador fechar a aba. A cada 2 segundos o host manda um checkpoint do mundo (incluindo as estrelas) para os dois próximos da fila. Se um cliente passa 5 segundos sem ouvir o host, ele não assume na hora. Primeiro pergunta aos outros quem eles estão seguindo, porque pode ser só a conexão dele que caiu:
const oldStillServing = hints.some((h) => h.host === old && h.epoch >= this.epoch)
const newerHost = hints.some((h) => h.host && h.host !== old && h.epoch > this.epoch)
if (oldStillServing || newerHost) {
// alguém ainda enxerga um host que eu não enxergo: espero em vez de rachar a sala
}
Se ninguém mais vê o host antigo, o próximo da fila restaura o último checkpoint no próprio Worker, incrementa um número de época e se anuncia. Todo mundo reconecta mantendo seus planetas. Se dois peers acabarem hospedando ao mesmo tempo, vence a época maior e, no empate, o id menor; o outro volta a ser cliente. Tenho um teste end-to-end com três sessões de navegador isoladas, em WebRTC de verdade, que passa por sala, chat e troca de host.
Gravidade que dá pra jogar
As regras de movimento ficam num módulo compartilhado, physics.ts, que o host e o cliente importam. Estrelas, buracos negros e planetas acima de certa massa puxam. Fora do raio da fonte a força cai com o quadrado da distância, dentro dela cai linearmente até o centro:
const a = d2 >= r2 ? (s.g * r2) / d2 : s.g * (d / s.r)
Física realista pura não era divertida. Dois ajustes fizeram virar jogo:
- A gravidade é proporcional ao quanto você consegue empurrar de volta. Planeta grande é mais lento, então com física de verdade toda estrela seria uma armadilha pra ele. Aqui a atração é multiplicada pelo empuxo do planeta (relativo a um planeta inicial), e a de uma estrela nunca passa de 80% desse empuxo. Apontar pra fora sempre escapa; só não encoste, porque queima.
- O estilingue não perde velocidade. Normalmente a velocidade converge devagar pra onde você aponta. Mas se você já está mais rápido que a sua velocidade máxima na direção em que está apontando, esse excesso some a só 15% do ritmo normal. Mergulhe rente a uma estrela, siga em frente e saia bem mais rápido, com teto de 3,2× a velocidade máxima.
A linha pontilhada da imagem abaixo é a previsão de trajetória. O cliente reexecuta exatamente as mesmas regras de movimento do host por 60 passos (de 1,4 a 4 segundos à frente), com as entidades que ele enxerga, e pinta de vermelho o trecho que entra na zona de queima de uma estrela.

Outra mudança em relação ao agar.io: planetas com menos de 25% de diferença de massa não se comem, eles colidem (impulso com coeficiente de restituição 0,45). O tranco que cada lado leva é comparado com a própria velocidade máxima; passando de um limite, o planeta perde até 25% da massa em até 8 pedaços que voam da superfície. O dono não pode engolir os próprios pedaços por 1,5 s, então toda batida forte vira uma disputa pelos destroços.
As versões antigas continuam no ar
Não queria que a v3 apagasse a v1 e a v2. Um único deploy serve tudo: a versão atual em / e as antigas congeladas em /v1/ e /v2/. O script npm run archive -- v2.0.0 faz checkout da tag num git worktree, instala as dependências daquela tag, compila com o base do Vite em /v2/ e grava o resultado em archive/. Depois ajusta o build pra conviver com a versão atual:
- um shim de
localStoragepõe um prefixo nas chaves da versão congelada: na primeira visita ela lê os dados da versão atual (você mantém nome e nível), mas só grava na própria cópia; - o manifesto PWA abre
/v2/, os source maps e o service worker antigo saem, e links para os antigos deploys separados (lagario-vN.vercel.app) viram/vN/.
A rede também é versionada: cada versão entra no P2P com um namespace próprio, então um cliente da v2 nunca cai numa sala da v3.
Stack e feedback
TypeScript, Vite, WebGL2 (com o renderer Canvas 2D da v1 como fallback) e bots completando as salas vazias. Quero muito feedback sobre a sensação da física, principalmente o estilingue, e respondo dúvidas sobre o P2P nos comentários.
Jogue grátis: https://lagario.thomasar.dev (e compare com https://lagario.thomasar.dev/v1/ e /v2/)