How to sync Node reparenting MultiplayerSynchronizer

Godot Version

v.4.4.1

Question

Hi everyone, I am trying to synchronize my pick-up action across all clients. The Item I am trying to pick up has a MultiplayerSychronizer which syncs position and rotation. I am unable to understand how to synchronize the parent Node.

When picking up the item I simply call a function try_pick_up which is along the lines of:

func try_pick_up(item: Item) -> void:
      if player.has_main_hand_item():
            return
      item.reset_transform()
      item.reparent(main_hand, false)

The Item is correctly synchronized until the time I pick it up. Once picked up by one of the clients it de-syncs (I assumed it was linked to the SceneTree not being identical amongst all clients).

After further investigation at run-time, I indeed confirmed that the SceneTree is not the same between clients. The one client who has picked up the object correctly has the item under its main_hand node, but this does not sync for other peers.

I tried playing around with @rpc annotation, but I have got to admit that I still can’t grasp how I am supposed to use @rpc at its fullest. Thinking about it I don’t think I should make try_pick_up an rpc call, rather sync the parent of the item object, but that’s not an option under the MultiplayerSynchronizer sync-able parameters list. So how do I do this? :slight_smile: - I smile not to cry at this point -

I would put a multiplayerspawner in the player hand. And also have a multiplayerspawner watching all the pick up items. Ive never tried reparent but the multiplayerspawner listens for child added removed signals, so ithink it will be fine.

Rpc and multiplayer synchronizers are not meant to be used together like this when moving nodes. Spawners and synchers need to be used together for this.

1 Like

I’ve also thought of this solution but then I was wondering if we really don’t have any better approach.
I mean once you start adding a MultiplayerSpawner to all the entities who can pick up an object and all the ones who can store one (i.e. a table to which you want to parent something as a sort of visual storage), in my head it starts becoming quite a heavy/demanding solution just to keep track of a Node’s parent. That’s why I thought: there must be a better way, no?

But if that’s not the case, I guess I’ll have to do it this way. Thank you :slight_smile:

Tip: If you use the custom spawn you dont need to add the scene to the spawn list.

The multiplayer nodes share responsibilities for the replication config and track when a remote scene has been requested to spawn.

RPCs have their own replication configuration and do not interact with the multiplayer nodes code base. You could write your own scene replication with RPCs, but that is a different matter.

Everything is happening in c++ so it will be as fast as it can be. And spawners only react to node entry and exit. So unless your doing node movements 1000 time a second i wouldnt worry about it.

1 Like

So the custom spawner works like a charm and I don’t need to add any item to the list beforehand which is amazing, but I was wondering:

Do you know how it actually works? I can’t really follow the logic, to be honest.

So let’s suppose I have a class A like this - let’s assume, for the sake of my sanity, that we have a reference to custom_spawner which is of type CustomMultiplayerSpawner - :

class_name A
extends Node

var res: String = "res://some_resource/resource.tscn"

func do_something() -> void:
	custom_spawner.spawn(res)

Now let’s suppose the CustoMultiplayerSpawner class is as follows:

class_name CustomMultiplayerSpawner
extends MultiplayerSpawner

func _ready() -> void:
	spawn_function = custom_spawn_function

func custom_spawn_function(res: String) -> Node:
	var node: Node = load(res).instantiate()
	return node

Now, let’s also assume we spawn res in the game world and we want a class B to interact with this Node to reparent it under a different MultiplayerSpawner. The “optimal” - I believe - way is to avoid reparenting and simply create a duplicate of this instance, so let’s say we have:

class_name B
extends Node

func move_instance(hit: Node) -> void:
	# I assume hit to be the result of a raycast hit
	var new_node: Node = hit.duplicate()

	# prefab_path returns the "res" path mentioned in class A - just to be clear
	custom_spawner.spawn(new_node.some_custom_field.prefab_path)

	# I just change the position to explain the point I want to get to
	new_node.position = Vector3(55.0, 234.0, 99.75)

	hit.queue_free() # to remove it from the current holder spawner

