Browse Source

Merge pull request #75467 from Faless/mp/4.x_watch

[MP] Implement "watched" properties (reliable/on change).
Rémi Verschelde 2 years ago
parent
commit
513f43e774

+ 10 - 2
modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml

@@ -51,6 +51,9 @@
 		</method>
 	</methods>
 	<members>
+		<member name="delta_interval" type="float" setter="set_delta_interval" getter="get_delta_interval" default="0.0">
+			Time interval between delta synchronizations. When set to [code]0.0[/code] (the default), delta synchronizations happen every network process frame.
+		</member>
 		<member name="public_visibility" type="bool" setter="set_visibility_public" getter="is_visibility_public" default="true">
 			Whether synchronization should be visible to all peers by default. See [method set_visibility_for] and [method add_visibility_filter] for ways of configuring fine-grained visibility options.
 		</member>
@@ -58,7 +61,7 @@
 			Resource containing which properties to synchronize.
 		</member>
 		<member name="replication_interval" type="float" setter="set_replication_interval" getter="get_replication_interval" default="0.0">
-			Time interval between synchronizes. When set to [code]0.0[/code] (the default), synchronizes happen every network process frame.
+			Time interval between synchronizations. When set to [code]0.0[/code] (the default), synchronizations happen every network process frame.
 		</member>
 		<member name="root_path" type="NodePath" setter="set_root_path" getter="get_root_path" default="NodePath(&quot;..&quot;)">
 			Node path that replicated properties are relative to.
@@ -69,9 +72,14 @@
 		</member>
 	</members>
 	<signals>
+		<signal name="delta_synchronized">
+			<description>
+				Emitted when a new delta synchronization state is received by this synchronizer after the properties have been updated.
+			</description>
+		</signal>
 		<signal name="synchronized">
 			<description>
-				Emitted when a new synchronization state is received by this synchronizer after the variables have been updated.
+				Emitted when a new synchronization state is received by this synchronizer after the properties have been updated.
 			</description>
 		</signal>
 		<signal name="visibility_changed">

+ 6 - 0
modules/multiplayer/doc_classes/SceneMultiplayer.xml

@@ -70,6 +70,12 @@
 		<member name="auth_timeout" type="float" setter="set_auth_timeout" getter="get_auth_timeout" default="3.0">
 			If set to a value greater than [code]0.0[/code], the maximum amount of time peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals.
 		</member>
+		<member name="max_delta_packet_size" type="int" setter="set_max_delta_packet_size" getter="get_max_delta_packet_size" default="65535">
+			Maximum size of each delta packet. Higher values increase the chance of receiving full updates in a single frame, but also the chance of causing networking congestion (higher latency, disconnections). See [MultiplayerSynchronizer].
+		</member>
+		<member name="max_sync_packet_size" type="int" setter="set_max_sync_packet_size" getter="get_max_sync_packet_size" default="1350">
+			Maximum size of each synchronization packet. Higher values increase the chance of receiving full updates in a single frame, but also the chance of packet loss. See [MultiplayerSynchronizer].
+		</member>
 		<member name="refuse_new_connections" type="bool" setter="set_refuse_new_connections" getter="is_refusing_new_connections" default="false">
 			If [code]true[/code], the MultiplayerAPI's [member MultiplayerAPI.multiplayer_peer] refuses new incoming connections.
 		</member>

+ 15 - 0
modules/multiplayer/doc_classes/SceneReplicationConfig.xml

@@ -50,6 +50,13 @@
 				Returns whether the property identified by the given [param path] is configured to be synchronized on process.
 			</description>
 		</method>
+		<method name="property_get_watch">
+			<return type="bool" />
+			<param index="0" name="path" type="NodePath" />
+			<description>
+				Returns whether the property identified by the given [code]path[/code] is configured to be reliably synchronized when changes are detected on process.
+			</description>
+		</method>
 		<method name="property_set_spawn">
 			<return type="void" />
 			<param index="0" name="path" type="NodePath" />
@@ -66,6 +73,14 @@
 				Sets whether the property identified by the given [param path] is configured to be synchronized on process.
 			</description>
 		</method>
+		<method name="property_set_watch">
+			<return type="void" />
+			<param index="0" name="path" type="NodePath" />
+			<param index="1" name="enabled" type="bool" />
+			<description>
+				Sets whether the property identified by the given [code]path[/code] is configured to be reliably synchronized when changes are detected on process.
+			</description>
+		</method>
 		<method name="remove_property">
 			<return type="void" />
 			<param index="0" name="path" type="NodePath" />

+ 29 - 6
modules/multiplayer/editor/replication_editor.cpp

