Issue with MultiplayerSpawner:

Godot Version

4.5.1

Question

I am trying to spawn a bullet that is then spawned and synchronized between all players.

This works fine for the host, and clients can see the host’s bullets just fine. However, an error fires every time a bullet is fired on both host and clients, and bullets fired by clients are warped position 0,0,0 and then move through the ground extremely slowly.

Here is the code on the gun responsible for firing the bullet.

extends Node3D

var speed = 200

@onready var spawner: MultiplayerSpawner = $MultiplayerSpawner
@onready var spawn_location = get_tree().root.get_node("Main/GameWorld")

@export var projectile : PackedScene

func _ready() -> void:
	spawner.spawn_path = spawn_location.get_path()

func fire():
	var bullet = projectile.instantiate()	
	var camera = get_parent()
	bullet.global_transform = camera.global_transform
	bullet.speed = camera.global_transform.basis.z * speed
	spawn_location.add_child(bullet, true)

The error it returns is

E 0:01:43:988   test_gun.gd:19 @ fire(): Condition "tobj.spawner != ObjectID()" is true. Returning: ERR_ALREADY_IN_USE
  <C++ Source>  modules/multiplayer/scene_replication_interface.cpp:164 @ on_spawn()
  <Stack Trace> test_gun.gd:19 @ fire()
                player.gd:60 @ _physics_process()

I… don’t really know what that means. Can anyone help me decipher this or tell me what I’m doing wrong?

Godot Version 4.6

Hi! Have you been able to sort this issue out? I am now facing it too. The only similarity I manage to see between your setup and mine is that we both, in some way, are affecting a spawn_path property:

extends Player


func _enter_tree() -> void:
	# [...]
	var character_spawner: MultiplayerSpawner = (load("uid://1krxvcsbbbq8") as PackedScene).instantiate()
	character_spawner.set_multiplayer_authority(get_multiplayer_authority())
	world.add_child(character_spawner) # [world] is a global


func _ready() -> void:
	if is_multiplayer_authority():
		add_character()


func add_character() -> void:
	var new_character = Humanoid3D.instantiate() # [Humanoid3D] is a custom class
	new_character.name = name
	world.add_child(new_character, true)

In my case, the exception is only fired on the host and I haven’t really checked if it causes any visible issues. This is how the error is described in the engine source code:

// Spawn state needs to be callected after "ready", but the spawn order follows "enter_tree".

Between the fact that “calleted” seems like a typo and that source comments tend to be ambiguous, I cannot make sense out of this without C++ knowledge.

Okay, after a little of help, I have a better intuition of this error’s nature. Correct me if I’m wrong.

ERR_FAIL_COND_V(tobj.spawner != ObjectID(), ERR_ALREADY_IN_USE);

This line of code acts as a guard to prevent spawning nodes from already being assigned to a spawner, setting just one spawner as the one in control. This “control” concept is important, remember spawned nodes disappear when their spawner disappears or disconnects from the multiplayer session.

Regarding your specific case, you have, effectively, multiple spawners pointing to a same spawn path: each gun has its own spawner pointing to spawn_location, "Main/Gameworld”. Presuming they all belong to different authorities (the player), that still doesn’t take away the fact that all spawners are visible in each client’s SceneTree.

No matter who sends the spawn data, the scenes are still instantiated locally the same way any other would and thus, the engine recognizes that a spawnable scene is being spawned by another spawner, so it shouldn’t try to spawn it too. It’s a guard to prevent a spawning loop, clever :sweat_smile: .

This error appearing is probably a sign that the engine is saving you from making a great mistake but still happens near the end of an important callback, so we should not discard that your warped position and velocity issue might be caused by it. The best solution I envision is creating, from a single spawner owned by the host, containers for each peer and their spawnable scenes. So:

var spawn_location = get_tree().root.get_node("Main/GameWorld/Player[X]")

Thank you, flamenco687! I came across your comment while searching for something similar. That was a big help for my case. I was also editing the spawn_path property, and that resulted in the same issue - multiple spawners using the same path. Even though they all had different authorities, and the entities had different names, there was still the ERR_ALREADY_IN_USE conflict.

Moving each Player’s spawn path to a unique sub-node in the level for each Player, as you suggested, works a treat.

Although, I do still have an issue with editing the spawn_path - For example, if I have the Players each get the Gameworld, create a sub node under that with their own unique name for their spawned Guns, and attach their spawned and owned Guns to that node. When the Client receives the Host’s spawned Guns, they are created on the Client at the default, unchanged spawner path that the Spawner originally pointed to (in my case, the Player’s node itself). And thus, they don’t sync, because they have different paths on the Client and Server! It seems like the Host’s guns spawn on the Client at that original, temporary path before it gets updated in the Player’s _ready function.

I think what I might do is have an array of unique spawn paths and MultiplayerSpawners set up permanently in the Gameworld, and have the Host assign usage of them out to each Player as they join the match. That way I don’t have to edit the spawn_path.

I think I understand your issue but it seems simple to solve? It should be enough to have spawners point to themselves and directly instance nodes under them sincespawn_pathworks with relative NodePaths indeed.

I should have added that, since the writing of my first reply I’ve learned better the ropes of Godot multiplayer. I don’t know specifically your approach but you only really need one node to be spawned by the host for the player. Then, without further spawners, all clients can instantiate expected nodes of a player.

e.g.: You don’t need the host to spawn a spawner for every player “guns”. If you already have a player node, spawned by the host, you can just instantiate the guns container from the script attached to the player, since all clients execute all shared code unless explicitly stated. This way you save not just on complexity but also performance.

What made it a little tricker was that my ‘gun’ nodes needed to be totally separate from the players (not a child), so I was trying to have the Spawner under the Player node but then point to a new path in the owning level at runtime.

Still, you have helped me again - I have a bit more understanding about how the replication works by path, so I don’t need to RPC the spawn as long as the objects have MultiplayerSynchronizers and are on identical paths in all clients. I had actually tried that approach earlier but I had run into an issue that made me think I needed to use a Spawner or RPCs.

The issue I ran into was an odd race condition issue where I had a one-frame delay on the Players creating their gun nodes, which meant the clients would receive replication for the Host’s gun one frame before they had actually created them on their end, and this would totally stop the host’s gun from syncing on the client even after the client had created it on the same path.

I just reworked it to remove the one-frame delay on gun creation and that did the trick!

I guess this approach only works if the sub-objects are created at the same frame that the owner is first replicated. I will probably have to revisit the permanent per-player GameSpawners in the Gameworld idea if I eventually have client-owned objects that need to spawn at runtime.