Voltar para o Blog
Quest Log

MultiplayerSynchronizer no Godot: Sincronizar Jogadores

Editor da Godot mostrando um node MultiplayerSynchronizer selecionado com o painel de Replication listando propriedades de um personagem

Como sincronizar jogadores no Godot multiplayer com o MultiplayerSynchronizer: replication config, posição vs eventos, autoridade e o erro de replicar tudo.

Ferramenta:Godot

Se você já montou o básico de rede e agora quer sincronizar jogadores no Godot multiplayer sem escrever um RPC para cada pixel que o personagem anda, o node certo é o MultiplayerSynchronizer. Ele existe justamente para o tipo de dado que muda o tempo todo: posição, rotação, estado de animação, vida. Em vez de você mandar manualmente "andei para a direita" trinta vezes por segundo, o Synchronizer pega as propriedades que você listar e manda o valor atual delas para as outras máquinas, sozinho. Este post é o aprofundamento desse node: o que ele replica, como montar o replication config, quando usar posição e quando usar evento, como a autoridade entra na jogada e o erro clássico de querer sincronizar tudo.

Se os conceitos de servidor, cliente e o anotador @rpc ainda estão confusos, vale ler antes o guia de multiplayer no Godot com RPC. Aqui eu assumo que você já sabe abrir uma conexão com ENetMultiplayerPeer e já entende o que é um peer. O foco agora é o estado contínuo.

O que o Synchronizer replica: propriedades, não eventos

A primeira coisa a gravar: o MultiplayerSynchronizer replica propriedades, não ações. Ele não sabe que você atirou. Ele sabe que a propriedade position do personagem vale tal coisa agora e que, no último envio, valia outra. O trabalho dele é manter a lista de propriedades que você escolher igual em todas as máquinas, pegando o valor da máquina com autoridade e empurrando para o resto.

Isso muda o jeito de pensar. Com RPC você pensa em verbos: "mandar executar X". Com o Synchronizer você pensa em substantivos: "este valor precisa estar igual em todo mundo". Posição, rotação, a vida atual, qual animação está tocando, se o personagem está agachado. Tudo que é estado e que, se chegar um pouco atrasado, o próximo update corrige.

Na prática você adiciona o node como filho do que quer sincronizar. Uma estrutura comum de cena de jogador fica assim: um CharacterBody2D como raiz, o sprite, o colisor e, pendurado ali, um MultiplayerSynchronizer. Ele tem uma propriedade chamada root_path que aponta para o node cujas propriedades ele vai observar, por padrão o pai.

Montando o replication config

O coração do node é o replication config. Com o MultiplayerSynchronizer selecionado, aparece no rodapé do editor o painel "Replication". É ali que você adiciona cada propriedade que quer sincronizar, clicando no node alvo e escolhendo o campo, por exemplo position, rotation, ou uma variável do seu script.

Cada propriedade na lista tem duas opções que importam muito: Spawn e Sync (também chamada de Watch em versões do painel). "Spawn" manda o valor uma vez, no nascimento do node, para quem chega depois já receber o estado inicial. "Sync" manda o valor continuamente, a cada atualização de rede. Vida inicial, por exemplo, você quer no Spawn. Posição, que muda todo frame, no Sync.

Dá para configurar o mesmo via código, o que é útil para entender o que o painel faz por baixo. A API real é SceneReplicationConfig:

extends MultiplayerSynchronizer

func _ready() -> void:
    var config: SceneReplicationConfig = SceneReplicationConfig.new()

    var pos_path: NodePath = NodePath(".:position")
    config.add_property(pos_path)
    config.property_set_spawn(pos_path, true)
    config.property_set_replication_mode(pos_path, SceneReplicationConfig.REPLICATION_MODE_ALWAYS)

    var hp_path: NodePath = NodePath(".:current_health")
    config.add_property(hp_path)
    config.property_set_spawn(hp_path, true)
    config.property_set_replication_mode(hp_path, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE)

    replication_config = config

Repare no NodePath(".:position"): o ponto é o node alvo (relativo ao root_path) e, depois dos dois-pontos, vem a propriedade. O REPLICATION_MODE_ON_CHANGE é um detalhe que salva banda: a current_health só vai para a rede quando o valor muda, em vez de ir em todo frame como a posição. Na prática você vai montar isso pelo painel na maioria das vezes, mas saber a API ajuda quando precisa gerar config em runtime.

