MultiplayerSpawner's spawns unable to apply properties. Bug, or my code?

Godot Version

4.4 Beta 4

Question

Hi, all. I am making a game where you can place cards and they will move toward the enemy’s towers, very similar to Clash Royale. Currently, players can enter a match with another player, and the server.

The server is able to spawn the troops with multiplayerspawner, and everything looks great, until it’s time to shoot. The tanks will see each other, shoot, and boom, the clients crash. Using breakpoints, it seems that on the host, everything works fine; the tank instantiates the bullet node, and applies values to the instantiated bullet’s variables, like it’s target and damage.

However, the clients just crash after adding the bullet as a sibling! The tanks instantiate the bullet, but they do not apply any values to the variables!! It is all null. Target = null, damage = null, everything is null. However, it still will finish, and add_sibling(bulletinstance). It will then crash, because “target” is nil.

func _on_attack_time_timeout() -> void:
    #print("attack time timeout happened, attempting to do takedamage()")
    #closest_enemy_in_attack_range.TakeDamage(Damage, "Melee")
    var bulletinstance = slinger_bullet.instantiate()
    bulletinstance.projectiledamage = Damage
    if is_instance_valid(closest_enemy_in_attack_range) == false:
        Get_Closest_Enemy_In_Attack()
    bulletinstance.target = closest_enemy_in_attack_range
    bulletinstance.position = self.global_position
    add_sibling(bulletinstance)

Are you running the code to add a sibling on each peer or only the server? When you see nulls and the crash on clients, is that the Spawner’s automatic rpc calls?

1 Like

Not able to check my computer right now, but I’m pretty sure that the peers’ Tanks are also trying to add_sibling(bulletinstance).

I don’t believe they’re rpc calls, iirc it’s the bulletinstance throwing the error once they are added in the scene with add_sibling.

Is the solution just to have the function check if it’s the server, and if not, then pass? That way the server can spawn them with multiplayerspawner? When I get back home I can provide more details. Thank you.

If the server is meant to be doing everything then yeah you could use is_server which is true when running on peer 1 (server id) , and also is_multiplayer_authority, as nodes by default have authority set to 1.

1 Like

Still having trouble, even when I use that method. I also have tried passing through the properties using an rpc and I still get the same result…Is there any info I can give that’ll help? Are the multiplayer nodes even worth it or are they complete shit?

Oh thats bad, dont send objects via rpc.

It all comes down to authority and anti-cheat measures. If the player wants to spawn a bullet the server should be the only one to do so. The only authority given to the client is input.

Now you can do a peer-to-peer network topology but it is a little harder to pull off in Godot.

Anyway if you use a MultiplayerSpawner the authority of the spawner should be the one to create and add the node to the tree. The spawner will spawn it for the other peers.

Now you got to be smart about how to set properties after the spawn on remote peers. You can use a MultiplayerSynchronizer with on spawn replication options to configure the state after the spawn. Or you can modify your workflow to use a custom spawn function on the spawner to set the properties before the spawn. (On spawn option of a synchronizer might do this already, i haven’t fully parsed that part of the SceneMultiplayerAPI)

When a multiplayer spawner spawns a node it is at its default state. At its core the spawner just instantiates the scene and sets its name. Nothing else. The custom spawn function allows you to associate some data for the spawn of the node, but the majority of the responsibility for setting properties is the MultiplayerSynchronizer. These two nodes work in tandem under the SceneMultiplayerAPI.

1 Like

I was just doing it that way to see if I could set properties, but that way didn’t work out either :frowning:
Anyway, I tried using multiplayer synchronizer already but it doesn’t sync variables like target…why? And how do I make it sync that? I tried @export but it still wouldn’t show up in properties… I’ve read the manual and I couldn’t find anything about why it isn’t allowed. I did @export var target : Node3D

1 Like

When the Synchronizer is active you should see a “Path:” text box above the fields its syncing. Just copy/paste a variable from the script there and press ‘Add from path’.

So as for whether or not the Multiplayer nodes are useful… its easier to do away with the Synchronizer because you want to minimize network traffic and its easy to start packing state / do a little prediction / interpolate between updates.

You don’t always need the Spawner to synchronize spawns / despawns. For example, you want a bullet to spawn, have arbitrary initialization, and then do its thing on each machine. At some point you need to guard the flow of execution with is_server(). When the tanks see each other and shoot – that should probably only be happening on the server. (Clients can handle turret turning, sounds, anything that doesn’t really matter.) But the code should eventually have if !is_server(): return #we're done here and then afterwards, you check LOS, decide to spawn and bullet, and…

You have a function that spawns and sets up the bullet, annotated with @rpc("authority", "call_local", "reliable"). ‘Authority’ doesn’t necessarily mean server, it means who has authority over the node, which is the server by default. Call local makes it so the code is executed on the machine that called the function. By setting authority to 1 (or keeping it 1), annotating with “authority” and “call_local”, we can have code that executes on every machine and only the server can initiate it.

func _physics_process(delta: float) -> void:
    # common update logic for tanks
    ...
    # you can have remote only processing
     if !is_multiplayer_authority():
        remote_update(delta)
        return # early return, everything below is running only on authority

    # authority only processing
    ...
    if bullet_firing_conditions:
        spawn_bullet.rpc(target, damage, ...)
    return

A bullet can be treated the same way. It can be predicted remotely, but controlled by the authority. When it collides, make sure only the authority is applying damage. You could (for example) let any machine play impact effects, start a timer, and queue_free() the bullet.

2 Likes

Im not sure what target is in this case, but if it is a Node/Object/Resource you will be out of luck, for good reasons.

This would be a different topic to discuss unless this is what you are trying to solve?

In one sense you let the server/authority do the target search and have other mechanism show any visual aspects.

In the simplest sense to have all peers do the same thing you can pass NodePaths between peers and they do searches for the target. There are also instance IDs, but i dont know if its possible to keep those in sync between processes.

The more complicated approach would require a deep understanding of the system that is architected in a way that can easily synch state via native types.

I’m back, and I will tell you what I’ve done. I have made it so that the server does all of the work, and when it’s time for the tanks to shoot, they VISUALLY shoot for the client, but there isn’t actually any logic, and the bullet’s position is synced using MultiplayerSynchronizer!!

The server simply runs the shoot function, checks if it’s the server (True), applies the properties, (INCLUDING the new one, “is_Visual”, which it sets to false), and adds sibling. Then, when the bullet gets spawned with multiplayerspawner, and has all default values, is_Visual value is set to true by default, which makes the bullet script not run anything!!

Unsure if this method is good practice, but man, it works very well so far. Thank you all so much.

1 Like

I can appreciate the technique. Do you use on on spawn setting in the replication config for is_visible?