What I can’t wrap my head around is this. On the Host we have the original resource instance. Ok and it makes sense.
The MultiplayerSpawner will sync it across all peers. The custom spawn function does not accept a Node - already strange since it accepts a Variant, but after some tries it didn’t work for me by passing a Node directly.
So my solution was to pass the path and then instantiate a new Node.

Fine… BUT: if the peer is instantiating a new Node, how on earth is the line

new_node.position = some_value

affecting the position of the object that is a different instance from the original one?

I mean logically speaking they are two different objects, right? So how does this synchronization work? It’s a bit as if it acts like “a pointer” (sorry for abusing the term right now, it’s just to give the idea). I really can’t understand how it works and the documentation doesn’t mention much.

You should get an error when passing an Object inheriting class, like a Node, for spawning. This is because objects can contain code and could be used as en exploit. You can however disable this protection but it is not advised.

Regarding position, it doesnt make sense at all that be the case, and i think you explained it correctly. They are different instances and should note effect each other.

The thing im not sure about is the magic position value? When a spawner spawns a node it will put it at the origin of its watched node.

You may have redacted the code for readability, and that is appreciated, but why do something like that at all, why duplicate when all you need to do is get the res path from the object?

I guess if there is an issue, it would seem like the duplicate of the hit node was not completely a deep copy and references where shared with the original node. If that was true this wouldn’t be a sync issue but a node duplicate issue.


Also as i think about it, the server will have all the data for each instance, it may be a little tricky to reparent on the server and have all the data traverse to the new tree location. When the authority of the spawner removes a watched node it will free all remote peer nodes.

When you add to another spawn location in the tree all the peers will get a default instance of the node, then a multiplayer synchronizer should be used to fill that data back in.

I could see how using custom spawns gets a little tricky because the data needs to be preserved. And synchronizers can get a little finicky if they dont have an end point on the peer to sync too. It may be prudent to use the spawn list, so you dont have to make complicated customs spawns like this… Because it would be easiest to just have the preserved original node, on the authority, to be plopped down in the new, watched, tree location. Otherwise you would need to send a dictionary version of all the data for the custom spawn, to avoid doing the duplicate node stuff to get around how the custom spawn works..

Idk, my only comment with using the res path is that strings can be expensive to send. But if you want to continue using custom spawn, a data dictionary would make it even more expensive.

I think I read that somewhere else on the Forum as well. I could not find a toggle for that in the settings, but even if, I don’t know if I wanna go that route to be honest. Even though the game is co-op, you never know what people can add or do to the code. So yeah ahah

I did deduct some logic, and yes I think you are right, I believe I just kept it there as a byproduct of me testing out sending the item directly over the network. So I’ll fix that, thanks!

It would, but I was wondering if adding ALL the in-game items/interactables/pickup-able makes sense or if it just clutters the spawn list with junk. Hence the custom spawn.
I was also thinking of adding the item to the list just to sync it over the network, and then delete it. But perhaps that has too high of a cost, and I would also need a way to tell the peer which item I want to add, and at that point I might just as well instantiate it directly.

I see the point. Perhaps it makes sense - if continuing with custom spawns - to serialize the strings into some sort of id so that id N corresponds to item X, and at that point I guess that a short is enough to keep track of every item I will have.

And for the last point, I have no idea. Documentation about this is quite unreliable.
They mention some Flags under the duplicate method with a default value of 15 which doesnt even exist in the DuplicateFlags. But if this “magic” is happening somehow there’s either a reason or a bug :rofl:

Its a bit mask representing all options enabled

Could you link where you’ve got this from? The DuplicateFlags page doesn’t really explain much

Excuse my mistake its a bit field. Common programming concept where each bit can be called a flag.

If you notice the values for each flag are not consecutive. 1,2,4, 8. if you do a binary representation they look like this

0000 = 0
0001 = 1
0010 = 2
0100 = 4
1000 = 8
…
1111 = 15 (1+2+4+8)

Each bit represents its own concept and can be added to other bits to represent multiple things, like options that are either on or off.

In GDScript you can store 64 bits/flags, in the integer type.

1 Like