Changing position isn't working correctly

Godot Version

4.3

Question

Im new to Godot but can’t understand the following:
I have a gun script which emits the hit player.

the player script connects this signal correctly. it looks like this:

player.gd:

func receive_damage_pre_function(player_id):
	rpc("receive_damage", player_id)

@rpc("any_peer")
func receive_damage(player_id):
	var local_id = multiplayer.get_unique_id()
	if player_id == local_id:
		health -= 1
		print("Player: ", player_id, " has left: ", health)
		if health <= 0:
			print(player_id, " is dead")
			health = 3
			label_you_are_dead.show() 
			position = Vector3.ZERO   

I have a MultiplayerSynchronizer connected to this Player scene.
The script itself is actually working fine.
When Player 1 (shooter) shoots at Player 2 (target), Player 2’s ID is transmitted into the function and health is going down until he should respawn. but then the position change isnt done. When I change the position of MultiplayerSynchronizer to “only when changes happen” i can actually see that Player 1’s position is changed, but only to Player 2’s perspective, also the label is shown at the Shooter from Player1’s perspective.

However, when I place a get_tree().quit() after the respawn, the correct player’s game shuts down…I dont get it.
I hope you can understand my problem. Im still new so if you need any information I will provide them.

It would help to know how you intend authority to be set, and how you want your guns to function over the net.

And how you are guarding with is_server() and/or is_multiplayer_authority(). ie are signals happening on every peer? How is this playing out from each players perspective?

im not sure what you mean with authority to be set.

so the signals are global and sent to every peer. my thoughts are that sending the function to every peer and every peer is checking if the hit player id from the gun is their own Id. therefore losing health. Ive added some console outputs and it checks out in every way. the function is triggered on every peer other than the one who fired the gun and the one who got hit loses health. but somehow the position (which ive also put out in the console log if it sets the correct player back) isnt respawning the hit player but the one firing the gun. but it should be working since shutting down the game does work on the one getting hit so it is triggered correctly.

Edit:
It’s meant to be a Peer to Peer Connection

One Player hosts the game

func _on_host_button_pressed() → void:
main_menu.hide()
enet_peer.create_server(PORT)
multiplayer.multiplayer_peer = enet_peer
multiplayer.peer_connected.connect(_on_peer_connected)
var host_ip = get_local_ip()
add_player.rpc(multiplayer.get_unique_id())
SignalBus.emit_signal(“host_button_gedrueckt”)
for peer in multiplayer.get_peers():
rpc_id(peer, “_receive_host_ip”, host_ip)

@rpc(“any_peer”, “call_local”)
func add_player(peer_id: int) → void:
if has_node(str(peer_id)):
return

If this helps

Peer to peer as in the game trusts clients? Or peer to peer as in the more complicated (than I assumed this was) architecture where every peer is connected to every other peer? RPCs seem like they allow peers to communicate directly with other peers but its still server-client architecture, unless you specifically create a mesh network. (As far as I understand it.)

Its important to know how exactly your code is flowing on every peer. What I mean is what is deciding to hurt players, and is it guarded with is_multiplayer_authority()? If not, its possible each peer is running code that invokes the RPC to hurt the target player. P1 shooting P2 might be causing P1 to try and hurt P2, as well as P2 to try and hurt P2, which would make debugging more confusing.

Yes, every peer is connected to every other peer.
I will provide as much info as possible.

this is my Pistol script:
It just checks if the client shooting owns the gun. (Otherwise every gun in the game would shoot at the same time.) and it emitts the shot player id via a signal.

extends StaticBody3D

@onready var pistol_shoot_sound = $Pistol/AudioStreamPlayer3D_Pistol
@onready var pistol_animations: AnimationPlayer = $Pistol/Pistol_Animation
@onready var muzzle_flash: GPUParticles3D = $Pistol/Muzzleflash

var bullet_raycast = null
var pistol_equipped = false


