Syncing custom resources

Godot Version

4.2.2.stable

Question

I’m trying to figure out why my variables aren’t syncing to other clients. So I made a new project to find out what I’m doing wrong.

For variables of primitive types (e.g. integers) this works as you would expect. Adding them to a multiplayersynchronizer is enough (sync_iters in my project).

For variables of more complex types (e.g. arrays, or dictionaries) or variables that are nested in complex types (e.g. an integer in a custom resource) things don’t work as straightforward. Instead, for example, in the case of an array variable, the values only sync to other clients as follows:

  • Use a function to modify the array values and emit a signal which indicates that its values have changed
  • Connect the signal to a function that modifies a primitive type property
  • When syncing the primitive type, the array will sync too
  • Connecting the sync_iters_changed signal to your displays should update their values on change

This works for complex types and simple types nested in a custom resource as well. One sidenote: if a nested type is specified this pattern does not work (e.g. syncing is possible for “var some_property : Array = ” but not for “var some_property : Array[int] =

Now my question is, is there a simpler way to get this working? The reason i ask is this pattern forces me to update all my displays on any change.

Main scene:

extends Control


#### Constants
const PORT = 4433
const CONNECTION_INFO_SCENE_PATH : String = "res://test_sync/connection_info.tscn"
const CONNECTION_INFO : PackedScene = preload(CONNECTION_INFO_SCENE_PATH)

#### Nodes
@onready var connections = $VBoxContainer/Connections
@onready var multiplayer_spawner = $MultiplayerSpawner
@onready var client_info = $VBoxContainer/ClientInfo/ClientInfo
@onready var multiples_info = $MultiplesContainer/MultiplesInfo

#### Buttons
@onready var host_button = $Buttons/HostButton
@onready var join_button = $Buttons/JoinButton
@onready var increment_sync_iters = $Buttons/IncrementSyncIters
@onready var gen_random_cr_prop = $Buttons/GenRandomCRProp
@onready var add_item = $Buttons/AddItem
@onready var add_item_cr = $Buttons/AddItemCR
@onready var multiples_test = $Buttons/MultiplesTest
@onready var update_multiples_display = $Buttons/UpdateMultiplesDisplay


func _ready():
	host_button.pressed.connect(_on_host_button_pressed)
	join_button.pressed.connect(_on_join_button_pressed)
	increment_sync_iters.pressed.connect(_on_increment_sync_iters_pressed)
	gen_random_cr_prop.pressed.connect(_on_gen_random_cr_prop_pressed)
	add_item.pressed.connect(_on_add_item_pressed)
	add_item_cr.pressed.connect(_on_add_item_cr_pressed)
	multiples_test.pressed.connect(_on_multiples_test_pressed)
	update_multiples_display.pressed.connect(update_multiples_info)
	setup_spawner()

func setup_spawner():
	multiplayer_spawner.spawn_path = NodePath(connections.get_path())
	multiplayer_spawner.add_spawnable_scene(CONNECTION_INFO_SCENE_PATH)
	multiplayer_spawner.spawn_function = spawn_label

func update_multiples_info():
	print("Updating multiples info from: ", multiplayer.get_unique_id())
	var label_txt = "@multiples:"
	for c in connections.get_children():
		if not c is ConnectionInfoInstance:
			continue
		var id = str(c.name)
		var smd = c.test_resource_sync.sync_multiples_d
		var sma = c.test_resource_sync.sync_multiples_a
		label_txt += "\n\tsync_iters_now: %s" % c.sync_iters
		label_txt += "\n\tnode " + id
		label_txt += "\n\t\tdict: %s\n\t\tarr: %s" % [smd, sma]
		label_txt += "\n\n"
	multiples_info.text = label_txt


func spawn_label(id : int):
	var p_node = CONNECTION_INFO.instantiate()
	p_node.name = str(id)
	#p_node.test_resource_sync.multiples_changed.connect(update_multiples_info)
	p_node.sync_iters_changed.connect(update_multiples_info)
	
	return p_node

func start_game():
	client_info.set_client_info()
	# if not host skip this code
	if not multiplayer.is_server():
		return
		
	# setup callbacks for when peers connect or disconnect
	multiplayer.peer_connected.connect(add_player)
	multiplayer.peer_disconnected.connect(del_player)
	
	# for each connected peer, add player
	for id in multiplayer.get_peers():
		add_player.call(id)
		
	# if not dedicated server, add host player
	if not OS.has_feature("dedicated_server"):
		add_player.call(1)

func _on_host_button_pressed():
	# create peer object, server and connect to created server
	print("Pressed host")
	var peer = ENetMultiplayerPeer.new()
	peer.create_server(PORT)
	
	# if failed to create server, notify and exit
	if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED:
		OS.alert("Failed to start multiplayer server.")
		return
	
	# if connected, set peer and start game
	multiplayer.multiplayer_peer = peer
	start_game()


func _on_join_button_pressed():
	# get server to be joined, create new peer object, and connect to server
	print("Pressed join")
	var serverAddress = "localhost"
	var peer = ENetMultiplayerPeer.new()
	peer.create_client(serverAddress, PORT)
	
	# if connect failed, notify and exit
	if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED:
		OS.alert("Failed to connect to server")
		return
	
	# if connected, set peer and start game
	multiplayer.multiplayer_peer = peer
	start_game()

func _on_increment_sync_iters_pressed():
	for c in connections.get_children():
		if c.name == str(multiplayer.get_unique_id()):
			print("pressed gen random prop: ", c.name)
			c.sync_iters += 1
		
	
func _on_gen_random_cr_prop_pressed():
	for c in connections.get_children():
		if c.name == str(multiplayer.get_unique_id()):
			print("pressed gen random cr prop: ", c.name)
			c.test_resource_sync.randomize_stats()

func _on_add_item_pressed():
	print("@add_item called from ", multiplayer.get_unique_id())
	for c in connections.get_children():
		if c.name == str(multiplayer.get_unique_id()):
			c.add_to_array(randi_range(0, 20))
			print("@add_item array:", c.sync_array)
			c.add_to_array_nested(randi_range(0, 20))
			print("@add_item array nested:", c.sync_array)
			c.add_to_dict(randi_range(0, 20), randi_range(0, 20))
			print("@add_item dict:", c.sync_array)

func _on_add_item_cr_pressed():
	print("@add_item_cr called from ", multiplayer.get_unique_id())
	for c in connections.get_children():
		if c.name == str(multiplayer.get_unique_id()):
			c.test_resource_sync.add_to_array_cr(randi_range(0, 20))
			print("@add_item_cr array:", c.test_resource_sync.sync_array_cr)
			c.test_resource_sync.add_to_array_nested_cr(randi_range(0, 20))
			print("@add_item_cr array nested:", c.test_resource_sync.sync_array_cr)
			c.test_resource_sync.add_to_dict_cr(randi_range(0, 20), randi_range(0, 20))
			print("@add_item_cr dict:", c.test_resource_sync.sync_array_cr)

func _on_multiples_test_pressed():
	print("@multiples_test called from ", multiplayer.get_unique_id())
	for c in connections.get_children():
		if c.name == str(multiplayer.get_unique_id()):
			c.test_resource_sync.add_to_multiples(randi_range(0, 20), randi_range(0, 20))
			print("@multiples_test dict:", c.test_resource_sync.sync_multiples_d)
			print("@multiples_test array:", c.test_resource_sync.sync_multiples_a)
	

func add_player(id : int) -> void:
	print("Player added: ", id)
	multiplayer_spawner.spawn(id)
	

func del_player(id : int) -> void:
	print("Player deleted: ", id)
	for c in connections.get_children():
		if str(c.name) == str(id):
			c.queue_free()

Player instance:

extends Label
class_name ConnectionInfoInstance

#### Syncable props
@export var sync_iters : int = 0:
	set(new_int):
		sync_iters = new_int
		sync_iters_changed.emit()
@export var test_resource_sync : TestResourceSync

# properties with nested types cannot be synced
@export var sync_array : Array = []
@export var sync_array_nested : Array[int] = []
@export var sync_dict : Dictionary = {}


#### Signals
signal sync_iters_changed
signal sync_array_changed
signal sync_array_nested_changed
signal sync_dict_changed


#### Built-in
func _enter_tree():
	print("setting mp auth: ", name.to_int())
	set_multiplayer_authority(name.to_int(), true)

func _ready():
	# update label txt
	update_label_txt()
	
	# sync trigger
	sync_iters_changed.connect(update_label_txt)
	
	# connect sync objects
	sync_array_changed.connect(trigger_sync)
	sync_array_nested_changed.connect(trigger_sync)
	sync_dict_changed.connect(trigger_sync)
	
	# connect custom resource sync properties
	test_resource_sync.stats_changed.connect(trigger_sync)
	
	# connect custom resource sync objects
	test_resource_sync.cr_object_changed.connect(trigger_sync)
	
	# connect
	test_resource_sync.multiples_changed.connect(trigger_sync)


#### Modify instance properties
func add_to_array(item : int):
	print("@add_to_array from: ", multiplayer.get_unique_id())
	sync_array.append(item)
	sync_array_changed.emit()

func add_to_array_nested(item : int):
	print("@add_to_array from: ", multiplayer.get_unique_id())
	sync_array_nested.append(item)
	sync_array_nested_changed.emit()

func add_to_dict(k : int, v : int):
	sync_dict[k] = v
	sync_dict_changed.emit()

func trigger_sync():
	sync_iters += 1

#### Display functions
func update_label_txt():
	print("Updating label text: ", name)
	if is_node_ready():
		var label_txt : String = "id: " + str(name) + ", is_server: " + str(str(name) == "1")
		label_txt += ", sync_iters: " + str(sync_iters) + "\n\t" + test_resource_sync.gen_string()
		label_txt += "\n\tsync_array: " + str(sync_array) + ", sync_dict: " + str(sync_dict)
		label_txt += "\n\tsync_array_nested: " + str(sync_array_nested)
		label_txt += "\n\tsync_array_cr: " + str(test_resource_sync.sync_array_cr) + ", sync_dict_cr: " + str(test_resource_sync.sync_dict_cr)
		label_txt += "\n\tsync_array_nested_cr: " + str(test_resource_sync.sync_array_nested_cr)
		label_txt += "\n\tmultiples_d: " + str(test_resource_sync.sync_multiples_d) + ", multiples_a: " + str(test_resource_sync.sync_multiples_a)
		text = label_txt

Custom Resource in player instance

extends Resource
class_name TestResourceSync

@export var intelligence : int = 10
@export var strength : int = 10
@export var dexterity : int = 10
@export var hp : int = 10

@export var sync_array_cr : Array = []
@export var sync_array_nested_cr : Array[int] = []
@export var sync_dict_cr : Dictionary = {}

@export var sync_multiples_d = {}
@export var sync_multiples_a = []

signal stats_changed
signal cr_object_changed
signal multiples_changed

func randomize_stats():
	print("stats being randomized")
	intelligence = randi_range(1, 20)
	strength = randi_range(1, 20)
	dexterity = randi_range(1, 20)
	hp = randi_range(1, 20)
	stats_changed.emit()

func gen_string() -> String:
	return "int: %s, str: %s, dex: %s, hp: %s" % [intelligence, strength, dexterity, hp]

func add_to_array_cr(item : int):
	sync_array_cr.append(item)
	cr_object_changed.emit()
	
func add_to_array_nested_cr(item : int):
	sync_array_nested_cr.append(item)
	cr_object_changed.emit()
	
func add_to_dict_cr(k : int, v : int):
	sync_dict_cr[k] = v
	cr_object_changed.emit()

func add_to_multiples(k : int, v : int):
	sync_multiples_d[k] = v
	sync_multiples_a.append_array([k, v])
	multiples_changed.emit()