Multiplayer Player Positioning

Godot Version

4.0

Question

Hey everyone, I simply created a multiplayer game with team selection function. When host selects team it spawns at correct Marker3D and prints correct location. When client selects team it prints correct location but it spawns at position of CharacterBody3D in Player scene. Can you help about it?

extends Node3D

@onready var main_menu: PanelContainer = $CanvasLayer/MainMenu
@onready var team_select_menu: PanelContainer = $CanvasLayer/TeamSelectMenu

@onready var address_entry: LineEdit = $CanvasLayer/MainMenu/MarginContainer/VBoxContainer/AddressEntry
@onready var spawn_1: Marker3D = $Spawn_1
@onready var spawn_2: Marker3D = $Spawn_2


const Player = preload("res://player.tscn")
const PORT = 9999

var enet_peer = ENetMultiplayerPeer.new()
var team_number
var mp_mode

func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("Quit"):
		get_tree().quit()


func _on_host_button_pressed() -> void:
	main_menu.hide()
	
	enet_peer.create_server(PORT)
	multiplayer.multiplayer_peer = enet_peer
	multiplayer.peer_connected.connect(add_player)
	multiplayer.peer_disconnected.connect(remove_player)
	
	team_select_menu.show()

func _on_join_button_pressed() -> void:
	main_menu.hide()
	mp_mode = 2
			
	team_select_menu.show()	

	
func _on_ct_button_pressed() -> void:
	if mp_mode == 2:
		team_select_menu.hide()
		team_number = 2
		enet_peer.create_client("localhost", PORT)
		multiplayer.multiplayer_peer = enet_peer
	else:
		team_select_menu.hide()
		team_number = 2
		add_player(multiplayer.get_unique_id())
		
		


func _on_t_button_pressed() -> void:
	if mp_mode == 2:
		team_select_menu.hide()
		team_number = 1
		enet_peer.create_client("localhost", PORT)
		multiplayer.multiplayer_peer = enet_peer
	else:
		team_select_menu.hide()
		team_number = 1
		add_player(multiplayer.get_unique_id())

func add_player(peer_id):
	var player = Player.instantiate()
	player.name = str(peer_id)
	add_child(player)
	
	print(team_number)
	
	if(team_number == 2): player.global_transform.origin = spawn_2.global_transform.origin
	if(team_number == 1): player.global_transform.origin = spawn_1.global_transform.origin
	
	print(player.global_position)
		
func remove_player(peer_id):
	var player = get_node_or_null(str(peer_id))
	if player:
		player.queue_free()

It probably relates to your authority setting on the MultiplayerSynchronizer.

( You are probably giving authority to the character on _ready, after spawn. Which makes the peer-authed MultiplayerSynchronizer send back its default child spawn position. )

You should create a custom spawn function that has the global position as a parameter, or change your network architecture to central authority. (Allowing only input to be sent back from remote peer players, and making all character MultiplayerSynchronizers authed to the host )

Thank you for your answer.

I am giving authority to the character on ;

func _enter_tree():
	set_multiplayer_authority(str(name).to_int())

How can i create that custom spawn function?

1 Like

In your add player node, add something like this. (There may be some bugs I just wrote this off the top of my head )

func _ready():
	$MultiplayerSpawner.set_spawn_function(my_spawn)


func add_player(peer_id):
	var spawn_pos = spawn_1.global_position
	if team_number == 2:
		spawn_pos = spawn_2.global_position

	$MultiplayerSpawner.spawn(peer_id, spawn_pos )
	
	

func my_spawn(peer_id:int, spawn_pos:Vector3) -> Node:
	var player = Player.instantiate()
	player.name = str(peer_id)
	print(team_number)

	# not sure global position will work if child is not added yet, maybe try just position? Although you will have to send a different spawn position from host
	player.global_position = spawn_pos 
	
	print(player.global_position)
	return player


Thank you for your answer again.

I tried your help but there is something ;

$MultiplayerSpawner.spawn(peer_id, spawn_pos )