@@ -226,7 +226,7 @@ ReplicationEditor::ReplicationEditor() {
 
 	tree = memnew(Tree);
 	tree->set_hide_root(true);
-	tree->set_columns(4);
+	tree->set_columns(5);
 	tree->set_column_titles_visible(true);
 	tree->set_column_title(0, TTR("Properties"));
 	tree->set_column_expand(0, true);
@@ -235,8 +235,11 @@ ReplicationEditor::ReplicationEditor() {
 	tree->set_column_custom_minimum_width(1, 100);
 	tree->set_column_title(2, TTR("Sync"));
 	tree->set_column_custom_minimum_width(2, 100);
+	tree->set_column_title(3, TTR("Watch"));
+	tree->set_column_custom_minimum_width(3, 100);
 	tree->set_column_expand(2, false);
 	tree->set_column_expand(3, false);
+	tree->set_column_expand(4, false);
 	tree->create_item();
 	tree->connect("button_clicked", callable_mp(this, &ReplicationEditor::_tree_button_pressed));
 	tree->connect("item_edited", callable_mp(this, &ReplicationEditor::_tree_item_edited));
@@ -353,17 +356,30 @@ void ReplicationEditor::_tree_item_edited() {
 		return;
 	}
 	int column = tree->get_edited_column();
-	ERR_FAIL_COND(column < 1 || column > 2);
+	ERR_FAIL_COND(column < 1 || column > 3);
 	const NodePath prop = ti->get_metadata(0);
 	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 	bool value = ti->is_checked(column);
+
+	// We have a hard limit of 64 watchable properties per synchronizer.
+	if (column == 3 && value && config->get_watch_properties().size() > 64) {
+		error_dialog->set_text(TTR("Each MultiplayerSynchronizer can have no more than 64 watched properties."));
+		error_dialog->popup_centered();
+		ti->set_checked(column, false);
+		return;
+	}
 	String method;
 	if (column == 1) {
 		undo_redo->create_action(TTR("Set spawn property"));
 		method = "property_set_spawn";
-	} else {
+	} else if (column == 2) {
 		undo_redo->create_action(TTR("Set sync property"));
 		method = "property_set_sync";
+	} else if (column == 3) {
+		undo_redo->create_action(TTR("Set watch property"));
+		method = "property_set_watch";
+	} else {
+		ERR_FAIL();
 	}
 	undo_redo->add_do_method(config.ptr(), method, prop, value);
 	undo_redo->add_undo_method(config.ptr(), method, prop, !value);
@@ -395,12 +411,14 @@ void ReplicationEditor::_dialog_closed(bool p_confirmed) {
 		int idx = config->property_get_index(prop);
 		bool spawn = config->property_get_spawn(prop);
 		bool sync = config->property_get_sync(prop);
+		bool watch = config->property_get_watch(prop);
 		EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 		undo_redo->create_action(TTR("Remove Property"));
 		undo_redo->add_do_method(config.ptr(), "remove_property", prop);
 		undo_redo->add_undo_method(config.ptr(), "add_property", prop, idx);
 		undo_redo->add_undo_method(config.ptr(), "property_set_spawn", prop, spawn);
 		undo_redo->add_undo_method(config.ptr(), "property_set_sync", prop, sync);
+		undo_redo->add_undo_method(config.ptr(), "property_set_watch", prop, watch);
 		undo_redo->add_do_method(this, "_update_config");
 		undo_redo->add_undo_method(this, "_update_config");
 		undo_redo->commit_action();
@@ -436,7 +454,7 @@ void ReplicationEditor::_update_config() {
 	}
 	for (int i = 0; i < props.size(); i++) {
 		const NodePath path = props[i];
-		_add_property(path, config->property_get_spawn(path), config->property_get_sync(path));
+		_add_property(path, config->property_get_spawn(path), config->property_get_sync(path), config->property_get_watch(path));
 	}
 }
 
@@ -460,13 +478,14 @@ Ref<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) {
 	return get_theme_icon(p_node->get_class(), "EditorIcons");
 }
 
-void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync) {
+void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, bool p_sync, bool p_watch) {
 	String prop = String(p_property);
 	TreeItem *item = tree->create_item();
 	item->set_selectable(0, false);
 	item->set_selectable(1, false);
 	item->set_selectable(2, false);
 	item->set_selectable(3, false);
+	item->set_selectable(4, false);
 	item->set_text(0, prop);
 	item->set_metadata(0, prop);
 	Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr;
@@ -482,7 +501,7 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn,
 		icon = _get_class_icon(node);
 	}
 	item->set_icon(0, icon);
-	item->add_button(3, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
+	item->add_button(4, get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
 	item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER);
 	item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK);
 	item->set_checked(1, p_spawn);
@@ -491,4 +510,8 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn,
 	item->set_cell_mode(2, TreeItem::CELL_MODE_CHECK);
 	item->set_checked(2, p_sync);
 	item->set_editable(2, true);
+	item->set_text_alignment(3, HORIZONTAL_ALIGNMENT_CENTER);
+	item->set_cell_mode(3, TreeItem::CELL_MODE_CHECK);
+	item->set_checked(3, p_watch);
+	item->set_editable(3, true);
 }

+ 1 - 1
modules/multiplayer/editor/replication_editor.h

@@ -76,7 +76,7 @@ private:
 	void _update_checked(const NodePath &p_prop, int p_column, bool p_checked);
 	void _update_config();
 	void _dialog_closed(bool p_confirmed);
-	void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true);
+	void _add_property(const NodePath &p_property, bool p_spawn = true, bool p_sync = true, bool p_watch = false);
 
 	void _pick_node_filter_text_changed(const String &p_newtext);
 	void _pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates);

+ 103 - 10
modules/multiplayer/multiplayer_synchronizer.cpp

