Synchronization between player help

Godot Version

4.5.1

Question

Hello i’m learning to create a 3D multiplayer game with one headless instance acting like a server and x clients.

Currently i have this code which works :

NetworkManager :

var peer: ENetMultiplayerPeer

var playerName: String = ""
var player_names: Dictionary = {}

signal player_data_received(id)

func host_game(port: int = 9000):
	peer = ENetMultiplayerPeer.new()
	peer.create_server(port)
	multiplayer.multiplayer_peer = peer
	print("Hosting on port ", port)
	get_tree().change_scene_to_file("res://scenes/Game.tscn")
	multiplayer.peer_connected.connect(_on_peer_connected)
	multiplayer.peer_disconnected.connect(_on_peer_disconnected)

func join_game(ip: String, port: int = 9000):
	peer = ENetMultiplayerPeer.new()
	peer.create_client(ip, port)
	multiplayer.multiplayer_peer = peer
	multiplayer.connected_to_server.connect(_on_connection_success)

func _on_peer_connected(id):
	print("Peer connected : ", id)

func _on_peer_disconnected(id):
	player_names.erase(id)
	print("Peer disconnected : ", id)
	
func _on_connection_success():
	get_tree().change_scene_to_file("res://scenes/Game.tscn")
	send_player_info.rpc_id(1, playerName)
	
@rpc("any_peer", "reliable")
func send_player_info(pseudo: String):
	var id = multiplayer.get_remote_sender_id()
	#player_names[id] = pseudo
	player_data_received.emit(id)

Game.gd :

func _ready():
	multiplayer.peer_connected.connect(_on_player_joined)
	#NetworkManager.player_data_received2.connect(_on_player_joined)

func _on_player_joined(id):
	if multiplayer.is_server():
		spawn_player.rpc(id,id,"test")
		for existing_id in players.keys():
			if existing_id != id:
				spawn_player.rpc_id(id, existing_id, existing_id, "test")

