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()