@@ -105,7 +105,7 @@ Node *MultiplayerSynchronizer::get_root_node() {
 
 void MultiplayerSynchronizer::reset() {
 	net_id = 0;
-	last_sync_msec = 0;
+	last_sync_usec = 0;
 	last_inbound_sync = 0;
 }
 
@@ -117,16 +117,17 @@ void MultiplayerSynchronizer::set_net_id(uint32_t p_net_id) {
 	net_id = p_net_id;
 }
 
-bool MultiplayerSynchronizer::update_outbound_sync_time(uint64_t p_msec) {
-	if (last_sync_msec == p_msec) {
-		// last_sync_msec has been updated on this frame.
+bool MultiplayerSynchronizer::update_outbound_sync_time(uint64_t p_usec) {
+	if (last_sync_usec == p_usec) {
+		// last_sync_usec has been updated in this frame.
 		return true;
 	}
-	if (p_msec >= last_sync_msec + interval_msec) {
-		last_sync_msec = p_msec;
-		return true;
+	if (p_usec < last_sync_usec + sync_interval_usec) {
+		// Too soon, should skip this synchronization frame.
+		return false;
 	}
-	return false;
+	last_sync_usec = p_usec;
+	return true;
 }
 
 bool MultiplayerSynchronizer::update_inbound_sync_time(uint16_t p_network_time) {
@@ -243,6 +244,9 @@ void MultiplayerSynchronizer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_replication_interval", "milliseconds"), &MultiplayerSynchronizer::set_replication_interval);
 	ClassDB::bind_method(D_METHOD("get_replication_interval"), &MultiplayerSynchronizer::get_replication_interval);
 
+	ClassDB::bind_method(D_METHOD("set_delta_interval", "milliseconds"), &MultiplayerSynchronizer::set_delta_interval);
+	ClassDB::bind_method(D_METHOD("get_delta_interval"), &MultiplayerSynchronizer::get_delta_interval);
+
 	ClassDB::bind_method(D_METHOD("set_replication_config", "config"), &MultiplayerSynchronizer::set_replication_config);
 	ClassDB::bind_method(D_METHOD("get_replication_config"), &MultiplayerSynchronizer::get_replication_config);
 
@@ -260,6 +264,7 @@ void MultiplayerSynchronizer::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "replication_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_replication_interval", "get_replication_interval");
+	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "delta_interval", PROPERTY_HINT_RANGE, "0,5,0.001,suffix:s"), "set_delta_interval", "get_delta_interval");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "replication_config", PROPERTY_HINT_RESOURCE_TYPE, "SceneReplicationConfig", PROPERTY_USAGE_NO_EDITOR), "set_replication_config", "get_replication_config");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_update_mode", PROPERTY_HINT_ENUM, "Idle,Physics,None"), "set_visibility_update_mode", "get_visibility_update_mode");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "public_visibility"), "set_visibility_public", "is_visibility_public");
@@ -269,6 +274,7 @@ void MultiplayerSynchronizer::_bind_methods() {
 	BIND_ENUM_CONSTANT(VISIBILITY_PROCESS_NONE);
 
 	ADD_SIGNAL(MethodInfo("synchronized"));
+	ADD_SIGNAL(MethodInfo("delta_synchronized"));
 	ADD_SIGNAL(MethodInfo("visibility_changed", PropertyInfo(Variant::INT, "for_peer")));
 }
 