but the thing is, when i want to use the signal and modify Game.gd such as : (just uncommenting 3rd line and commenting 2nd line :

func _ready():
	#multiplayer.peer_connected.connect(_on_player_joined)
	NetworkManager.player_data_received2.connect(_on_player_joined)

then i have a strange bug with this configuration :

Player 1 connect everything is ok

Player 2 connect but can’t see the movement of player 1

Player 3 connect but can’t ses the movement of player 1 and 2

thus, player 1 can see P2 and P3 moving, and P2 can see P3 moving

So i guess it has something to do with my MultiplayerSynchronizer in the player scene but can’t figured out why it’s working in first case but not with the signal emit (second case)

the MultiplayerSynchronizer sync the player position

the goal of the changement is to use an signal to emit() with the player’s id and his pseudo choosed (giving more data). i just could emit an additional signal, but i just want to understand why my code breaks

if you need more code / scene screenshot tell me

Have you made sure the signal flow is equivalent?

With the first way:

P1 connects and peer connected is emit twice “globally.” Once on the server and once on client P1.

P2 connects and peer connected is emit four times. Server gets P2, P1 gets P2, P2 gets Server, P2 gets P1.

With P3 the signal is emit 6 times globally, and so on.

It looks like when you send player info on connection success, the logic is “when a player connects, tell others” but the first way is more like “when a player connects, tell others, and also tell that player about the others”

1 Like

@neozuki

The signal flow seems equivalent to me

with the signal method (2nd option) :

When P2 join, on p2 instance, P1 is spawned in the middle of the area (with the spawn function) and i checked in the debuugger, : he got the right authority (player 1 authority) and the right multiplayerSynchronizer authority (1) but i can’t understand why after spawning he is not sync any movement.

i did a log for both case :

i added : print("_on_player_joined : called from : ", multiplayer.get_unique_id())

in top of the func _on_player_joined()

option 1 (works) : peer_connected directy

_on_player_joined : called from : 1
_on_player_joined : called from : 1
_on_player_joined : called from : 1622364423
_on_player_joined : called from : 727979785

option 2 (doesn’t work) : signal :

_on_player_joined : called from : 1
_on_player_joined : called from : 1

we can see option only got two call from the server instance, but it’s due to client that call

send_player_info.rpc_id(1, playerName)

so “player_data_received.emit(id,pseudo)” should only be called by the server and then call the “_on_player_joined” func. It’s ok because this function start directly with “if multiplayer.is_server():”

so i don’t understand why i have a different behavior between both option

Is it possible that in join_game the “connection success” signal is emitted before you connect the signal to the “send info” part? So this part is broken?

I think that’s it. In game.gd (method 1) you connect directly to the multiplayer API so it’s working, but in method 2 it’s connected to data received which is never emitted because send info was never called.

Edit: I could be wrong about how signals are called but I assume the signal is being emitted when you form the connection?

if i do the reverse flow :

NetworkManager.player_data_received.connect(_on_player_joined) called by

player_data_received.emit(id) called by

func send_player_info() called by

send_player_info.rpc_id(1, playerName) called by

_on_connection_success() called by

multiplayer.connected_to_server.connect(_on_connection_success) called by

join_game(ip: String, port: int = 9000)

i think it’s ok because i got :

spawn_player.rpc_id(id, existing_id, existing_id, “test”)

is well executed on every player because i see the P1 spawning on the P2 instance with the right name:

Enregistrement 2026-02-20 164044

but P2 doesn’t have the sync (position) working for P1 so i’m very confused how different it is between the two methods

I don’t know enough about MultiplayerSynchronizer to guess at why it’s not working. I have a working multiplayer prototype where peers can move around, shoot at each other, and chat… but it’s client authoritative and relies on MultiplayerSpawner + manual syncing via rpcs. I had the same problem as you at some point… it’s frustrating I can’t remember what the problem was.

You are making it server authoritative right?

If it works the first way, I assume you have force_readable set in your spawn function. And the internal names look like they’re being set.

I’d be willing to debug more if you wanted to share some minimal setup. For what it’s worth, I’ve uploaded my relevant code for my multiplayer prototype here: GitHub - neozuki/mptest1

It’s the global stuff that sets up multiplayer and syncs user data like name, color, etc.

@neozuki Ok so,

while i was cleaning my code to send you a working setup,

i was wondering why i’m not using a spawner for the multiplayer (MultiplayerSpawner object)

so instead of using rpc spawn (spawn_player.rpc…) i tried with a MultiplayerSpawner. And it seems to work !

I just have one little mis understanding :

my game.gd is :

extends Node3D

@export var player_scene: PackedScene
var spawns: Array[Vector3] = []
@onready var spawner = $MultiplayerSpawnerPlayer
# Dictionary to track players by their peer ID
var players := {}

func _ready():
	# Collect spawn points
	for child in $SpawnPoints.get_children():
		spawns.append(child.global_position)
		
	if multiplayer.is_server():
		NetworkManager.player_data_received.connect(_on_player_joined)
	
	spawner.spawned.connect(_on_player_spawned)

func _on_player_joined(id,pseudo):
	spawn_player(id,0,pseudo)

func _on_player_spawned(node: Node):
	if(node.name == str(multiplayer.get_unique_id())):
		print("Node: ", node.name, " | Authority: ", node.get_multiplayer_authority(), " | Local ID: ", multiplayer.get_unique_id())
		node.set_multiplayer_authority(multiplayer.get_unique_id())
		node.get_node("MultiplayerSynchronizer").set_multiplayer_authority(1)
		print("Node: ", node.name, " | Authority: ", node.get_multiplayer_authority(), " | Local ID: ", multiplayer.get_unique_id())
		$HUD.initialize(node)
		_attach_camera(node)

func _attach_camera(target_player: Node):
	await get_tree().process_frame
	var cam = get_tree().get_first_node_in_group("CameraRig")
	if cam:
		cam.call_deferred("follow_target", target_player)
		
func spawn_player(id: int, spawn_index: int, pseudo: String):
	var player = player_scene.instantiate()
	player.projectile_container = $Projectiles
	player.add_to_group("player")

	player.name = str(id)
	player.pseudo=pseudo
	player.global_position = spawns[spawn_index % spawns.size()]
	player.spawn_point = spawns[spawn_index % spawns.size()]
	
	%Players.add_child(player)
	player.set_multiplayer_authority(id)
	player.get_node("MultiplayerSynchronizer").set_multiplayer_authority(1)
	
	# Track in dictionary
	players[id] = player

so my logic is following →

For server, when client join this line trigger (listener) →

NetworkManager.player_data_received.connect(_on_player_joined)

then _on_player_joined() is just calling spawn_player()

i create a player scene, set pseudo… and most importantly the player authority which should be the id of the client connecting. But i realized even if server set the authority = client

the client see the authority for the player at 1. I don’t get it why MultiplayerSpawner spawn a player without giving the good authority

so my client have to manually set authority to himself in the function : _on_player_spawned

this func should just be useful to attach camera to the player when the player spawn but i have to set authority too (kinda strange for a client)

in the print() i have :

Node: 1387452276 | Authority: 1 | Local ID: 1387452276
Node: 1387452276 | Authority: 1387452276 | Local ID: 1387452276

so it confirm client recieve the new “player scene” with server’s authority meanwhile server did well set the good authority

do you know how to fix and my code should be clean after that ?

The docs here say that authority is not automatically replicated for peers. It defaults to 1 so clients need to have it set manually.

As far as I know you need to do add_child( player, true ) so that the name you give it in code is the name it has in the scene tree at runtime. This is why you see that pattern where a player node is named (by who has authority over the spawner, the server in this case) after a peer id, and then inside the player script you set the authority by the name in _ready for example. But if you don’t set force_readable then nodes added at runtime have dynamic names to ensure they’re unique. But that’s not an issue here because peer id is already unique.

This is just for nodes that you want the high level multiplayer API to handle though. It’s the multiplayer API that cares about authority and consistent names & node paths.

1 Like

Ok, so i guess it’s the right place into my spawned call back

i tried to put the authority sync into the multiplayerSynchronizer :

But doesn’t work wells. I guess i will stick to the spawned function callback