Skip to content

Commit

Permalink
Merge pull request #75467 from Faless/mp/4.x_watch
Browse files Browse the repository at this point in the history
[MP] Implement "watched" properties (reliable/on change).
  • Loading branch information
akien-mga committed May 24, 2023
2 parents 774f4a7 + f1e0d50 commit 513f43e
Show file tree
Hide file tree
Showing 13 changed files with 422 additions and 50 deletions.
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

0 comments on commit 513f43e

Please sign in to comment.