godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.13k stars 21.19k forks source link

MultiplayerSynchronizer doesn't synchronize Resources #75264

Closed Mastermori closed 1 year ago

Mastermori commented 1 year ago

Godot version

4.0.1stable

System information

Windows 11

Issue description

The new MultiplayerSynchronizer node does not sync resources. It never updates them, neither on spawn, nor on sync. Using the same Synchronizer to sync other properties (such as Vectors or ints) works fine. This is pretty major as it makes using the MultiplayerSpawner to spawn a node which needs to have a resource set on spawn (in my case I want to sync a custom PlayerStats resource which contains _maxhealth, which is used in _Player.ready()), as you can't use the Spawner for it then, because you cannot add custom spawn synchronization without a MultiplayerSynchronizer.

Steps to reproduce

Assume a tester node: grafik with a script:

#tester.gd
extends Node2D

@export var some_resource: Texture2D

func _input(event: InputEvent) -> void:
    if multiplayer.is_server():
        if event.is_action("ui_accept"):
            some_resource = GradientTexture2D.new()

As you can see a resource (Texture2D) is exported so that it can be synced by a MultiplayerSynchronizer. Now we add it to the Synchronizer of the Tester: grafik We add the net code from the official blogpost to a simple UI. grafik

# multiplayer.gd
extends Control

const PORT = 4433

func _ready():
    # Start paused.
    get_tree().paused = true

func _on_host_pressed():
    # Start as server.
    var peer = ENetMultiplayerPeer.new()
    peer.create_server(PORT)
    if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED:
        OS.alert("Failed to start multiplayer server.")
        return
    multiplayer.multiplayer_peer = peer
    start_game()
    # calls the world.start() method (see below) to add the first tester to the world.
    get_parent().start()

func _on_connect_pressed():
    # Start as client.
    var txt : String = %IPEdit.text
    if txt == "":
        OS.alert("Need a remote to connect to.")
        return
    var peer = ENetMultiplayerPeer.new()
    peer.create_client(txt, PORT)
    if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED:
        OS.alert("Failed to start multiplayer client.")
        return
    multiplayer.multiplayer_peer = peer
    start_game()

func start_game():
    # Hide the UI and unpause to start the game.
    hide()
    get_tree().paused = false

Together with a Container to spawn the Tester in (to test if they are synced on spawn), also with the code from that same blogpost:

#world.gd
extends Node

# was _ready() in the blogpost, changed here to spawn the first player only after host button is pressed.
func start():
    # We only need to spawn players on the server.
    if not multiplayer.is_server():
        return

    multiplayer.peer_connected.connect(add_player)
    multiplayer.peer_disconnected.connect(del_player)

    # Spawn already connected players.
    for id in multiplayer.get_peers():
        add_player(id)

    # Spawn the local player unless this is a dedicated server export.
    if not OS.has_feature("dedicated_server"):
        add_player(1)

func _exit_tree():
    if not multiplayer.is_server():
        return
    multiplayer.peer_connected.disconnect(add_player)
    multiplayer.peer_disconnected.disconnect(del_player)

func add_player(id: int):
    var tester = preload("res://tester.tscn").instantiate()
    # Randomize character position.
    tester.name = str(id)
    $Container.add_child(tester, true)

func del_player(id: int):
    if not $Container.has_node(str(id)):
        return
    $Container.get_node(str(id)).queue_free()

Now running the project and connecting to clients, we can use the remote debug tool (in the scene tree) to look at the resource at runtime. Host: grafik Client: grafik

Pressing Enter changes the resource on the server but not on the client.

Minimal reproduction project

MultiplayerSynchronizerBug.zip

AThousandShips commented 1 year ago

Duplicate of #74325, this is by design, and a pr is open to indicate and manage this for the editor

Mastermori commented 1 year ago

Oh, sorry I didn't find that while looking for existing issues. So there is no way of syncing any "object type properties", does that mean you cannot sync any type of object that isn't a primitive value? Would I need to sync every single primitive value of the resource individually then?

AThousandShips commented 1 year ago

No problem!

Or use RPC, but generally transferring complex data isn't what the synchronization API is for, it's for compact data, you can synchronize arrays and dictionaries, if they are small enough

For other data the time when they are changed are quite clear, like when changing the player state or similar, and not vert regular which is when synchronization is helpful, and can be sent with RPC calls instead, or encoded

Mastermori commented 1 year ago

Ok, thank you for the quick help :) Closing this, as it adds nothing new to the duplicate mentioned above.