spawn expects only 1 argument and that is data: Variant. I tried to update with it but i couldn’t make it. Can you help me again?

ah okay, we just need to package the data into a single variant, we can try a dictionary in this case for readability. an array could work too.

func _ready():
	$MultiplayerSpawner.set_spawn_function(my_spawn)


func add_player(peer_id):
	var spawn_pos = spawn_1.global_position
	if team_number == 2:
		spawn_pos = spawn_2.global_position

	$MultiplayerSpawner.spawn( {"peer_id":peer_id,  "spawn_pos":spawn_pos } )

func my_spawn(data:Dictionary) -> Node:
	var player = Player.instantiate()
	player.name = str(data.peer_id)
	print(team_number)

	# not sure global position will work if child is not added yet, maybe try just position? Although you will have to send a different spawn position from host
	player.global_position = data.spawn_pos 
	
	print(player.global_position)
	return player
1 Like

Thank you so much it solved my problem.

Actually i missed something. :frowning_face:
Client spawning at host location. I found this bug with using " print(team_number) ". Host prints team_number correctly, but client prints it 3 times. 1 time whatever host team_number is and 2 times client team_number. Can you help me about it ?

extends Node3D

@onready var main_menu: PanelContainer = $CanvasLayer/MainMenu
@onready var team_select_menu: PanelContainer = $CanvasLayer/TeamSelectMenu

@onready var address_entry: LineEdit = $CanvasLayer/MainMenu/MarginContainer/VBoxContainer/AddressEntry
@onready var spawn_1: Marker3D = $Spawn_1
@onready var spawn_2: Marker3D = $Spawn_2


const Player = preload("res://player.tscn")
const PORT = 9999

var enet_peer = ENetMultiplayerPeer.new()
var team_number
var mp_mode

func _ready():
	$MultiplayerSpawner.set_spawn_function(my_spawn)
	
	
func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("Quit"):
		get_tree().quit()


func _on_host_button_pressed() -> void:
	main_menu.hide()
	
	enet_peer.create_server(PORT)
	multiplayer.multiplayer_peer = enet_peer
	multiplayer.peer_connected.connect(add_player)
	multiplayer.peer_disconnected.connect(remove_player)
	
	team_select_menu.show()

func _on_join_button_pressed() -> void:
	main_menu.hide()
	mp_mode = 2
			
	team_select_menu.show()	

	
func _on_ct_button_pressed() -> void:
	if mp_mode == 2:
		team_select_menu.hide()
		team_number = 2
		enet_peer.create_client("localhost", PORT)
		multiplayer.multiplayer_peer = enet_peer
	else:
		team_select_menu.hide()
		team_number = 2
		add_player(multiplayer.get_unique_id())
		
		

func _on_t_button_pressed() -> void:
	if mp_mode == 2:
		team_select_menu.hide()
		team_number = 1
		enet_peer.create_client("localhost", PORT)
		multiplayer.multiplayer_peer = enet_peer
	else:
		team_select_menu.hide()
		team_number = 1
		add_player(multiplayer.get_unique_id())

func add_player(peer_id):
	var spawn_pos = spawn_1.global_position
	if team_number == 2:
		spawn_pos = spawn_2.global_position

	$MultiplayerSpawner.spawn( {"peer_id":peer_id,  "spawn_pos":spawn_pos } )
		
func remove_player(peer_id):
	var player = get_node_or_null(str(peer_id))
	if player:
		player.queue_free()

func my_spawn(data:Dictionary) -> Node:
	var player = Player.instantiate()
	player.name = str(data.peer_id)
	print(team_number)

	call_deferred("set_player_position", player, data.spawn_pos)
	
	return player

func set_player_position(player: Node3D, spawn_pos: Vector3) -> void:
	if player.is_inside_tree():
		player.global_position = spawn_pos

So you probably need to rpc that team choice, of t or ct, to the host that adds the player.

And your current code just adds the player when they connect.

You probably need to have a connect button that joins the server first. Then the user can select a team, which will call an RPC the team choice back to the host, that will eventually trigger the player spawn function. (There are other, and many, ways to go about this, the important thing is you need to get the peer information, on team choice, back to the host, or peer, that will spawn the player.)