func _input(event):
	if event.is_action_pressed("shoot"):
		var owner_id = get_meta("owner_id", -1)
		if owner_id == multiplayer.get_unique_id():	
			rpc("fire")

@rpc("any_peer","call_local")
func fire():
	if pistol_animations.current_animation != "shoot":
		rpc("play_shoot_effects_rpc")
		var owner_id = get_meta("owner_id", -1)
		var raycast = SignalBus.get_bullet_raycast(owner_id)
		if raycast and raycast.is_colliding():
			var hit_object = raycast.get_collider()
			if hit_object != null:
				print("🎯", hit_object.name, "getroffen")
				if hit_object.is_in_group("Players"):
					var player_id = hit_object.get_multiplayer_authority()
					SignalBus.emit_signal("player_hit", player_id)
			else:
				print("❌no hit")
		else:
			print("❌no hit")


@rpc("any_peer", "call_local")
func play_shoot_effects_rpc():
		pistol_shoot_sound.play()
		pistol_animations.play("shoot")
		muzzle_flash.restart()
		muzzle_flash.emitting = true

then there is a signalbus script which is globally available.
signal player_hit(player_id)

And at last a snippet out of the player script:

func _ready():  
	SignalBus.player_hit.connect(receive_damage_pre_function)

func receive_damage_pre_function(player_id):
	rpc("receive_damage", player_id)

@rpc("any_peer")
func receive_damage(player_id):
	var local_id = multiplayer.get_unique_id()
	if player_id == local_id:
		health -= 1
		if health <= 0:
			print(player_id, " died")
			health = 3
			get_tree().quit() # ive added it to check if it would shut down the correct peer
			label_you_are_dead.show() 
			position = Vector3.ZERO

If needed i can post the whole Player script here. changing position actually works in the
func _physics_process(delta: float) → void function.

I do not understand very well but, it seems like Vector3.Zero is the center of the world? Should you use global_position?

I’ve tried it globally as well. I really don’t get it why it doesn’t work…

ive made a video to demonstrate this behaviour, (left is Host, Peer ID 1, and right is client) basically i wanted to show a red screen when beeing shot, but it is only shown to the one shooting and only from the other’s perspective. The logs are perfectly correct. Anyway, the spawn_ghost triggers corretly at player 1, but probably because its in another script.

func receive_damage_pre_function(player_id):
print("receive_damage_pre_function called by: ",multiplayer.get_unique_id())
print("local ID: ",multiplayer.get_unique_id(), " received player_id from pistol script: ", player_id)
rpc_id(player_id,“receive_damage”, player_id)

