Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[MP] Implement "watched" properties (reliable/on change). #75467

Merged
merged 1 commit into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions modules/multiplayer/doc_classes/MultiplayerSynchronizer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@
</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>
<member name="replication_config" type="SceneReplicationConfig" setter="set_replication_config" getter="get_replication_config">
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.
Expand All @@ -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">
Expand Down
6 changes: 6 additions & 0 deletions modules/multiplayer/doc_classes/SceneMultiplayer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
15 changes: 15 additions & 0 deletions modules/multiplayer/doc_classes/SceneReplicationConfig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand All @@ -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" />
Expand Down
35 changes: 29 additions & 6 deletions modules/multiplayer/editor/replication_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
}
}

Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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);
}
2 changes: 1 addition & 1 deletion modules/multiplayer/editor/replication_editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class ReplicationEditor : public VBoxContainer {
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);
Expand Down
113 changes: 103 additions & 10 deletions modules/multiplayer/multiplayer_synchronizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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);

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

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading