Multiplayer clients spawn at 0,0,0. Host spawns at indicated position

Godot Version

v4.2.1

Question

I have some code that allows a host to create a server and spawn next to a defined spawn point. Clients can join and spawn, however they are positioning at 0,0,0 instead of the spawn point. They can otherwise move and see/be seen by the host correctly.

The players ServerSynchronizer is set to ‘spawn’ and ‘always’. I’ve checked that the player is being added to the correct node.

The add_player function instantiates the clients character, sets the id/name, positions it and adds it to a dedicated, empty node.

func add_player(id: int):
	# Instantiate a player and spawn it
	var character = preload("res://player.tscn").instantiate()

	# Set player id.
	character.player = id
	
	# Set player name
	character.name = str(id)
	
	# Get the position of the appropriate spawn point
	var spawnPoint = get_node("mship1").position
	print("Ship position: ", spawnPoint)

	# Move the player to that position
	character.position = Vector3(spawnPoint.x, spawnPoint.y + 20, spawnPoint.z - 20)
	print("Character position: ", character.position)
	$Players.add_child(character, true)

I’ve then added a print to the players ready function to confirm its position

func _ready():
	# Give authority over the player input to the appropriate peer.
	if not is_multiplayer_authority(): return
	# Set the camera as current if we are this player.
	if str(name).to_int() == multiplayer.get_unique_id():
		$Camera3D.current = true
		#var camera = $Camera3D.current
	capture_mouse()
	print("Players 'on ready' position: ", position)

When run, the host prints:

Ship position: (292.334, 22.0841, 724.584)
Character position: (292.334, 42.0841, 704.584)
Players 'on ready' position: (292.334, 42.0841, 704.584)

The client prints

Ship position: (292.334, 22.0841, 724.584)
Character position: (292.334, 42.0841, 704.584)
Players 'on ready' position: (0, 0, 0)

My best guess is this is some kind of client/server permission issue, but tbh its got me stumped.

After some poking, I’ve made a few observations.
1 - The client spawn is relevant to the ‘Players’ node that is acting as parent to the player nodes (This is an otherwise empty node, its just for organisation
2 - The host is firing the ‘on ready’ prints BEFORE the line that sets it position. Meanwhile, the client is doing the reverse.
3 - Even using global positioning, things don’t line up. In the below output, the ship global position is the actual spawn point, but the ‘on ready’ global-position is returning something else.

<HOST OUTPUT>
Ship global_position: (292.334, 22.0841, 724.584)
Players 'on ready' global-position: (82.0633, 23.4845, 762.729)
Players 'on ready' position: (0, 0, 0)
Character global_position: (292.334, 42.0841, 704.584)
<CLIENT OUTPUT>
Ship global_position: (292.334, 22.0841, 724.584)
Character global_position: (292.334, 42.0841, 704.584)
Players 'on ready' global-position: (82.0633, 23.4845, 762.729)
Players 'on ready' position: (0, 0, 0)

After some further testing, it seems that if I add the child and immediately set the position, the host spawns correctly, the client spawns on ‘Players’ node.

If I add the child and then await ‘character.ready’ before moving, both the host and the client spawn on ‘Players’ node.

Current iteration of code:

func _ready():
	# Give authority over the player input to the appropriate peer.
	if not is_multiplayer_authority(): return
	# Set the camera as current if we are this player.
	if str(name).to_int() == multiplayer.get_unique_id():
		$Camera3D.current = true
		#var camera = $Camera3D.current
	capture_mouse()
	print("Players 'on ready' global-position: ", global_position)
	print("Players 'on ready' position: ", position)
	return
func add_player(id: int):
	# Instantiate a player and spawn it
	var character = preload("res://player.tscn").instantiate()

	# Set player id.
	character.player = id
	print(character.player)

	# Set player name
	character.name = str(id)
	
	# Get the position of the appropriate spawn point
	var spawnPoint = get_node("mship1").global_position
	print("Ship global_position: ", spawnPoint)
	$Players.add_child(character, true)
	# Wait untill the 
	if not character.is_node_ready():
		print("Node not yet ready, waiting")
		await character.ready
		# Move the player to that position
		character.global_position = Vector3(spawnPoint.x, spawnPoint.y + 20, spawnPoint.z - 20)
		print("Character global_position: ", character.global_position)
	else:
		character.global_position = Vector3(spawnPoint.x, spawnPoint.y + 20, spawnPoint.z - 20)
		print("Character global_position: ", character.global_position)
	return

Please tell me you got this working, I face the same exact issue in 4.3 - it seems I cannot override the position of client players.

It look like you need to some how include the id you gave them as you are targeting the first player, Idk how though because I could never get past the connection but i will still try and help

I’m afraid not, I ended up putting it down in the end and I’ve not gone back to it since. I might come back to it again, see if I can figure anything out with a fresh look

Hey, it’s me smarter by 3 days - I know exactly why this issue is happening but I have no idea how to fix it other than having server authoritive architecture in your project.

First of all - if you follow any guides made by this guy - it will solve your issue. He creates player prefabs with server authority, meaning that if you want to manipulate player in any way - only server can do that. He captures player’s input and monitors it with multiplayer synchronizers and rpc calls, that way server watches each client’s input and manipulates the object accordingly. The server can do whatever they want with player instances and clients don’t, like setting the position like in your case.

There’s a potential issue with this solution and that’s because it’s server side movement for everybody, including all clients. This might not be an issue for you depending on the game you’re making, pretty sure a game I played recently called Wild West does exactly that so that there are no suspicious movement desynchronizations and it works just fine.

The reason why this might be an issue and is for me is because it creates an input delay. The time it takes for the client to communicate with the server i.e. ping is the time it takes for the player to move after pressing a movement key. Usually this time is pretty low, like up to 50ms, but trust me - even 50ms delay feels awful in an FPP game I’m making. So, this solution doesn’t work for me, but maybe will work for you.

Other than that, I’m back to the drawing board cause I want to stick to the client authoratitive approach and somehow make it work.