Outra propriedade do próprio Synchronizer que vale conhecer é o replication_interval. Ele define de quantos em quantos segundos as propriedades em modo contínuo são enviadas. Zero significa todo frame de rede. Para dados que mudam devagar, subir esse intervalo corta tráfego sem ninguém perceber.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Posição x eventos: Synchronizer para o fluxo, RPC para o pontual

Essa é a decisão que mais confunde quem está começando a sincronizar jogadores no Godot multiplayer. A regra prática: se o dado é um fluxo contínuo em que só o valor mais recente importa, use o Synchronizer. Se é um acontecimento pontual que não pode se perder, use RPC.

Posição é o exemplo perfeito de fluxo. Se um pacote de posição se perde, tudo bem: o próximo chega em milissegundos e corrige. Por isso o Synchronizer manda esses dados em modo unreliable, o mais leve, aceitando perda em troca de velocidade. Não faz sentido garantir a entrega de uma posição que já ficou velha.

Agora pense num tiro. Se o pacote "fulano atirou" se perde, o tiro simplesmente não acontece na tela do outro jogador, e não existe "próximo update" que conserte: foi um evento único. Isso tem que ir por RPC reliable, com garantia de entrega e ordem. Mesma coisa para dano aplicado, abrir uma porta, trocar de arma, pontuar. São verbos, não estados.

Um caso misto deixa isso claro. A posição do projétil, enquanto ele voa, pode ir pelo Synchronizer. Mas o momento em que o gatilho é puxado, e o momento em que ele acerta alguém e aplica dano, são eventos de RPC. Misturar os dois na mesma cena é o normal, não a exceção. O Synchronizer cuida do "onde as coisas estão" e o RPC cuida do "o que acabou de acontecer".

Autoridade: quem tem o direito de escrever

O MultiplayerSynchronizer depende de autoridade para saber de onde vem a verdade. A máquina que tem autoridade sobre o node é a que lê as propriedades locais e as envia; as outras recebem e aplicam, sem mandar de volta. Se você não definir isso direito, duas máquinas tentam ditar a mesma position ao mesmo tempo e o resultado é o personagem tremendo ou voltando no tempo.

Em jogos onde cada jogador controla o próprio personagem, o padrão é dar a autoridade do personagem ao peer dono dele, logo que a cena nasce:

extends CharacterBody2D

func setup_player(peer_id: int) -> void:
    set_multiplayer_authority(peer_id)

func _physics_process(delta: float) -> void:
    if not is_multiplayer_authority():
        return

    var direction: float = Input.get_axis("ui_left", "ui_right")
    velocity.x = direction * 300.0
    move_and_slide()

O if not is_multiplayer_authority(): return é o guarda mais importante do código de multiplayer. Só quem tem autoridade processa input e move o corpo. Nas outras máquinas esse _physics_process não faz nada, porque a posição vai chegar pronta pelo Synchronizer. Sem esse guarda, cada cliente tenta mover a cópia local do personagem alheio com o próprio input, e aí mora o caos.

Repare que a autoridade é por node, não global. Normalmente o servidor tem autoridade sobre inimigos e sobre o mundo, e cada cliente tem autoridade sobre o próprio avatar. Se você quer que o servidor seja a fonte da verdade até da posição do jogador, para evitar trapaça, o desenho muda: o cliente manda input por RPC, o servidor processa e aí o servidor tem a autoridade do Synchronizer. Esse é um assunto por si só, que eu detalho no post sobre autoridade de servidor.

O erro comum: sincronizar tudo

O engano que quase todo mundo comete uma vez é jogar meia dúzia de propriedades no replication config "por garantia" e deixar tudo em modo contínuo. O jogo funciona em teste local, onde a rede é a própria máquina, e aí trava quando duas pessoas jogam de verdade pela internet.

O motivo é direto: cada propriedade no modo contínuo vira tráfego em cada atualização. Se você sincroniza posição, rotação, velocidade, vida, munição, estado de animação e mais três flags, tudo todo frame, multiplicado por quantos jogadores existem, a banda estoura e a latência sobe. O Synchronizer não cobra nada por propriedade listada em teste local, mas cobra caro na vida real.