@@ -300,11 +306,20 @@ void MultiplayerSynchronizer::_notification(int p_what) {
 
 void MultiplayerSynchronizer::set_replication_interval(double p_interval) {
 	ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)");
-	interval_msec = uint64_t(p_interval * 1000);
+	sync_interval_usec = uint64_t(p_interval * 1000 * 1000);
 }
 
 double MultiplayerSynchronizer::get_replication_interval() const {
-	return double(interval_msec) / 1000.0;
+	return double(sync_interval_usec) / 1000.0 / 1000.0;
+}
+
+void MultiplayerSynchronizer::set_delta_interval(double p_interval) {
+	ERR_FAIL_COND_MSG(p_interval < 0, "Interval must be greater or equal to 0 (where 0 means default)");
+	delta_interval_usec = uint64_t(p_interval * 1000 * 1000);
+}
+
+double MultiplayerSynchronizer::get_delta_interval() const {
+	return double(delta_interval_usec) / 1000.0 / 1000.0;
 }
 
 void MultiplayerSynchronizer::set_replication_config(Ref<SceneReplicationConfig> p_config) {
@@ -349,6 +364,84 @@ void MultiplayerSynchronizer::set_multiplayer_authority(int p_peer_id, bool p_re
 	get_multiplayer()->object_configuration_add(node, this);
 }
 
+Error MultiplayerSynchronizer::_watch_changes(uint64_t p_usec) {
+	ERR_FAIL_COND_V(replication_config.is_null(), FAILED);
+	const List<NodePath> props = replication_config->get_watch_properties();
+	if (props.size() != watchers.size()) {
+		watchers.resize(props.size());
+	}
+	if (props.size() == 0) {
+		return OK;
+	}
+	Node *node = get_root_node();
+	ERR_FAIL_COND_V(!node, FAILED);
+	int idx = -1;
+	Watcher *ptr = watchers.ptrw();
+	for (const NodePath &prop : props) {
+		idx++;
+		bool valid = false;
+		const Object *obj = _get_prop_target(node, prop);
+		ERR_CONTINUE_MSG(!obj, vformat("Node not found for property '%s'.", prop));
+		Variant v = obj->get(prop.get_concatenated_subnames(), &valid);
+		ERR_CONTINUE_MSG(!valid, vformat("Property '%s' not found.", prop));
+		Watcher &w = ptr[idx];
+		if (w.prop != prop) {
+			w.prop = prop;
+			w.value = v.duplicate(true);
+			w.last_change_usec = p_usec;
+		} else if (!w.value.hash_compare(v)) {
+			w.value = v.duplicate(true);
+			w.last_change_usec = p_usec;
+		}
+	}
+	return OK;
+}
+
+List<Variant> MultiplayerSynchronizer::get_delta_state(uint64_t p_cur_usec, uint64_t p_last_usec, uint64_t &r_indexes) {
+	r_indexes = 0;
+	List<Variant> out;
+
+	if (last_watch_usec == p_cur_usec) {
+		// We already watched for changes in this frame.
+
+	} else if (p_cur_usec < p_last_usec + delta_interval_usec) {
+		// Too soon skip delta synchronization.
+		return out;
+
+	} else {
+		// Watch for changes.
+		Error err = _watch_changes(p_cur_usec);
+		ERR_FAIL_COND_V(err != OK, out);
+		last_watch_usec = p_cur_usec;
+	}
+
+	const Watcher *ptr = watchers.size() ? watchers.ptr() : nullptr;
+	for (int i = 0; i < watchers.size(); i++) {
+		const Watcher &w = ptr[i];
+		if (w.last_change_usec <= p_last_usec) {
+			continue;
+		}
+		out.push_back(w.value);
+		r_indexes |= 1ULL << i;
+	}
+	return out;
+}
+
+List<NodePath> MultiplayerSynchronizer::get_delta_properties(uint64_t p_indexes) {
+	List<NodePath> out;
+	ERR_FAIL_COND_V(replication_config.is_null(), out);
+	const List<NodePath> watch_props = replication_config->get_watch_properties();
+	int idx = 0;
+	for (const NodePath &prop : watch_props) {
+		if ((p_indexes & (1ULL << idx)) == 0) {
+			continue;
+		}
+		out.push_back(prop);
+		idx++;
+	}
+	return out;
+}
+
 MultiplayerSynchronizer::MultiplayerSynchronizer() {
 	// Publicly visible by default.
 	peer_visibility.insert(0);

+ 19 - 3
modules/multiplayer/multiplayer_synchronizer.h

@@ -46,15 +46,24 @@ public:
 	};
 
 private:
+	struct Watcher {
+		NodePath prop;
+		uint64_t last_change_usec = 0;
+		Variant value;
+	};
+
 	Ref<SceneReplicationConfig> replication_config;
 	NodePath root_path = NodePath(".."); // Start with parent, like with AnimationPlayer.
-	uint64_t interval_msec = 0;
+	uint64_t sync_interval_usec = 0;
+	uint64_t delta_interval_usec = 0;
 	VisibilityUpdateMode visibility_update_mode = VISIBILITY_PROCESS_IDLE;
 	HashSet<Callable> visibility_filters;
 	HashSet<int> peer_visibility;
+	Vector<Watcher> watchers;
+	uint64_t last_watch_usec = 0;
 
 	ObjectID root_node_cache;
-	uint64_t last_sync_msec = 0;
+	uint64_t last_sync_usec = 0;
 	uint16_t last_inbound_sync = 0;
 	uint32_t net_id = 0;
 
@@ -62,6 +71,7 @@ private:
 	void _start();
 	void _stop();
 	void _update_process();
+	Error _watch_changes(uint64_t p_usec);
 
 protected:
 	static void _bind_methods();
@@ -77,7 +87,7 @@ public:
 	uint32_t get_net_id() const;
 	void set_net_id(uint32_t p_net_id);
 
-	bool update_outbound_sync_time(uint64_t p_msec);
+	bool update_outbound_sync_time(uint64_t p_usec);
 	bool update_inbound_sync_time(uint16_t p_network_time);
 
 	PackedStringArray get_configuration_warnings() const override;
@@ -85,6 +95,9 @@ public:
 	void set_replication_interval(double p_interval);
 	double get_replication_interval() const;
 
+	void set_delta_interval(double p_interval);
+	double get_delta_interval() const;
+
 	void set_replication_config(Ref<SceneReplicationConfig> p_config);
 	Ref<SceneReplicationConfig> get_replication_config();
 
@@ -103,6 +116,9 @@ public:
 	void remove_visibility_filter(Callable p_callback);
 	VisibilityUpdateMode get_visibility_update_mode() const;
 
+	List<Variant> get_delta_state(uint64_t p_cur_usec, uint64_t p_last_usec, uint64_t &r_indexes);
+	List<NodePath> get_delta_properties(uint64_t p_indexes);
+
 	MultiplayerSynchronizer();
 };
 

+ 23 - 0
modules/multiplayer/scene_multiplayer.cpp

@@ -617,6 +617,22 @@ bool SceneMultiplayer::is_server_relay_enabled() const {
 	return server_relay;
 }
 
+void SceneMultiplayer::set_max_sync_packet_size(int p_size) {
+	replicator->set_max_sync_packet_size(p_size);
+}
+
+int SceneMultiplayer::get_max_sync_packet_size() const {
+	return replicator->get_max_sync_packet_size();
+}
+
+void SceneMultiplayer::set_max_delta_packet_size(int p_size) {
+	replicator->set_max_delta_packet_size(p_size);
+}
+
+int SceneMultiplayer::get_max_delta_packet_size() const {
+	return replicator->get_max_delta_packet_size();
+}
+
 void SceneMultiplayer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_root_path", "path"), &SceneMultiplayer::set_root_path);
 	ClassDB::bind_method(D_METHOD("get_root_path"), &SceneMultiplayer::get_root_path);
@@ -641,12 +657,19 @@ void SceneMultiplayer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("is_server_relay_enabled"), &SceneMultiplayer::is_server_relay_enabled);
 	ClassDB::bind_method(D_METHOD("send_bytes", "bytes", "id", "mode", "channel"), &SceneMultiplayer::send_bytes, DEFVAL(MultiplayerPeer::TARGET_PEER_BROADCAST), DEFVAL(MultiplayerPeer::TRANSFER_MODE_RELIABLE), DEFVAL(0));
 
