diff --git a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
index 2c93539ae1b..12a2b4ed193 100644
--- a/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
+++ b/modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
@@ -51,6 +51,9 @@
+
+ Time interval between delta synchronizations. When set to [code]0.0[/code] (the default), delta synchronizations happen every network process frame.
+
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.
@@ -58,7 +61,7 @@
Resource containing which properties to synchronize.
- 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.
Node path that replicated properties are relative to.
@@ -69,9 +72,14 @@
+
+
+ Emitted when a new delta synchronization state is received by this synchronizer after the properties have been updated.
+
+
- 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.
diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
index 2445d60f48a..2df2d53b4d6 100644
--- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml
+++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml
@@ -70,6 +70,12 @@
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.
+
+ 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].
+
+
+ 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].
+
If [code]true[/code], the MultiplayerAPI's [member MultiplayerAPI.multiplayer_peer] refuses new incoming connections.
diff --git a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml b/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
index 53ea1d19a1f..2c2ae385929 100644
--- a/modules/multiplayer/doc_classes/SceneReplicationConfig.xml
+++ b/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.
+
+
+
+
+ Returns whether the property identified by the given [code]path[/code] is configured to be reliably synchronized when changes are detected on process.
+
+
@@ -66,6 +73,14 @@
Sets whether the property identified by the given [param path] is configured to be synchronized on process.
+
+
+
+
+
+ Sets whether the property identified by the given [code]path[/code] is configured to be reliably synchronized when changes are detected on process.
+
+
diff --git a/modules/multiplayer/editor/replication_editor.cpp b/modules/multiplayer/editor/replication_editor.cpp
index 1f707f1192e..7e70c12542c 100644
--- a/modules/multiplayer/editor/replication_editor.cpp
+++ b/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 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);
}
diff --git a/modules/multiplayer/editor/replication_editor.h b/modules/multiplayer/editor/replication_editor.h
index d1e0e0a5415..262c10ea812 100644
--- a/modules/multiplayer/editor/replication_editor.h
+++ b/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 &p_select_candidates);
diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp
index 458b6a664a4..e3c31352a7b 100644
--- a/modules/multiplayer/multiplayer_synchronizer.cpp
+++ b/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 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 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 MultiplayerSynchronizer::get_delta_state(uint64_t p_cur_usec, uint64_t p_last_usec, uint64_t &r_indexes) {
+ r_indexes = 0;
+ List 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 MultiplayerSynchronizer::get_delta_properties(uint64_t p_indexes) {
+ List out;
+ ERR_FAIL_COND_V(replication_config.is_null(), out);
+ const List 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);
diff --git a/modules/multiplayer/multiplayer_synchronizer.h b/modules/multiplayer/multiplayer_synchronizer.h
index 11590f41565..6fb249d1994 100644
--- a/modules/multiplayer/multiplayer_synchronizer.h
+++ b/modules/multiplayer/multiplayer_synchronizer.h
@@ -46,15 +46,24 @@ public:
};
private:
+ struct Watcher {
+ NodePath prop;
+ uint64_t last_change_usec = 0;
+ Variant value;
+ };
+
Ref 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 visibility_filters;
HashSet peer_visibility;
+ Vector 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 p_config);
Ref get_replication_config();
@@ -103,6 +116,9 @@ public:
void remove_visibility_filter(Callable p_callback);
VisibilityUpdateMode get_visibility_update_mode() const;
+ List get_delta_state(uint64_t p_cur_usec, uint64_t p_last_usec, uint64_t &r_indexes);
+ List get_delta_properties(uint64_t p_indexes);
+
MultiplayerSynchronizer();
};
diff --git a/modules/multiplayer/scene_multiplayer.cpp b/modules/multiplayer/scene_multiplayer.cpp
index 01fc1b52751..7a424e83f88 100644
--- a/modules/multiplayer/scene_multiplayer.cpp
+++ b/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);
diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h
index 1dbbf07853b..678ae932f1c 100644
--- a/modules/multiplayer/scene_multiplayer.h
+++ b/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 get_path_cache() { return cache; }
Ref get_replicator() { return replicator; }
diff --git a/modules/multiplayer/scene_replication_config.cpp b/modules/multiplayer/scene_replication_config.cpp
index b91c755c627..af6af35219d 100644
--- a/modules/multiplayer/scene_replication_config.cpp
+++ b/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 *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::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::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);
}
diff --git a/modules/multiplayer/scene_replication_config.h b/modules/multiplayer/scene_replication_config.h
index addfec4da3d..d4b0a611bc3 100644
--- a/modules/multiplayer/scene_replication_config.h
+++ b/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 properties;
List spawn_props;
List sync_props;
+ List 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 &get_spawn_properties() { return spawn_props; }
const List &get_sync_properties() { return sync_props; }
+ const List &get_watch_properties() { return watch_props; }
SceneReplicationConfig() {}
};
diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp
index 5889b8f5f99..b058bf7a521 100644
--- a/modules/multiplayer/scene_replication_interface.cpp
+++ b/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 &E : peers_info) {
const HashSet 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 &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 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(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(sid);
+ }
+ return sync;
+}
+
+void SceneReplicationInterface::_send_delta(int p_peer, const HashSet p_synchronizers, uint64_t p_usec, const HashMap 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(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 delta = sync->get_delta_state(p_usec, last_usec, indexes);
+
+ if (!delta.size()) {
+ continue; // Nothing to update.
+ }
+
+ Vector 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 props = sync->get_delta_properties(indexes);
+ ERR_FAIL_COND_V(props.size() == 0, ERR_INVALID_DATA);
+ Vector 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 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 p
for (const ObjectID &oid : p_synchronizers) {
MultiplayerSynchronizer *sync = get_id_as(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 vars;
@@ -711,7 +823,7 @@ void SceneReplicationInterface::_send_sync(int p_peer, const HashSet 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 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(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(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;
+}
diff --git a/modules/multiplayer/scene_replication_interface.h b/modules/multiplayer/scene_replication_interface.h
index cf45db21386..0af45c16b4a 100644
--- a/modules/multiplayer/scene_replication_interface.h
+++ b/modules/multiplayer/scene_replication_interface.h
@@ -62,6 +62,7 @@ private:
struct PeerInfo {
HashSet sync_nodes;
HashSet spawn_nodes;
+ HashMap last_watch_usecs;
HashMap recv_sync_ids;
HashMap 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 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 p_synchronizers, uint16_t p_sync_net_time, uint64_t p_usec);
+ void _send_delta(int p_peer, const HashSet p_synchronizers, uint64_t p_usec, const HashMap 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;
}