MultiplayerSynchronizer and Resources

Godot Version

4.7

Context and explanations

Hello everyone,

I am currently experimenting with the MultiplayerSynchronizer which is a great node, but has some limitations to consider, especially when it comes to Resource replication.

The scenario is fairly simple. I just want this node to replicate the PhysicsMaterial property of a node, as the server may be required to change it.

My primary goal is to automate this process via replication. I would love to avoid using RPCs for such thing.

I came up with a solution but I am a bit struggling on how to effectively implement it, as it seems a little whacky so far. The idea is that I could make a specialized object responsible of proxying the Resource. By “proxying”, I mean expose properties that are bound to the resource’s properties. That way, the synchronizer will deal with this node, read the properties it must synchronize and just send them through the network.

Here is the two test scripts :
test_multiplayer_proxy.gd

@abstract extends Node

@export var target_resource_name: NodePath

var target_resource: Resource:
	get:
		if target_resource:
			return target_resource
		
		target_resource = get_node_and_resource(target_resource_name)[1]
		return target_resource

and test_physics_material_multiplayer_proxy.gd

extends "res://internals/net/test/proxy/test_multiplayer_proxy.gd"

@export var friction: float:
	get: return (target_resource as PhysicsMaterial).friction
	set(v): (target_resource as PhysicsMaterial).friction = v

As it is a PoC, I am just synchronizing the friction property.
This technique seems whacky because the magic happens within the getters/setters, regardless of the lifecycle of nodes. As a PoC, it does work but I would love to improve the technique so far, especially as it relies on NodePaths which can be tricky to debug over time.

The hierarchy is :
image

And the synchronizer follows the property that way :

So far it does work.

Question

Is there any better way of doing this? I don’t want to make a huge game so far and the idea would be to keep it fairly simple. This solution proved to be quite handy, because you would not be inheriting every node you would want to replicate, which here would be a complete nightmare.

I would also consider better implementation ideas. I really do not like the NodePath here because it does not help. Would you make a class per resource type and then inherit again to just fetch the resource from the node? That also seems counter intuitive as it would require a lot of inheritance.

I thought of making my own replication system because I have read that the MultiplayerSpawner and MultiplayerSynchronizer were a bit rigid, but I wanted to give them a try beforehand. On top of that, making a replication system for such a simple game would take time which I didn’t plan.


Thanks for your time,
Varonex

Doesn’t using <name of node>:physics_material_override:friction directly work?

In that specific case it may help !
Now if I take another example where I make a custom resource object and want some of its properties to replicate through the network. How would I do that? Would I specifically need overrides all the time, as with physics_material_override?

In my test I was embedding a PhysicsMaterial resource to a Node2D just as an example. In this case the physics_material_override does not exist. The question is also all about whether or not such setup would be plausible in-game, and I do believe I would have nodes with resources I want to sync but no overrides.

Doing node:example_resource:field as a replication step does not work of course.

Why wouldn’t that node path work? It works with custom resources too, so long as your properties (or :field) is @exported

Well, I may have made a mistake if you can attes it does work. It would be a great surprise !

I will provide some additional context:

Setup

  • This is the scene I want to synchronize, which is sent on network through a MultiplayerSpawner from another scene (and all that works fine). The goal of this scene is to get spawned, and during its readiness, it tweens SpawnedScene:position and at the same time it just modifies SpawnedScene:example_resource:friction. Note that the following screens show the scene and SpawnedScene’s inspector:



    Note how example_resource is exported within the editor.

  • This is the MultiplayerSynchronizer of the same scene:


As you can see, I did use the NodePath SpawnedScene:example_resource:friction. I think it is also worth mentioning that the resource per se is not selectable in the “add property to sync” when choosing SpawnedScene:

  • This is the “main” scene that spawns in the SpawnedScene scene. Its goal is to spawn the scene and destroy it after a certain amount of time:

Testing

  • I have the test session running with 2 distinct instances, and one receives the --server argument. The one with --server is created as server, and the other one as client. Both can communicate properly so far, the connection is done properly.

  • When both run, the SpawnedScene scene gets instantiated and deleted beautifully by the MultiplayerSpawner node. However I keep getting this error in the client:
    image

If I were to replace SpawnedScene:example_resource:friction by MultiplayerPhysicsMaterialProxy:friction, it does work beautifully with absolutely no warning whatsoever.


I may have not done things properly? If you mention that it does indeed work, I would be glad to see where my mistake is ! :pray:

Thanks for your time


EDIT: You’ll find the relevant scripts here

test_mover_node.gd (script attached to SpawnedScene)
extends Node2D

@export var to: Vector2

@export var start_after: float = 2
@export var lasts: float = 5

@export var example_resource: PhysicsMaterial

func _ready() -> void:
	if multiplayer.is_server():
		await get_tree().create_timer(start_after).timeout
		example_resource.friction = .5
		
		var tween: Tween = create_tween()
		tween.tween_property(self, ^"position", to, lasts)
		tween.play()

test_netcode.gd (The script attached to TestNetcode which is in the main scene)
extends Node2D

@export var scene_to_spawn: PackedScene
@export var time_before_deletion: float = 10

func _ready() -> void:
	# Netcode stuff.
	var is_server: bool = "--server" in OS.get_cmdline_args()
	prints("Is server", is_server)
	
	var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new()
	
	if is_server:
		peer.set_bind_ip("127.0.0.1")
		peer.create_server(9111, 2)
	else:
		peer.create_client("127.0.0.1", 9111)
	
	multiplayer.multiplayer_peer = peer
	
	# Spawning & managing (only if server)
	if is_server:
		var spawned: Node2D = scene_to_spawn.instantiate()
		$MultiplayerSynchronized.add_child(spawned)
		
		await get_tree().create_timer(time_before_deletion).timeout
		
		spawned.queue_free()

Bumping the topic