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; }