+	ClassDB::bind_method(D_METHOD("get_max_sync_packet_size"), &SceneMultiplayer::get_max_sync_packet_size);
+	ClassDB::bind_method(D_METHOD("set_max_sync_packet_size", "size"), &SceneMultiplayer::set_max_sync_packet_size);
+	ClassDB::bind_method(D_METHOD("get_max_delta_packet_size"), &SceneMultiplayer::get_max_delta_packet_size);
+	ClassDB::bind_method(D_METHOD("set_max_delta_packet_size", "size"), &SceneMultiplayer::set_max_delta_packet_size);
+
 	ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_path"), "set_root_path", "get_root_path");
 	ADD_PROPERTY(PropertyInfo(Variant::CALLABLE, "auth_callback"), "set_auth_callback", "get_auth_callback");
 	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "auth_timeout", PROPERTY_HINT_RANGE, "0,30,0.1,or_greater,suffix:s"), "set_auth_timeout", "get_auth_timeout");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_connections"), "set_refuse_new_connections", "is_refusing_new_connections");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "server_relay"), "set_server_relay_enabled", "is_server_relay_enabled");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "max_sync_packet_size"), "set_max_sync_packet_size", "get_max_sync_packet_size");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "max_delta_packet_size"), "set_max_delta_packet_size", "get_max_delta_packet_size");
 
 	ADD_PROPERTY_DEFAULT("refuse_new_connections", false);
 

+ 6 - 0
modules/multiplayer/scene_multiplayer.h

@@ -195,6 +195,12 @@ public:
 	void set_server_relay_enabled(bool p_enabled);
 	bool is_server_relay_enabled() const;
 
+	void set_max_sync_packet_size(int p_size);
+	int get_max_sync_packet_size() const;
+
+	void set_max_delta_packet_size(int p_size);
+	int get_max_delta_packet_size() const;
+
 	Ref<SceneCacheInterface> get_path_cache() { return cache; }
 	Ref<SceneReplicationInterface> get_replicator() { return replicator; }
 

+ 35 - 0
modules/multiplayer/scene_replication_config.cpp

@@ -72,6 +72,14 @@ bool SceneReplicationConfig::_set(const StringName &p_name, const Variant &p_val
 				spawn_props.erase(prop.name);
 			}
 			return true;
+		} else if (what == "watch") {
+			prop.watch = p_value;
+			if (prop.watch) {
+				watch_props.push_back(prop.name);
+			} else {
+				watch_props.erase(prop.name);
+			}
+			return true;
 		}
 	}
 	return false;
@@ -94,6 +102,9 @@ bool SceneReplicationConfig::_get(const StringName &p_name, Variant &r_ret) cons
 		} else if (what == "spawn") {
 			r_ret = prop.spawn;
 			return true;
+		} else if (what == "watch") {
+			r_ret = prop.watch;
+			return true;
 		}
 	}
 	return false;
@@ -104,6 +115,7 @@ void SceneReplicationConfig::_get_property_list(List<PropertyInfo> *p_list) cons
 		p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
 		p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/spawn", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
 		p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
+		p_list->push_back(PropertyInfo(Variant::STRING, "properties/" + itos(i) + "/watch", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL));
 	}
 }
 
@@ -212,6 +224,27 @@ void SceneReplicationConfig::property_set_sync(const NodePath &p_path, bool p_en
 	}
 }
 
+bool SceneReplicationConfig::property_get_watch(const NodePath &p_path) {
+	List<ReplicationProperty>::Element *E = properties.find(p_path);
+	ERR_FAIL_COND_V(!E, false);
+	return E->get().watch;
+}
+
+void SceneReplicationConfig::property_set_watch(const NodePath &p_path, bool p_enabled) {
+	List<ReplicationProperty>::Element *E = properties.find(p_path);
+	ERR_FAIL_COND(!E);
+	if (E->get().watch == p_enabled) {
+		return;
+	}
+	E->get().watch = p_enabled;
+	watch_props.clear();
+	for (const ReplicationProperty &prop : properties) {
+		if (prop.watch) {
+			watch_props.push_back(p_path);
+		}
+	}
+}
+
 void SceneReplicationConfig::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_properties"), &SceneReplicationConfig::get_properties);
 	ClassDB::bind_method(D_METHOD("add_property", "path", "index"), &SceneReplicationConfig::add_property, DEFVAL(-1));
@@ -222,4 +255,6 @@ void SceneReplicationConfig::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("property_set_spawn", "path", "enabled"), &SceneReplicationConfig::property_set_spawn);
 	ClassDB::bind_method(D_METHOD("property_get_sync", "path"), &SceneReplicationConfig::property_get_sync);
 	ClassDB::bind_method(D_METHOD("property_set_sync", "path", "enabled"), &SceneReplicationConfig::property_set_sync);
+	ClassDB::bind_method(D_METHOD("property_get_watch", "path"), &SceneReplicationConfig::property_get_watch);
+	ClassDB::bind_method(D_METHOD("property_set_watch", "path", "enabled"), &SceneReplicationConfig::property_set_watch);
 }

+ 6 - 0
modules/multiplayer/scene_replication_config.h

@@ -45,6 +45,7 @@ private:
 		NodePath name;
 		bool spawn = true;
 		bool sync = true;
