Leveraging Godot's Multiplayer Spawner/Synchroniser appropriately

Godot Version

4.2.2

Question

Hello all

I’m heavily leaning on Godot’s MultiplayerSpawner and MultiplayerSynchronizer to spawn all “multiplayer” objects within my game:

  • Game: Node2D

** PlayerSpawner: MultiplayerSpawner

*** Player1 (player input authority delegated to client)

*** Player2 (player input authority delegated to client)

** LevelSpawner: MultiplayerSpawner

*** Level

** ProjectileSpawner: MultiplayerSpawner

*** Projectile1 (tagged with killer_id)

*** Projectile2 (tagged with killer_id)

** UISpawner: MultiplayerSpawner

*** LobbyMenu

*** PlayerHUD (authority delegated to client)

*** PauseMenu

I guess what I’m asking is, have I gone too far?

No, I use multiplayer spawner and syncing with game stats too.

Interesting… I attempted this too with a dictionary containing nested classes with functions etc. without much luck.

Do you store sync stats purely using native types? (with functions for modifying outside)

There is an option somewhere in the multiplayer functionality to allow Object decoding. This is set to false by default to prevent executable code to be passed between peers as a security precaution.

Dictionaries are possible, I think also may need potentially manually add inner properties to the replication config.

Like nodepath:dic_prop:key

I went through an ordeal where I wanted to just add properties to a structure and have them automatically picked up when I added a new property, as well as auto complete and error warning in the code.

I ended up with a class and I put it inside a single MultiplayerSynchronizer node.

I redacted some of the code to only show the real important parts.

Summary
extends MultiplayerSynchronizer

class_name ScoreCard

class Card extends Resource:
	var active : bool = true
	var player_id : int = -1
	var score : float = 0.0
	var minor_score : float = 0.0
	var rank : int = 0
	var wrecks : int = 0 #kills

	func _init(id:int = -1):
		player_id = id


var card : Card = Card.new()

func _ready():
	replication_config = setup_replication_config()

func setup_replication_config() -> SceneReplicationConfig:
	var rc : = SceneReplicationConfig.new()
	var props = get_local_filtered_properties()
	for prop in props:
		var prop_path : NodePath = NodePath(".:card:" + prop.name)
		rc.add_property(prop_path)
		rc.property_set_spawn(prop_path,true)
		rc.property_set_replication_mode(prop_path,SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE)
	return rc

func get_local_filtered_properties() -> Array:
	var script = card.get_script()
	var props:Array = script.get_script_property_list()
	props.pop_front() # removes hidden script property reference
	return props

func get_card()->Card:
	return card

If I ever wanted to type cast the inner card class I could use ScoreCard.Card type.

This single node scene was then added to a spawn list.

I have other systems to associate this scorecard to a player and update the values, but those or another topic. And there many other ways to go about it.

1 Like

From SceneMultiplayer class.

1 Like

This is really cool - thank you so much for sharing!

Well… time to overhaul my stats system… again…

Just thinking about how I might use this approach, is my understanding correct (thinking about how I might use this for player stats).

  • Create a PlayerStats class with appropriate properties as a MultiplayerSynchronizer (as per your example above).

  • Instance a PlayerStatsSynchronizer node for each player that joins (and associate with other systems).

  • Server then executes functions on PlayerStats for game events.

I suppose I could the same with game stats/scoreboard although that is venturing into nested data (which might be handled as you suggest nodepath:dic_prop:key

1 Like

I do this for player configuration as well.

I do in some areas use an RPC function. Like one off things like “hey server I want to respawn” kind of thing. But I have had more issues trying to get that logic right.

I didn’t think to use it with UI like you mentioned. I might try playing around with figuring that out.

1 Like

Yeah getting RPC logic right takes my brain down a rabbit hole every time.

I ended up removing the UI from my ui_spawner as I didn’t like the idea that every player in the game (for each game) had hidden UI elements floating about.

At one point I was de-instancing UI elements that weren’t needed which felt even worse.

… oh god i’m thinking about overhauling all my managers now… in this new world where everything is a MultiplayerSpawner :upside_down_face:

1 Like

Currently all my shared game UI are singletons that consume playerconfig and scorecards to show the state of the game. I don’t have a lobby UI yet.

For player hud UI, it is built into the player, and when a remote player is added, the UI is deleted from the player scene internally after checking if this is a remote player or the local player.

It contaminates the code a bit. I couldn’t think of a better way at the time.

1 Like

My ui_spawner does spawn the lobby_menu. I don’t know why i’ve made an exception here thinking about it…

I moved my HUD for my player into the root Game node which is outside of the synchronised nodes. I have RPC functions for perform HUD actions like setting progress bars and calling animations (although my HUD is very simple atm so this may change).

1 Like