[MP] Implement "watched" properties.

Checked at "delta_interval" (default = every frame), synchronized
(reliably) if changes are detected.
This commit is contained in:
Fabio Alessandrelli 2023-03-28 09:30:58 +02:00
parent f581f21dd6
commit f1e0d50841
13 changed files with 422 additions and 50 deletions

View file

@ -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">

View file

@ -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>

View file

@ -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" />

View file

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

View file

@ -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);

View file

@ -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);

View file

@ -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();
};

View file

@ -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);

View file

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

View file

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

View file

@ -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() {}
};

View file

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

View file

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