@rpc
func receive_damage(player_id):
print(“receive_damage was called by peer: “, multiplayer.get_unique_id())
var local_id = multiplayer.get_unique_id()
print(player_id, " reached receive_damage”)
print(local_id, " is the peer ID”)
if player_id == local_id:
print(multiplayer.get_unique_id(), " is the correct peer in which this function should happen locally")
health -= 1
damage_sprite.show()
print("Player: ", player_id, " has left: “, health)
if health <= 0:
print(player_id, " died”)
position = Vector3.ZERO
rpc(“spawn_ghost”, player_id)

@rpc(“any_peer”)
func spawn_ghost(player_id):
SignalBus.emit_signal(“spawn_ghost_player”, player_id) # Globales Signal senden

I think the setup is a little confusing, it might help to refactor a little bit but its just a suggestion.

Are you using owner_id to keep track of which peer controls objects? You can do that with set_multiplayer_authority(peer_id) and when an object is picked up by a player: set_multiplayer_authority(player_obj.get_multiplayer_authority())

@rpc("any_peer", "call_local")
func fire():
    ...

If we set weapons to have the authority of the peer who owns them as above, then this RPC would be @rpc("authority", "call_local") so that only the player who owns it can fire it. Having it be callable by anyone but then filter by owner id inside feels like a workaround.

You can also be more direct with RPC calls, but maybe you need them to route through signalbus. For example:

@rpc("authority", "call_local")
func shoot():
    ...
    if raycast && raycast.hit_object.is_in_group("Players"):
        raycast.hit_object.take_damage.rpc(amount)

in player:

#call local if effects of this func should be visible on attackers machine
#maybe we spawn some effect or player health should be synced
@rpc("any_peer")
func take_damage(amount):
    var attacker_id = multiplayer.get_remote_sender_id()
    ...

The bug might be from the nonstandard way you are deciding authority and how you then set up RPCs.

Im using META Datas to to assign picked up items to the one holding it, after you pick it up, the owner Meta of the objekt sets to the peer id


@rpc("any_peer", "call_local")
func pick_up_item(item_name: String, texture: Texture):
	
	var slots = [item_name_slot_1, item_name_slot_2, item_name_slot_3, item_name_slot_4]
	var free_slot = -1
	
	var base_name = get_base_item_name(item_name)

	if base_name in item_scenes:
		var new_item = item_scenes[base_name].instantiate()
		new_item.set_meta("original_name", item_name)
		new_item.set_meta("owner_id", multiplayer.get_unique_id())   
	
	

	for i in range(slots.size()):
		if slots[i] == null:  
			free_slot = i
			break
	

	if free_slot == -1:
		print("❌")
		return
	
	match free_slot:
		0:
			item_name_slot_1 = item_name
			$Hotbar/HBoxContainer/item_slot_1.texture = texture
		1:
			item_name_slot_2 = item_name
			$Hotbar/HBoxContainer/item_slot_2.texture = texture
		2:
			item_name_slot_3 = item_name
			$Hotbar/HBoxContainer/item_slot_3.texture = texture
		3:
			item_name_slot_4 = item_name
			$Hotbar/HBoxContainer/item_slot_4.texture = texture
	

	if is_instance_valid(pick_up_object): 
		var object_path = pick_up_object.get_path()
		rpc("remove_object", object_path)
	else:
		print("❌")
func instantiate_item(item_name: String, player_id: int):
	print("📌 instantiate_item called by Player:", player_id, " Item:", item_name)

	var base_name = get_base_item_name(item_name)

	if base_name in item_scenes and item_scenes[base_name] != null:
		var item_scene = item_scenes[base_name]
		var new_item = item_scene.instantiate()
		if new_item == null:
			print("❌")
			return

		new_item.set_meta("original_name", item_name)
		new_item.set_meta("owner_id", player_id) 
		new_item.name = item_name  
		
		$nek/head/eyes/Camera3D.add_child(new_item)
		adjust_item_position(new_item)

		player_held_items[player_id] = new_item

		print("✅ Item instantiated for:", player_id, " Besitzer:", new_item.get_meta("owner_id"))
	else:
		print("⚠️")

Ok so I’ve changed it to Multiplayer Authority instead of working with owner_id in Meta data…but the error is the same.


func receive_damage_pre_function(player_id):
	print("receive_damage_pre_function called by: ",multiplayer.get_unique_id())
	print("local ID: ",multiplayer.get_unique_id(), " received player_id from pistol script: ", player_id)
	rpc_id(player_id,"receive_damage", player_id)

@rpc
func receive_damage(player_id):
	print("receive_damage was called by peer: ", multiplayer.get_unique_id())
	var local_id = multiplayer.get_unique_id()
	print(player_id, " reached receive_damage")
	print(local_id, " is the peer ID")
	if player_id == local_id:
		print(multiplayer.get_unique_id(), " is the correct peer in which this function should happen locally")
		health -= 1
		_damage_sprite()
		print(player_id)
		print("Player: ", player_id, " has left: ", health)
		if health <= 0:
			print(player_id, " died")
			position = Vector3.ZERO
			rpc("spawn_ghost", player_id)


func _damage_sprite():
	print("damage sprite is called locally on client: ", multiplayer.get_unique_id())
	damage_sprite.show()



	if Input.is_action_just_pressed("debug"):
		_damage_sprite()

ive also added a debug key to just test the function locally
And it works as intended, just not in the RTP case.

Pistol Script:

extends StaticBody3D

@onready var pistol_shoot_sound = $Pistol/AudioStreamPlayer3D_Pistol
@onready var pistol_animations: AnimationPlayer = $Pistol/Pistol_Animation
@onready var muzzle_flash: GPUParticles3D = $Pistol/Muzzleflash

var bullet_raycast = null
var pistol_equipped = false

func _input(event):
	if event.is_action_pressed("shoot"):
		var owner_id = get_multiplayer_authority()
		if owner_id == multiplayer.get_unique_id():
			rpc("fire")

@rpc("any_peer", "call_local")
func fire():
	if pistol_animations.current_animation == "shoot":
		return

	print("🔥", self.name, "FEUERT!")
	rpc("play_shoot_effects_rpc")

	# Hole die Multiplayer-ID des Schützen
	var owner_id = get_multiplayer_authority()

	# Hol den richtigen Raycast für diesen Spieler aus dem SignalBus
	var raycast = SignalBus.get_bullet_raycast(owner_id)
	if raycast and raycast.is_colliding():
		var hit_object = raycast.get_collider()
		if hit_object != null:
			print("🎯", hit_object.name, "getroffen")
			if hit_object.is_in_group("Players"):
				var player_id = hit_object.get_multiplayer_authority()
				SignalBus.emit_signal("player_hit", player_id)
		else:
			print("❌ Kein Treffer!")
	else:
		print("❌ Kein Treffer! (Raycast hat nichts getroffen)")

@rpc("any_peer", "call_local")
func play_shoot_effects_rpc():
	pistol_shoot_sound.play()
	pistol_animations.play("shoot")
	muzzle_flash.restart()
	muzzle_flash.emitting = true

Im really confused right now lol
Honestly if you like to I can upload this whole project and you can have a look into it. Im struggeling to fix this since 2 weeks.

EDIT: So I think it has something to do with how the authority is handled. When I press

	if Input.is_action_just_pressed("debug"):
		_damage_sprite()

when a client presses _damage_sprite(), it shows the damage sprite to all players at least from the clients perspective, from the other’s perspective its not there. so I think slowly i get the hang of the problem. Its kinda like a client tries to change a node from the other clients/hosts but isnt allowed to. So basically everyone is using the same player scene, and if you change a node at least locally it is synchronized to the others but only visible to the one changing it. Ive managed to solve this one by adding the visibility to the multiplayersynchronizer. but im still unable to do it in the rtp function

The annotation for rpc functions would be “authority” for functions that the owner (authority) would perform, even if that thing should happen on each machine.

This is because authority is the same as owner as you had it. Server is always id 1, and nodes start with “owner” (authority) set to 1. But we let players “own” their items and set the authority to their peer id, and then annotate certain functions with “authority” and “call_local” same as it it were server owned.

The very nature of calling some RPC, eg shoot.rpc() means it should happen for everyone. Putting “any_peer” on shoot isn’t necessarily wrong, it’s just saying that anyone can shoot any gun, even if they don’t own it. Even if we trust peers it helps to restrict things so there’s less chance of bugs.

I’d be willing to take a look at the project, I have a potato laptop but it should be fine.

ok so I’ve solved this problem by emitting a global signal.
basically i emit a signal from the player.gd script which the player.gd script itself connects to a function.

so player.gd in rdy:

func _ready():
SignalBus._damage_sprite.connect(_damage_sprite)

then i have a globalbus.gd script, which i added to autoload in the project settings.To make it global.

signal _damage_sprite(player_id)

and again in player.gd i emit this signal when a player gets hit:

SignalBus.emit_signal(“_damage_sprite”, player_id)

and there is the connected function from the signal in player.gd:

func _damage_sprite(player_id):
if multiplayer.get_unique_id() == player_id:

  damage_sprite.show()  

Thank you all for your time. this works best for me.

1 Like