Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Net] MultiplayerReplicator with initial state. #51534

Merged
merged 1 commit into from
Aug 18, 2021

Conversation

Faless
Copy link
Collaborator

@Faless Faless commented Aug 12, 2021

Supersedes #51465 . Some documentation is still missing.

Thanks everyone for the feedback on the other PR 🎊 !
I've opened this one to address concerns raised about bloating and customizaion:

  • This PR decouple the scene replication logic from the MultiplayerAPI, to a small MultiplayerReplicator
  • This PR adds custom hooks to give complete control over the spawn/despawn sending process (we should do something similar for RPCs at some point).

Simple

Like before, server mode replicates instances automatically from server to client, but spawn/despawn messages can now contain a state.
The state can be automatically encoded/decoded by passing the desired object properties to spawnable_config.
You can use script properties to optimize the state representation.

const Player = preload("res://player.tscn")

func _ready():
	var id = ResourceLoader.get_resource_uid(Player.resource_path)
	multiplayer.replicator.spawn_config(
		id,
		MultiplayerReplicator.REPLICATION_MODE_SERVER,
		# This defines the object properties that will be sent during spawn.
		# In server mode, when the spawn is received by a client from the server, the state will be set automatically in the new instance (unless custom hooks are specified).
		[&"position", &"label"]
	)

func _peer_connected(id : int):
	if multiplayer.is_network_server():
		# New spawns in the server are replicated automatically to clients when in REPLICATION_MODE_SERVER.
		# Removing the node from tree later on will also despawn it remotely.
		# When joining mid-game previously network spawned scenes are also replicated.
		# State is sent along and automatically set according to spawn_config
		var p = Player.instantiate()
		p.position = Vector2(100, 100)
		p.label = str(id)
		$Players.add_child(p)

Customization

Two new functions, spawn and despawn, convey the implementation independent method for requesting a spawn/despawn of an Object (and is also used internally by the server mode), while send_spawn and send_despawn represent the more low-level send event for a Variant to be used by the custom implementations.

2 callables can be specified to completely override the default implementation for sending and receiving the spawn/despawn event, working as callbacks for the new spawn/despawn methods.

The spawn command has a 9 bytes overhead (1 for cmd, 8 for scene id), and there's room for improvement here (if we go the route of using the number on a list).

When using a custom implementation spawn and despawn can be called with any Object, send_spawn/send_despawn can receive any Variant as a state, and the path is ignored.

const Player = preload("res://player.tscn")

func _ready():
	var id = ResourceLoader.get_resource_uid(Player.resource_path)
	multiplayer.replicator.spawn_config(
		id,
		MultiplayerReplicator.REPLICATION_MODE_SERVER,
		# The state definition does nothing by default if overrides are specified. But could still be used, if you wish, via:
		# multiplayer.replicator.encode_state(scene_id, object) # -> PackedByteArray
		# multiplayer.replicator.decode_state(scene_id, object, byte_array) # -> void, update the object via "set"
		[&"state", &"label"],
		self._send_spawn,
		self._recv_spawn
	)

func _peer_connected(id : int):
	if multiplayer.is_network_server():
		# New instances in the server will call your custom hook automatically when in REPLICATION_MODE_SERVER.
		# but you will have control on whether or not data will be sent and to whom.
		# Similarly, removing the node from tree later on will also call your custom hook.
		# When a peer joins mid-game your callback will also be called with the previously replicated scenes (and the destination will be the relevant peer).
		# None of the automation above is done if you opt for REPLICATION_MODE_CUSTOM instead.
		var p = Player.instantiate()
		#p.name = str(id)
		p.position = Vector2(100, 100)
		p.label = str(id)
		p.set_meta("id", id)
		$Players.add_child(p)

func _send_spawn(p_peer : int, p_scene : int, p_state : Object, p_spawn : bool):
	print("Sending resource %d spawn to peer: %d, object: %s" % [p_scene, p_peer, p_state])
	# Here we do the actual sending, but we could also filter peers.
	multiplayer.replicator.send_spawn(p_peer, p_scene, p_state.label.to_utf8_buffer())

func _recv_spawn(p_peer : int, p_scene : int, p_data, p_spawn : bool):
	var data = p_data.get_string_from_utf8() if typeof(p_data) == TYPE_RAW_ARRAY else p_data
	# Here we should spawn the desired instance.
	print("Receiving resource %d spawn from peer: %d. Data: %s" % [p_peer, p_scene, data])

Copy link
Contributor

@jonbonazza jonbonazza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it seems fine. Just one minor qualm, but it’s just coding style/architecture, so not a huge deal.

I wish the move to MultiplayerReplicator would have been done in a separate commit. With everything in one commit, it’s hard to see what exactly changed.

@@ -34,10 +34,15 @@
#include "core/io/multiplayer_peer.h"
#include "core/io/resource_uid.h"
#include "core/object/ref_counted.h"
#include "core/variant/typed_array.h"

class MultiplayerReplicator;

class MultiplayerAPI : public RefCounted {
GDCLASS(MultiplayerAPI, RefCounted);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we make MultiplayerReplicator a friend class here and not just use proper encapsulation in MultiplayerReplicator? I tend to find the use if friend classes to be somewhat of a code smell.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are absolutely right, it stank so much 😅 . Fixed.

@Faless
Copy link
Collaborator Author

Faless commented Aug 12, 2021

I wish the move to MultiplayerReplicator would have been done in a separate commit. With everything in one commit, it’s hard to see what exactly changed.

Yeah, I started working on it based on the other PR, so it kinda stuck. I'll see if I can split it.

@Faless Faless force-pushed the mp/4.x_replicator branch from 2ad070a to 1fbd39e Compare August 12, 2021 04:18
@Faless
Copy link
Collaborator Author

Faless commented Aug 12, 2021

@jonbonazza splitted it a bit (should be squashed back again pre-merge).

Copy link
Member

@mhilbrunner mhilbrunner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Played around with this a lot, seems like a great first step as discussed :)

Lets get this merged.

Move the former "spawnables" functions to a dedicated
MultiplayerReplicator class.
Support custom overrides in replicator.
Spawn/despawn messages can now contain a state.
The state can be automatically encoded/decoded by passing the desired
object properties to `spawnable_config`.
You can use script properties to optimize the state representation.
2 Callables can be also specified to completely override the default
implementation for sending and receiving the spawn/despawn event.
(9 bytes overhead, and there's room for improvement here).
When using a custom implementation `spawn` and `despawn` can be called
with any Object, `send_spawn`/`send_despawn` can receive any Variant as
a state, and the path is not required.

Two new functions, `spawn` and `despawn`, convey the implementation
independent method for requesting a spawn/despawn of an Object, while
`send_spawn` and `send_despawn` represent the more low-level send event
for a Variant to be used by the custom implementations.
@Faless Faless force-pushed the mp/4.x_replicator branch from 6ab1c1b to d4dd859 Compare August 18, 2021 09:23
@Faless Faless merged commit 2a9c4a5 into godotengine:master Aug 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants