Doug's Game Dev Log
Sobre
EN|PT-BR
2026-08-03

O Servidor Parou de Se Repetir

O trampo de hoje no td-survivors foi basicamente ensinar boas maneiras pro servidor de co-op: em vez de gritar o estado inteiro do jogo pra cada cliente trinta vezes por segundo, agora ele só comenta o que de fato mudou. No caminho o servidor de multiplayer também ganhou memória, e alguns inimigos e torres levaram uma cutucada.

Se você perdeu como o servidor de co-op funciona, o resumo: um processo Node headless roda a simulação de verdade, e os dois navegadores são clientes magrelos que só renderizam snapshots. O que é elegante, até você olhar quanto esses snapshots custam.

O servidor era muito, muito repetitivo

A cada tick, a 30Hz, o servidor serializava o mundo inteiro em JSON e disparava pra cada cliente. Posição de cada inimigo, cada campo de cada torre, cada projétil, cada orb de XP, todo frame, tendo mudado alguma coisa ou não. E olha: a maior parte do mundo não muda entre dois frames separados por 33 milissegundos. Eu tava pagando o preço cheio pra avisar todo mundo que aquela torre no canto continua sendo, de fato, uma torre no canto.

Então agora o servidor manda um delta: um diff campo a campo contra o snapshot do tick anterior. Se um inimigo andou, você recebe a posição nova dele. Se não andou, você não recebe nada sobre ele. Os escalares compartilhados e o estado por player continuam indo inteiros porque são minúsculos; a parte grande, os arrays de entidades, é o que apanha o diff. Pra não deixar quem entra atrasado ou reconecta se perder, o servidor também manda um keyframe completo a cada 2 segundos, e força um na hora que alguém entra, pra um cliente novo sempre bootar com o mundo inteiro em vez de tentar reconstruir a realidade a partir de um fluxo de "o goblin andou 0.3 unidade pra esquerda".

O codec mora num net/delta.ts novo e puro (diffSnapshot / applyDelta, com testes antes, porque errar sutilmente um codec de diff é o tipo de bug que estraga um fim de semana). O cliente só vai dobrando cada delta de volta num snapshot completo em memória e renderiza igualzinho antes. A camada de render nem ficou sabendo que algo mudou, que é o tipo mais gostoso de refactor.

Os números, que é a parte boa

Numa run de verdade com 2 players eu medi: ~49% menos banda por cliente, de 202 KB/s pra 103 KB/s. Mais ou menos metade.

E agora o detalhe contraintuitivo que eu não esperava. A CPU do servidor caiu também. Você pensaria que fazer diff de toda entidade em todo tick ia te custar caro, e custa: uns 12 microssegundos por tick, que dá 0,04% do orçamento do frame, ou seja, nada. Mas o payload que você entrega pro JSON.stringify agora é bem menor, e serializar menos coisa é mais barato que serializar tudo. A economia no stringify paga o diff com folga. Saldo final: uns 8 microssegundos economizados por tick. Menos banda e menos CPU, o que quase nunca rola ao mesmo tempo. Resolvi curtir em vez de ficar desconfiado.

O servidor ganhou memória

Tema relacionado: o servidor de co-op era um amnésico ferrenho. Da última vez eu comentei do ponto cego glorioso dele, ele não lê o seu save, então todo mundo entrava zerado, sem banishes e sem saber quem era.

Isso melhorou. Todo save agora tem um playerId permanente, e o cliente manda um perfilzinho (unlocks, níveis de banish/reroll) quando cria ou entra numa sala. O servidor reaproveita o seu slot quando o playerId reconecta, então se você cair e voltar, você retoma com XP, nível e inventário intactos em vez de renascer como um estranho. As rolagens de torre e upgrade agora são por player também: quem desbloqueou uma torre é oferecido ela, os outros não. O HP dos inimigos também escala com a galera, 1.5^(N-1) na hora do spawn, pra dois players não passarem o rodo numa wave calibrada pra um só (inimigo que já nasceu nunca é buffado, só os novos). E eu consertei o servidor travando no Ctrl+C, o bug menos glamouroso mas mais pessoalmente satisfatório do dia.

Já que eu tava ali dentro

Alguns ajustes de gameplay pegaram carona. O shieldbearer agora usa 5 anéis de escudo concêntricos que caem um de cada vez conforme ele apanha, então dá pra ler de verdade o quão perto ele tá de rachar. A torre de veneno foi retrabalhada de um pulso em área pra um dardo de veneno single-target: atira a cada 1,5s no próximo inimigo ainda não envenenado, não dá dano de impacto, e o veneno agora tica de forma discreta e sem perfurar, o que significa que cada tique arranca exatamente um anel de escudo do shieldbearer. Inimigo envenenado também virou roxo, porque verde no verde era crime. A fase 1 ganhou uma wave dedicada de enxame de brood e um cronograma refrescado pra combinar com os inimigos rebalanceados.

Na próxima: eu ainda adoraria ideias de torre pra um jogo sobre zapear micróbios sorridentes, e quero ver até onde o truque do delta escala quando tiver mais de dois cursores no tabuleiro. O servidor parou de se repetir. Agora eu descubro quanto ele aguenta carregar.