+		bool watch = false;
 
 		bool operator==(const ReplicationProperty &p_to) {
 			return name == p_to.name;
@@ -60,6 +61,7 @@ private:
 	List<ReplicationProperty> properties;
 	List<NodePath> spawn_props;
 	List<NodePath> sync_props;
+	List<NodePath> watch_props;
 
 protected:
 	static void _bind_methods();
@@ -82,8 +84,12 @@ public:
 	bool property_get_sync(const NodePath &p_path);
 	void property_set_sync(const NodePath &p_path, bool p_enabled);
 
+	bool property_get_watch(const NodePath &p_path);
+	void property_set_watch(const NodePath &p_path, bool p_enabled);
+
 	const List<NodePath> &get_spawn_properties() { return spawn_props; }
 	const List<NodePath> &get_sync_properties() { return sync_props; }
+	const List<NodePath> &get_watch_properties() { return watch_props; }
 
 	SceneReplicationConfig() {}
 };

+ 155 - 27
modules/multiplayer/scene_replication_interface.cpp

@@ -138,15 +138,16 @@ void SceneReplicationInterface::on_network_process() {
 		spawn_queue.clear();
 	}
 
-	// Process timed syncs.
-	uint64_t msec = OS::get_singleton()->get_ticks_msec();
+	// Process syncs.
+	uint64_t usec = OS::get_singleton()->get_ticks_usec();
 	for (KeyValue<int, PeerInfo> &E : peers_info) {
 		const HashSet<ObjectID> to_sync = E.value.sync_nodes;
 		if (to_sync.is_empty()) {
 			continue; // Nothing to sync
 		}
 		uint16_t sync_net_time = ++E.value.last_sent_sync;
-		_send_sync(E.key, to_sync, sync_net_time, msec);
+		_send_sync(E.key, to_sync, sync_net_time, usec);
+		_send_delta(E.key, to_sync, usec, E.value.last_watch_usecs);
 	}
 }
 
@@ -280,6 +281,7 @@ Error SceneReplicationInterface::on_replication_stop(Object *p_obj, Variant p_co
 	sync_nodes.erase(sid);
 	for (KeyValue<int, PeerInfo> &E : peers_info) {
 		E.value.sync_nodes.erase(sid);
+		E.value.last_watch_usecs.erase(sid);
 		if (sync->get_net_id()) {
 			E.value.recv_sync_ids.erase(sync->get_net_id());
 		}
@@ -357,6 +359,7 @@ Error SceneReplicationInterface::_update_sync_visibility(int p_peer, Multiplayer
 				E.value.sync_nodes.insert(sid);
 			} else {
 				E.value.sync_nodes.erase(sid);
+				E.value.last_watch_usecs.erase(sid);
 			}
 		}
 		return OK;
@@ -369,6 +372,7 @@ Error SceneReplicationInterface::_update_sync_visibility(int p_peer, Multiplayer
 			peers_info[p_peer].sync_nodes.insert(sid);
 		} else {
 			peers_info[p_peer].sync_nodes.erase(sid);
+			peers_info[p_peer].last_watch_usecs.erase(sid);
 		}
 		return OK;
 	}
@@ -670,8 +674,126 @@ Error SceneReplicationInterface::on_despawn_receive(int p_from, const uint8_t *p
 	return OK;
 }
 