To start we need to remove remove: multiplayer.peer_connected.connect(add_player) but, we can just print that a player is connected with a lamda function, until you may need it again.

multiplayer.peer_connected.connect( func( id ) : print( "connected: ",  id ) )

You will need to then devise an RPC function that peers can call once connected. this function will send the team choice back to the server.

@rpc("any_peer", "call_local", "reliable")
func add_player_to_team( team : int ) -> void:
  if not multiplayer.is_server():
    return
  team_number = team
  add_player( multiplayer.get_remote_sender_id() )
   

last thing we need is to hook it into your menu options, to call the RPC all we need to do is add_player_to_team.rpc_id( 1, team )

func _on_ct_button_pressed() -> void:
	team_select_menu.hide()
	team_number = 2
	# host can rpc it itself
	add_player_to_team.rpc_id(TARGET_PEER_SERVER, team_number)

I did optimize this as your mp_mode was to call add player in different ways, and is not needed, because host can call an RPC onto itself. allowing for the code to be reused.

final code:

extends Node3D

@onready var main_menu: PanelContainer = $CanvasLayer/MainMenu
@onready var team_select_menu: PanelContainer = $CanvasLayer/TeamSelectMenu

@onready var address_entry: LineEdit = $CanvasLayer/MainMenu/MarginContainer/VBoxContainer/AddressEntry
@onready var spawn_1: Marker3D = $Spawn_1
@onready var spawn_2: Marker3D = $Spawn_2


const Player = preload("res://player.tscn")
const PORT = 9999

var enet_peer = ENetMultiplayerPeer.new()
var team_number
var mp_mode

func _ready():
	$MultiplayerSpawner.set_spawn_function(my_spawn)
	
	
func _unhandled_input(event: InputEvent) -> void:
	if Input.is_action_just_pressed("Quit"):
		get_tree().quit()


func _on_host_button_pressed() -> void:
	main_menu.hide()
	
	enet_peer.create_server(PORT)
	multiplayer.multiplayer_peer = enet_peer
	multiplayer.peer_connected.connect( func( id ) : print( "connected: ",  id ) )
	multiplayer.peer_disconnected.connect(remove_player)
	
	team_select_menu.show()

func _on_join_button_pressed() -> void:
	main_menu.hide()
	# connect client here
	enet_peer.create_client("localhost", PORT)
	multiplayer.multiplayer_peer = enet_peer
			
	team_select_menu.show()	

	
func _on_ct_button_pressed() -> void:
	team_select_menu.hide()
	team_number = 2
	# host can rpc it itself
	add_player_to_team.rpc_id(TARGET_PEER_SERVER, team_number)


func _on_t_button_pressed() -> void:
	team_select_menu.hide()
	team_number = 1
	# host can rpc it itself
	add_player_to_team.rpc_id(TARGET_PEER_SERVER, team_number) 

### new
@rpc("any_peer", "call_local", "reliable")
func add_player_to_team( team : int ) -> void:
	if multiplayer.is_server():
		team_number = team
		add_player( multiplayer.get_remote_sender_id() )

func add_player(peer_id):
	var spawn_pos = spawn_1.global_position
	if team_number == 2:
		spawn_pos = spawn_2.global_position

	$MultiplayerSpawner.spawn( {"peer_id":peer_id,  "spawn_pos":spawn_pos } )
		
func remove_player(peer_id):
	var player = get_node_or_null(str(peer_id))
	if player:
		player.queue_free()

func my_spawn(data:Dictionary) -> Node:
	var player = Player.instantiate()
	player.name = str(data.peer_id)
	print(team_number)

	call_deferred("set_player_position", player, data.spawn_pos)
	
	return player

func set_player_position(player: Node3D, spawn_pos: Vector3) -> void:
	if player.is_inside_tree():
		player.global_position = spawn_pos

I’m hoping you can just copy paste this, I did verfiy some aspects locally.

1 Like

Thank you so much it solved my problem.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.