MultiplayerSpawner no Godot: Instanciar Jogadores em Rede

Instanciar jogadores no Godot multiplayer com o MultiplayerSpawner: spawn_path, cenas auto-spawnáveis, spawn function e o servidor criando o player.
Quando você vai instanciar jogadores no Godot multiplayer, bate uma dúvida que trava muita gente: eu crio a cena do player em qual máquina? No servidor? Em cada cliente? Nos dois? A resposta curta é "só no servidor", e a peça que faz isso dar certo é o node MultiplayerSpawner. Ele existe justamente para replicar a criação e a remoção de cenas em todos os pares conectados, sem você precisar mandar mensagem manual para cada um. Neste post a gente vai fundo nele: o papel que cumpre, a propriedade spawn_path, as cenas auto-spawnáveis, a spawn function custom para passar parâmetros, e o padrão de instanciar o jogador no sinal peer_connected quando alguém entra na partida.
Se você ainda não montou uma conexão de rede e nunca disparou uma função remota, vale ler antes o básico de multiplayer no Godot com RPC, porque aqui eu assumo que você já sabe abrir servidor, conectar cliente e o que é autoridade. Vou tratar o spawner como o próximo degrau, não como o primeiro.
O que o MultiplayerSpawner realmente faz
O MultiplayerSpawner tem uma função única e bem definida: ele vigia um node pai e replica, para todos os clientes, qualquer cena que nascer ou morrer embaixo desse pai. Nada mais. Ele não sincroniza posição, não manda input, não cuida de vida de inimigo. Ele responde a uma pergunta só: "quem existe na cena agora?".
A lógica por trás é a da rede high-level da Godot, onde o servidor é a fonte da verdade. Quando o servidor cria um node embaixo do pai vigiado, o spawner percebe, serializa qual cena era e sob qual pai ela entrou, e manda essa informação para cada cliente. Cada cliente então recria exatamente aquela cena, no mesmo lugar, com o mesmo nome de node. Quando o servidor remove o node, o spawner replica a remoção e a cena some em todas as telas.
Isso resolve o problema mais chato de instanciar jogadores em rede: a consistência. Sem o spawner, você teria que mandar uma RPC para cada cliente dizendo "cria um player com o nome X aqui", garantir que todos recebessem, tratar quem conectou atrasado, e por aí vai. O spawner empacota tudo isso. Você instancia uma vez, no servidor, e confia que vai aparecer igual em todo mundo.
Uma coisa importante de entender logo: o spawner replica a partir do servidor, na direção dos clientes. Ele não funciona como "qualquer máquina cria e os outros copiam". Quem instancia tem que ser quem tem autoridade sobre o pai vigiado, que no modelo normal é o servidor (id 1).
spawn_path e cenas auto-spawnáveis
Duas configurações fazem o spawner funcionar, e as duas ficam no painel do node no editor (ou dá para setar por código).
A primeira é o spawn_path. Essa propriedade aponta para o node pai cujos filhos serão vigiados. Tudo que nascer diretamente embaixo desse node vira candidato a ser replicado. Na prática você costuma criar um node vazio só para isso, algo como um Node chamado Players, e aponta o spawn_path do spawner para ele. Detalhe que gera bug silencioso: esse caminho precisa ser o mesmo caminho relativo em todas as máquinas. Se no servidor o pai está em /root/Game/Players e no cliente a árvore está montada diferente, a replicação não encontra o lugar certo e falha quieta.
A segunda configuração é a lista de cenas auto-spawnáveis. Você registra quais cenas o spawner tem permissão de replicar. No editor isso é o campo "Auto Spawn List"; por código você usa add_spawnable_scene() passando o caminho do arquivo da cena. Só cenas nessa lista serão replicadas automaticamente quando nascerem embaixo do spawn_path. É uma trava de segurança e de clareza: o spawner não sai replicando qualquer coisa, só o que você autorizou.
Montando por código, a configuração do lado do servidor fica assim:
extends Node
@onready var spawner: MultiplayerSpawner = $MultiplayerSpawner
@onready var container_jogadores: Node = $Players
const CENA_JOGADOR: String = "res://cenas/jogador.tscn"
func _ready() -> void:
# registra qual cena pode ser replicada automaticamente
spawner.add_spawnable_scene(CENA_JOGADOR)
# aponta para o node pai que sera vigiado
spawner.spawn_path = container_jogadores.get_path()
Com isso no ar, o contrato está firmado: qualquer instância de jogador.tscn que o servidor colocar embaixo do node Players vai aparecer, sozinha, em todos os clientes.
Instanciar jogadores quando alguém conecta
Agora o coração do assunto. O momento certo de criar a cena do jogador é quando um peer conecta, e o Godot avisa isso pelo sinal multiplayer.peer_connected. A regra é: só o servidor escuta esse sinal e só o servidor instancia.
Primeiro você conecta o sinal, mas guarda a instanciação para quem é servidor:
func _ready() -> void:
spawner.add_spawnable_scene(CENA_JOGADOR)
spawner.spawn_path = container_jogadores.get_path()
if multiplayer.is_server():
multiplayer.peer_connected.connect(_ao_conectar)
multiplayer.peer_disconnected.connect(_ao_desconectar)
# cria tambem o jogador do proprio host (id 1)
_ao_conectar(multiplayer.get_unique_id())
Repare que eu chamo _ao_conectar manualmente para o próprio host. O sinal peer_connected dispara para os peers que chegam depois, mas o host já está ali desde o começo, então ele não recebe o próprio sinal. Criar o jogador do id 1 na mão resolve isso.
O callback que instancia de fato:
func _ao_conectar(id: int) -> void:
var jogador: Node = load(CENA_JOGADOR).instantiate()
# o nome do node vira o id do peer: fica unico e previsivel
jogador.name = str(id)
container_jogadores.add_child(jogador)
Três linhas fazem o trabalho. O instantiate() cria a cena do jogador na memória. Definir jogador.name = str(id) dá a ele um nome único e igual em todas as máquinas, o que é essencial para o spawner casar o node certo entre servidor e clientes (e facilita muito achar o player de um peer depois). E o add_child() coloca ele embaixo do spawn_path, momento em que o MultiplayerSpawner entra em ação e replica a criação para todos os clientes.
Do lado do cliente, você não escreve nada disso. O cliente só recebe a cena pronta, criada pelo spawner. Essa é a parte que mais alivia: a lógica de instanciação vive num lugar só, no servidor.
A remoção segue a mesma filosofia. Quando alguém sai, o servidor apaga o node e o spawner replica o sumiço:
func _ao_desconectar(id: int) -> void:
var jogador: Node = container_jogadores.get_node_or_null(str(id))
if jogador != null:
jogador.queue_free()
Como o nome do node é o id do peer, achar quem saiu é direto com get_node_or_null(str(id)). O queue_free() no servidor some com o jogador em todas as telas.
Spawn function custom: nascendo com parâmetros
O fluxo acima cria sempre a mesma cena, do mesmo jeito. Mas e quando o jogador precisa nascer já com informação, tipo uma posição de spawn diferente, o personagem que a pessoa escolheu, ou a cor do time? O auto-spawn simples não passa dados: ele só replica "esta cena nasceu aqui". Para isso existe a spawn function custom.
Você registra uma função no spawner com spawn_function e, em vez de deixar o node nascer pelo add_child direto, chama spawner.spawn(dado). O spawner envia esse dado para todos os pares e, em cada máquina, chama sua função passando o dado como argumento. A função monta o node e o retorna. O valor que você passa precisa ser serializável pela rede (tipos básicos, dicionários, arrays desses tipos), porque ele viaja até os clientes.
func _ready() -> void:
spawner.spawn_function = _criar_jogador
spawner.spawn_path = container_jogadores.get_path()
if multiplayer.is_server():
multiplayer.peer_connected.connect(_ao_conectar)
_ao_conectar(multiplayer.get_unique_id())
func _ao_conectar(id: int) -> void:
# monta o dado inicial e pede ao spawner para replicar
var dado: Dictionary = {
"id": id,
"pos": _ponto_de_spawn_livre(),
}
spawner.spawn(dado)
# roda em TODAS as maquinas; recebe o dado e devolve o node pronto
func _criar_jogador(dado: Dictionary) -> Node:
var jogador: Node = load(CENA_JOGADOR).instantiate()
jogador.name = str(dado["id"])
jogador.position = dado["pos"]
# define a autoridade do node para o peer dono
jogador.set_multiplayer_authority(dado["id"])
return jogador
Alguns pontos que fazem diferença aqui. A _criar_jogador roda em todas as máquinas, não só no servidor, porque é ela que de fato constrói o node em cada peer. Por isso ela é o lugar natural para aplicar coisas que precisam valer em todo mundo desde o nascimento, como a posição inicial e a autoridade. O set_multiplayer_authority(dado["id"]) entrega o controle daquele jogador ao peer dono, que é o que permite, depois, ele mandar o próprio input sem que os outros clientes briguem pelo controle. E note que com spawn_function definida você não dá add_child na mão: quem adiciona o node embaixo do spawn_path é o próprio spawner, com o retorno da sua função.
A escolha entre auto-spawn simples e spawn function é pragmática. Se o jogador nasce sempre igual, o auto-spawn com add_spawnable_scene basta. Se ele precisa de parâmetros no nascimento, use a spawn function. Dá até para combinar, mas comece pelo mais simples que resolve seu caso.
O erro clássico: instanciar manualmente em cada cliente
Vale gastar um parágrafo no erro que quase todo mundo comete uma vez. A tentação é pensar assim: "cada máquina roda o jogo, então cada máquina cria os jogadores que precisa ver". Aí a pessoa coloca um instantiate() no _ready de cada cliente, ou dispara uma RPC genérica mandando todo mundo criar player.
Isso quebra de um jeito confuso. Cada cliente cria a própria cópia, sem vínculo com as cópias das outras máquinas. Elas ficam com autoridade errada, nomes que não batem, e quando você tenta sincronizar posição ou mandar um evento, as máquinas discordam sobre qual node é qual jogador. O resultado típico é jogador duplicado, player fantasma que ninguém controla, ou movimento que só aparece numa tela. E o pior: funciona na sua máquina de teste, onde host e cliente estão no mesmo PC, e só quebra quando tem rede de verdade no meio.
O modelo correto é o contrário do intuitivo: só o servidor instancia, e o MultiplayerSpawner replica. O cliente nunca cria jogador por conta própria. Ele recebe o que o servidor mandou. Guardar essa inversão na cabeça economiza horas de depuração, porque praticamente todo bug de "player aparecendo errado" vem de alguém instanciando no lugar que não devia.
Depois que o jogador existe e aparece em todas as telas, falta mantê-lo atualizado: posição, rotação, animação. Isso não é trabalho do spawner, é do node irmão, o MultiplayerSynchronizer para sincronizar jogadores. A divisão é limpa: o spawner diz quem existe, o synchronizer diz como cada um está agora. Usar os dois no lugar certo é o que deixa o multiplayer organizado.
Fechando
Para instanciar jogadores no Godot multiplayer sem dor de cabeça, a receita é enxuta. O MultiplayerSpawner replica criação e remoção de cenas a partir do servidor; você configura o spawn_path apontando para o node pai vigiado e registra a cena com add_spawnable_scene(). O servidor escuta multiplayer.peer_connected, instancia o player com instantiate(), dá a ele o nome do id do peer e adiciona embaixo do spawn_path, deixando o spawner replicar para todos. Quando precisa de parâmetros no nascimento, troca o auto-spawn pela spawn function custom com spawn_function e spawn(dado). E a regra de ouro: cliente nenhum instancia jogador por conta própria, isso é tarefa só do servidor.
Se esse texto mostrou que falta base de GDScript e organização de cenas para você acompanhar com folga, não tem problema, é um sinal útil. Um curso de criação de jogos para iniciantes firma o fundamento que torna o multiplayer bem menos assustador, porque metade da dificuldade de rede é, no fundo, organização de código. E para ver todas as peças do motor em contexto, o hub de Godot reúne o que você precisa em volta disso. Monte a conexão local com dois jogadores, faça o spawner criar os players no peer_connected, e só cresça a partir do que já funciona.
Perguntas frequentes
Para que serve o MultiplayerSpawner no Godot 4?
Ele replica a criação e a remoção de cenas em todos os clientes. Quando o servidor instancia uma cena filha do node apontado pelo spawn_path, o MultiplayerSpawner recria essa mesma cena em cada peer conectado, e quando ela é removida no servidor some em todo mundo. Assim você instancia uma vez, no servidor, e não precisa mandar RPC manual para cada cliente.
O que é o spawn_path do MultiplayerSpawner?
É a propriedade que aponta para o node pai cujos filhos serão vigiados e replicados. Todo node criado diretamente embaixo desse pai, sendo de uma cena registrada como auto-spawnável, é replicado automaticamente para os clientes. O spawn_path precisa apontar para o mesmo caminho relativo em todas as máquinas, senão a replicação falha.
Como instanciar o jogador quando alguém conecta?
No servidor, conecte o sinal multiplayer.peer_connected e, dentro do callback, instancie a cena do player com instantiate(), defina o nome do node como o id do peer e adicione como filho do node do spawn_path. O MultiplayerSpawner replica essa criação para todos os clientes sozinho. Nunca instancie o jogador manualmente em cada cliente.
O que é uma spawn function custom no MultiplayerSpawner?
É uma função que você registra com spawn_function para controlar como a cena nasce. Em vez de só carregar uma cena fixa, o spawner chama sua função passando um dado que você enviou no spawn(), e ela retorna o node já montado. Serve para passar parâmetros iniciais, como a posição de nascimento ou o personagem escolhido.
Qual a diferença entre MultiplayerSpawner e MultiplayerSynchronizer?
O MultiplayerSpawner cuida de quem existe: ele replica a criação e a remoção de nodes. O MultiplayerSynchronizer cuida do estado contínuo de quem já existe, espelhando valores como posição e rotação pela rede, frame a frame. Você usa os dois juntos: o spawner faz o jogador aparecer, o synchronizer mantém ele atualizado.
Por que não devo instanciar o jogador em cada cliente separadamente?
Porque cada máquina criaria sua própria cópia sem relação com as outras, e elas discordariam sobre quem é quem. A rede high-level da Godot espera que o servidor seja a fonte da verdade e que o MultiplayerSpawner replique a partir dele. Instanciar manualmente em cada cliente gera jogadores duplicados, autoridade errada e sincronização quebrada.