Três hábitos evitam isso. Primeiro, replique só o que o outro lado não consegue deduzir. A velocity, por exemplo, muitas vezes não precisa ir pela rede: se a posição chega, o outro lado pode interpolar o movimento sozinho. Segundo, use o modo on change para o que muda devagar. Vida, munição e estado não precisam ir todo frame; só quando o valor muda. Terceiro, ajuste o replication_interval por node. A posição do jogador pode ir a cada frame, mas a posição de um item de cenário que balança devagar pode ir dez vezes menos.

Pensar em banda desde o começo não é otimização prematura aqui, é parte do desenho. Rede é o recurso mais escasso de um jogo multiplayer, e o Synchronizer torna fácil demais desperdiçá-lo sem perceber. A pergunta certa para cada propriedade da lista é: "o outro lado realmente precisa receber isso, ou consegue descobrir sozinho?". Se consegue, tire da lista.

Juntando as peças

O fluxo completo de uma cena de jogador fica mais ou menos assim. O MultiplayerSpawner cria o personagem em todas as máquinas quando o peer conecta. No nascimento, você chama set_multiplayer_authority com o id do dono, usando o multiplayer.get_unique_id() para saber quem é cada um. O MultiplayerSynchronizer então mantém posição e estado de animação sincronizados, com a vida em modo on change. E os eventos pontuais, tiro e dano, continuam indo por RPC reliable.

Com esse desenho você tem a divisão limpa que todo multiplayer saudável tem: o Synchronizer cuida do contínuo, o RPC cuida do pontual, e a autoridade deixa claro quem escreve cada coisa. Comece com um só personagem andando sincronizado entre duas janelas, confirme que a posição flui e que o guarda de autoridade está no lugar, e só então vá somando. Se quiser revisar as ferramentas e o ecossistema da engine, o hub de Godot reúne o resto do caminho. Multiplayer continua sendo difícil, mas o MultiplayerSynchronizer tira do seu colo justamente a parte mais chata, que é manter o estado de todo mundo igual sem você escrever um RPC para cada frame.

Perguntas frequentes

O que o MultiplayerSynchronizer replica?

Ele replica propriedades de nodes, não eventos. Você monta uma lista no replication config (posição, rotação, vida, estado de animação) e o Synchronizer envia o valor atual dessas propriedades da máquina com autoridade para todas as outras, em intervalos regulares. Para disparar algo pontual, como um tiro, você usa RPC, não o Synchronizer.

Qual a diferença entre sincronizar posição e usar RPC?

Posição é estado contínuo: muda todo frame e o valor mais recente substitui o anterior, então o MultiplayerSynchronizer com envio unreliable é ideal. Eventos são pontuais: um tiro, um dano, abrir uma porta. Perder um evento quebra a lógica, então ele vai por RPC reliable. Use o Synchronizer para o que flui e RPC para o que acontece uma vez.

Como a autoridade afeta o MultiplayerSynchronizer?

O Synchronizer só envia dados a partir do peer que tem autoridade sobre o node (ou sobre ele próprio, dependendo do public_visibility). Os outros peers recebem e aplicam os valores, sem escrever de volta. Se a autoridade estiver errada, duas máquinas tentam mandar na mesma propriedade e a posição treme ou volta no tempo.

Por que não devo sincronizar todas as propriedades?

Cada propriedade na lista vira tráfego de rede em cada atualização. Replicar vida que muda raramente no mesmo ritmo da posição, ou sincronizar valores que dá para calcular localmente, infla a banda sem necessidade e piora a latência. Replique o mínimo que o outro lado não consegue deduzir sozinho.

O replication_interval controla o quê?

Ele define de quantos em quantos segundos o MultiplayerSynchronizer envia as propriedades marcadas para sincronização contínua. Zero significa enviar todo frame de rede. Aumentar o intervalo de dados que mudam devagar, como vida ou munição, reduz bastante o tráfego sem prejuízo perceptível.

Dá para usar MultiplayerSynchronizer sem MultiplayerSpawner?

Dá, se o node já existe na cena em todas as máquinas desde o início, como um objeto fixo do mapa. Mas para jogadores que entram e saem da partida você quase sempre quer o MultiplayerSpawner criando a cena em todo mundo e o MultiplayerSynchronizer mantendo o estado dela sincronizado depois.