-void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_msec) {
-	MAKE_ROOM(sync_mtu);
+bool SceneReplicationInterface::_verify_synchronizer(int p_peer, MultiplayerSynchronizer *p_sync, uint32_t &r_net_id) {
+	r_net_id = p_sync->get_net_id();
+	if (r_net_id == 0 || (r_net_id & 0x80000000)) {
+		int path_id = 0;
+		bool verified = multiplayer->get_path_cache()->send_object_cache(p_sync, p_peer, path_id);
+		ERR_FAIL_COND_V_MSG(path_id < 0, false, "This should never happen!");
+		if (r_net_id == 0) {
+			// First time path based ID.
+			r_net_id = path_id | 0x80000000;
+			p_sync->set_net_id(r_net_id | 0x80000000);
+		}
+		return verified;
+	}
+	return true;
+}
+
+MultiplayerSynchronizer *SceneReplicationInterface::_find_synchronizer(int p_peer, uint32_t p_net_id) {
+	MultiplayerSynchronizer *sync = nullptr;
+	if (p_net_id & 0x80000000) {
+		sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_path_cache()->get_cached_object(p_peer, p_net_id & 0x7FFFFFFF));
+	} else if (peers_info[p_peer].recv_sync_ids.has(p_net_id)) {
+		const ObjectID &sid = peers_info[p_peer].recv_sync_ids[p_net_id];
+		sync = get_id_as<MultiplayerSynchronizer>(sid);
+	}
+	return sync;
+}
+
+void SceneReplicationInterface::_send_delta(int p_peer, const HashSet<ObjectID> p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> p_last_watch_usecs) {
+	MAKE_ROOM(/* header */ 1 + /* element */ 4 + 8 + 4 + delta_mtu);
+	uint8_t *ptr = packet_cache.ptrw();
+	ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC | (1 << SceneMultiplayer::CMD_FLAG_0_SHIFT);
+	int ofs = 1;
+	for (const ObjectID &oid : p_synchronizers) {
+		MultiplayerSynchronizer *sync = get_id_as<MultiplayerSynchronizer>(oid);
+		ERR_CONTINUE(!sync || !sync->get_replication_config().is_valid() || !sync->is_multiplayer_authority());
+		uint32_t net_id;
+		if (!_verify_synchronizer(p_peer, sync, net_id)) {
+			continue;
+		}
+		uint64_t last_usec = p_last_watch_usecs.has(oid) ? p_last_watch_usecs[oid] : 0;
+		uint64_t indexes;
+		List<Variant> delta = sync->get_delta_state(p_usec, last_usec, indexes);
+
+		if (!delta.size()) {
+			continue; // Nothing to update.
+		}
+
+		Vector<const Variant *> varp;
+		varp.resize(delta.size());
+		const Variant **vptr = varp.ptrw();
+		int i = 0;
+		for (const Variant &v : delta) {
+			vptr[i] = &v;
+		}
+		int size;
+		Error err = MultiplayerAPI::encode_and_compress_variants(vptr, varp.size(), nullptr, size);
+		ERR_CONTINUE_MSG(err != OK, "Unable to encode delta state.");
+
+		ERR_CONTINUE_MSG(size > delta_mtu, vformat("Synchronizer delta bigger than MTU will not be sent (%d > %d): %s", size, delta_mtu, sync->get_path()));
+
+		if (ofs + 4 + 8 + 4 + size > delta_mtu) {
+			// Send what we got, and reset write.
+			_send_raw(packet_cache.ptr(), ofs, p_peer, true);
+			ofs = 1;
+		}
+		if (size) {
+			ofs += encode_uint32(sync->get_net_id(), &ptr[ofs]);
+			ofs += encode_uint64(indexes, &ptr[ofs]);
+			ofs += encode_uint32(size, &ptr[ofs]);
+			MultiplayerAPI::encode_and_compress_variants(vptr, varp.size(), &ptr[ofs], size);
+			ofs += size;
+		}
+#ifdef DEBUG_ENABLED
+		_profile_node_data("delta_out", oid, size);
+#endif
+		peers_info[p_peer].last_watch_usecs[oid] = p_usec;
+	}
+	if (ofs > 1) {
+		// Got some left over to send.
+		_send_raw(packet_cache.ptr(), ofs, p_peer, true);
+	}
+}
+
+Error SceneReplicationInterface::on_delta_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
+	int ofs = 1;
+	while (ofs + 4 + 8 + 4 < p_buffer_len) {
+		uint32_t net_id = decode_uint32(&p_buffer[ofs]);
+		ofs += 4;
+		uint64_t indexes = decode_uint64(&p_buffer[ofs]);
+		ofs += 8;
+		uint32_t size = decode_uint32(&p_buffer[ofs]);
+		ofs += 4;
+		ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA);
+		MultiplayerSynchronizer *sync = _find_synchronizer(p_from, net_id);
+		Node *node = sync ? sync->get_root_node() : nullptr;
+		if (!sync || sync->get_multiplayer_authority() != p_from || !node) {
+			ofs += size;
+			ERR_CONTINUE_MSG(true, "Ignoring delta for non-authority or invalid synchronizer.");
+		}
+		List<NodePath> props = sync->get_delta_properties(indexes);
+		ERR_FAIL_COND_V(props.size() == 0, ERR_INVALID_DATA);
+		Vector<Variant> vars;
+		vars.resize(props.size());
+		int consumed = 0;
+		Error err = MultiplayerAPI::decode_and_decompress_variants(vars, p_buffer + ofs, size, consumed);
+		ERR_FAIL_COND_V(err != OK, err);
+		ERR_FAIL_COND_V(uint32_t(consumed) != size, ERR_INVALID_DATA);
+		err = MultiplayerSynchronizer::set_state(props, node, vars);
+		ERR_FAIL_COND_V(err != OK, err);
+		ofs += size;
+		sync->emit_signal(SNAME("delta_synchronized"));
+#ifdef DEBUG_ENABLED
+		_profile_node_data("delta_in", sync->get_instance_id(), size);
+#endif
+	}
+	return OK;
+}
+
+void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec) {
+	MAKE_ROOM(/* header */ 3 + /* element */ 4 + 4 + sync_mtu);
 	uint8_t *ptr = packet_cache.ptrw();
 	ptr[0] = SceneMultiplayer::NETWORK_COMMAND_SYNC;
 	int ofs = 1;
@@ -681,26 +803,16 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p
 	for (const ObjectID &oid : p_synchronizers) {
 		MultiplayerSynchronizer *sync = get_id_as<MultiplayerSynchronizer>(oid);
 		ERR_CONTINUE(!sync || !sync->get_replication_config().is_valid() || !sync->is_multiplayer_authority());
-		if (!sync->update_outbound_sync_time(p_msec)) {
+		if (!sync->update_outbound_sync_time(p_usec)) {
 			continue; // nothing to sync.
 		}
 
 		Node *node = sync->get_root_node();
 		ERR_CONTINUE(!node);
 		uint32_t net_id = sync->get_net_id();
-		if (net_id == 0 || (net_id & 0x80000000)) {
-			int path_id = 0;
-			bool verified = multiplayer->get_path_cache()->send_object_cache(sync, p_peer, path_id);
-			ERR_CONTINUE_MSG(path_id < 0, "This should never happen!");
-			if (net_id == 0) {
-				// First time path based ID.
-				net_id = path_id | 0x80000000;
-				sync->set_net_id(net_id | 0x80000000);
-			}
-			if (!verified) {
-				// The path based sync is not yet confirmed, skipping.
-				continue;
-			}
+		if (!_verify_synchronizer(p_peer, sync, net_id)) {
+			// The path based sync is not yet confirmed, skipping.
+			continue;
 		}
 		int size;
 		Vector<Variant> vars;
@@ -711,7 +823,7 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p
 		err = MultiplayerAPI::encode_and_compress_variants(varp.ptrw(), varp.size(), nullptr, size);
 		ERR_CONTINUE_MSG(err != OK, "Unable to encode sync state.");
 		// TODO Handle single state above MTU.
-		ERR_CONTINUE_MSG(size > 3 + 4 + 4 + sync_mtu, vformat("Node states bigger then MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path()));
+		ERR_CONTINUE_MSG(size > sync_mtu, vformat("Node states bigger than MTU will not be sent (%d > %d): %s", size, sync_mtu, node->get_path()));
 		if (ofs + 4 + 4 + size > sync_mtu) {
 			// Send what we got, and reset write.
 			_send_raw(packet_cache.ptr(), ofs, p_peer, false);
@@ -735,6 +847,10 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet<ObjectID> p
 
 Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len) {
 	ERR_FAIL_COND_V_MSG(p_buffer_len < 11, ERR_INVALID_DATA, "Invalid sync packet received");
+	bool is_delta = (p_buffer[0] & (1 << SceneMultiplayer::CMD_FLAG_0_SHIFT)) != 0;
+	if (is_delta) {
+		return on_delta_receive(p_from, p_buffer, p_buffer_len);
+	}
 	uint16_t time = decode_uint16(&p_buffer[1]);
 	int ofs = 3;
 	while (ofs + 8 < p_buffer_len) {
@@ -743,13 +859,7 @@ Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_bu
 		uint32_t size = decode_uint32(&p_buffer[ofs]);
 		ofs += 4;
 		ERR_FAIL_COND_V(size > uint32_t(p_buffer_len - ofs), ERR_INVALID_DATA);
-		MultiplayerSynchronizer *sync = nullptr;
-		if (net_id & 0x80000000) {
-			sync = Object::cast_to<MultiplayerSynchronizer>(multiplayer->get_path_cache()->get_cached_object(p_from, net_id & 0x7FFFFFFF));
-		} else if (peers_info[p_from].recv_sync_ids.has(net_id)) {
-			const ObjectID &sid = peers_info[p_from].recv_sync_ids[net_id];
-			sync = get_id_as<MultiplayerSynchronizer>(sid);
-		}
+		MultiplayerSynchronizer *sync = _find_synchronizer(p_from, net_id);
 		if (!sync) {
 			// Not received yet.
 			ofs += size;
@@ -782,3 +892,21 @@ Error SceneReplicationInterface::on_sync_receive(int p_from, const uint8_t *p_bu
 	}
 	return OK;
 }
+
+void SceneReplicationInterface::set_max_sync_packet_size(int p_size) {
+	ERR_FAIL_COND_MSG(p_size < 128, "Sync maximum packet size must be at least 128 bytes.");
+	sync_mtu = p_size;
+}
+
+int SceneReplicationInterface::get_max_sync_packet_size() const {
+	return sync_mtu;
+}
+
+void SceneReplicationInterface::set_max_delta_packet_size(int p_size) {
+	ERR_FAIL_COND_MSG(p_size < 128, "Sync maximum packet size must be at least 128 bytes.");
+	delta_mtu = p_size;
+}
+
+int SceneReplicationInterface::get_max_delta_packet_size() const {
+	return delta_mtu;
+}

+ 14 - 1
modules/multiplayer/scene_replication_interface.h

@@ -62,6 +62,7 @@ private:
 	struct PeerInfo {
 		HashSet<ObjectID> sync_nodes;
 		HashSet<ObjectID> spawn_nodes;
+		HashMap<ObjectID, uint64_t> last_watch_usecs;
 		HashMap<uint32_t, ObjectID> recv_sync_ids;
 		HashMap<uint32_t, ObjectID> recv_nodes;
 		uint16_t last_sent_sync = 0;
@@ -88,12 +89,17 @@ private:
 	SceneMultiplayer *multiplayer = nullptr;
 	PackedByteArray packet_cache;
 	int sync_mtu = 1350; // Highly dependent on underlying protocol.
+	int delta_mtu = 65535;
 
 	TrackedNode &_track(const ObjectID &p_id);
 	void _untrack(const ObjectID &p_id);
 	void _node_ready(const ObjectID &p_oid);
 
-	void _send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_msec);
+	bool _verify_synchronizer(int p_peer, MultiplayerSynchronizer *p_sync, uint32_t &r_net_id);
+	MultiplayerSynchronizer *_find_synchronizer(int p_peer, uint32_t p_net_ida);
+
+	void _send_sync(int p_peer, const HashSet<ObjectID> p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec);
+	void _send_delta(int p_peer, const HashSet<ObjectID> p_synchronizers, uint64_t p_usec, const HashMap<ObjectID, uint64_t> p_last_watch_usecs);
 	Error _make_spawn_packet(Node *p_node, MultiplayerSpawner *p_spawner, int &r_len);
 	Error _make_despawn_packet(Node *p_node, int &r_len);
 	Error _send_raw(const uint8_t *p_buffer, int p_size, int p_peer, bool p_reliable);
@@ -127,9 +133,16 @@ public:
 	Error on_spawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
 	Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
 	Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
+	Error on_delta_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
 
 	bool is_rpc_visible(const ObjectID &p_oid, int p_peer) const;
 
+	void set_max_sync_packet_size(int p_size);
+	int get_max_sync_packet_size() const;
+
+	void set_max_delta_packet_size(int p_size);
+	int get_max_delta_packet_size() const;
+
 	SceneReplicationInterface(SceneMultiplayer *p_multiplayer) {
 		multiplayer = p_multiplayer;
 	}