summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabio Alessandrelli <fabio.alessandrelli@gmail.com>2021-08-09 19:45:31 +0200
committerGitHub <noreply@github.com>2021-08-09 19:45:31 +0200
commit511f9619736f47946f3d0613bc2642cf2b47beac (patch)
tree7543e0b115503bae9e7358924cb86dee7802bd38
parente3b6c46a9571249b381f183f44aa9dcb2f21d989 (diff)
parent9fcf3b5a9c3036d7eaad02feaef85160abb7a075 (diff)
Merge pull request #51097 from Faless/mp/4.x_spawnable
[Net] MultiplayerAPI remote scene spawning/despawning.
-rw-r--r--core/core_bind.cpp5
-rw-r--r--core/core_bind.h1
-rw-r--r--core/io/multiplayer_api.cpp186
-rw-r--r--core/io/multiplayer_api.h19
-rw-r--r--doc/classes/MultiplayerAPI.xml89
-rw-r--r--doc/classes/ResourceLoader.xml7
-rw-r--r--scene/main/node.cpp7
-rw-r--r--scene/main/node.h1
8 files changed, 315 insertions, 0 deletions
diff --git a/core/core_bind.cpp b/core/core_bind.cpp
index 05fc309a28..0984212c6e 100644
--- a/core/core_bind.cpp
+++ b/core/core_bind.cpp
@@ -107,6 +107,10 @@ bool _ResourceLoader::exists(const String &p_path, const String &p_type_hint) {
return ResourceLoader::exists(p_path, p_type_hint);
}
+ResourceUID::ID _ResourceLoader::get_resource_uid(const String &p_path) {
+ return ResourceLoader::get_resource_uid(p_path);
+}
+
void _ResourceLoader::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_threaded_request", "path", "type_hint", "use_sub_threads"), &_ResourceLoader::load_threaded_request, DEFVAL(""), DEFVAL(false));
ClassDB::bind_method(D_METHOD("load_threaded_get_status", "path", "progress"), &_ResourceLoader::load_threaded_get_status, DEFVAL(Array()));
@@ -118,6 +122,7 @@ void _ResourceLoader::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_dependencies", "path"), &_ResourceLoader::get_dependencies);
ClassDB::bind_method(D_METHOD("has_cached", "path"), &_ResourceLoader::has_cached);
ClassDB::bind_method(D_METHOD("exists", "path", "type_hint"), &_ResourceLoader::exists, DEFVAL(""));
+ ClassDB::bind_method(D_METHOD("get_resource_uid", "path"), &_ResourceLoader::get_resource_uid);
BIND_ENUM_CONSTANT(THREAD_LOAD_INVALID_RESOURCE);
BIND_ENUM_CONSTANT(THREAD_LOAD_IN_PROGRESS);
diff --git a/core/core_bind.h b/core/core_bind.h
index 1574c36d3c..1b3c388fb2 100644
--- a/core/core_bind.h
+++ b/core/core_bind.h
@@ -75,6 +75,7 @@ public:
PackedStringArray get_dependencies(const String &p_path);
bool has_cached(const String &p_path);
bool exists(const String &p_path, const String &p_type_hint = "");
+ ResourceUID::ID get_resource_uid(const String &p_path);
_ResourceLoader() { singleton = this; }
};
diff --git a/core/io/multiplayer_api.cpp b/core/io/multiplayer_api.cpp
index 1c3f231170..f792dc6cb4 100644
--- a/core/io/multiplayer_api.cpp
+++ b/core/io/multiplayer_api.cpp
@@ -33,6 +33,7 @@
#include "core/debugger/engine_debugger.h"
#include "core/io/marshalls.h"
#include "scene/main/node.h"
+#include "scene/resources/packed_scene.h"
#include <stdint.h>
@@ -146,6 +147,7 @@ void MultiplayerAPI::poll() {
}
void MultiplayerAPI::clear() {
+ replicated_nodes.clear();
connected_peers.clear();
path_get_cache.clear();
path_send_cache.clear();
@@ -322,6 +324,12 @@ void MultiplayerAPI::_process_packet(int p_from, const uint8_t *p_packet, int p_
case NETWORK_COMMAND_RAW: {
_process_raw(p_from, p_packet, p_packet_len);
} break;
+ case NETWORK_COMMAND_SPAWN: {
+ _process_spawn_despawn(p_from, p_packet, p_packet_len, true);
+ } break;
+ case NETWORK_COMMAND_DESPAWN: {
+ _process_spawn_despawn(p_from, p_packet, p_packet_len, false);
+ } break;
}
}
@@ -506,6 +514,64 @@ void MultiplayerAPI::_process_confirm_path(int p_from, const uint8_t *p_packet,
E->get() = true;
}
+void MultiplayerAPI::_process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn) {
+ ERR_FAIL_COND_MSG(p_packet_len < 18, "Invalid spawn packet received");
+ int ofs = 1;
+ ResourceUID::ID id = decode_uint64(&p_packet[ofs]);
+ ofs += 8;
+ ERR_FAIL_COND_MSG(!spawnables.has(id), "Invalid spawn ID received " + itos(id));
+
+ uint32_t node_target = decode_uint32(&p_packet[ofs]);
+ Node *parent = _process_get_node(p_from, nullptr, node_target, 0);
+ ofs += 4;
+ ERR_FAIL_COND_MSG(parent == nullptr, "Invalid packet received. Requested node was not found.");
+
+ uint32_t name_len = decode_uint32(&p_packet[ofs]);
+ ofs += 4;
+ ERR_FAIL_COND_MSG(name_len > uint32_t(p_packet_len - ofs), vformat("Invalid spawn packet size: %d, wants: %d", p_packet_len, ofs + name_len));
+ ERR_FAIL_COND_MSG(name_len < 1, "Zero spawn name size.");
+
+ const String name = String::utf8((const char *)&p_packet[ofs], name_len);
+ // We need to make sure no trickery happens here (e.g. despawning a subpath), but we want to allow autogenerated ("@") node names.
+ ERR_FAIL_COND_MSG(name.validate_node_name() != name.replace("@", ""), vformat("Invalid node name received: '%s'", name));
+ ofs += name_len;
+ PackedByteArray data;
+ if (p_packet_len > ofs) {
+ data.resize(p_packet_len - ofs);
+ memcpy(data.ptrw(), &p_packet[ofs], data.size());
+ }
+
+ SpawnMode mode = spawnables[id];
+ if (mode == SPAWN_MODE_SERVER && p_from == 1) {
+ String scene_path = ResourceUID::get_singleton()->get_id_path(id);
+ if (p_spawn) {
+ ERR_FAIL_COND_MSG(parent->has_node(name), vformat("Unable to spawn node. Node already exists: %s/%s", parent->get_path(), name));
+ RES res = ResourceLoader::load(scene_path);
+ ERR_FAIL_COND_MSG(!res.is_valid(), "Unable to load scene to spawn at path: " + scene_path);
+ PackedScene *scene = Object::cast_to<PackedScene>(res.ptr());
+ ERR_FAIL_COND(!scene);
+ Node *node = scene->instantiate();
+ ERR_FAIL_COND(!node);
+ replicated_nodes[node->get_instance_id()] = id;
+ parent->_add_child_nocheck(node, name);
+ emit_signal(SNAME("network_spawn"), p_from, id, node, data);
+ } else {
+ ERR_FAIL_COND_MSG(!parent->has_node(name), vformat("Path not found: %s/%s", parent->get_path(), name));
+ Node *node = parent->get_node(name);
+ ERR_FAIL_COND_MSG(!replicated_nodes.has(node->get_instance_id()), vformat("Trying to despawn a Node that was not replicated: %s/%s", parent->get_path(), name));
+ emit_signal(SNAME("network_despawn"), p_from, id, node, data);
+ replicated_nodes.erase(node->get_instance_id());
+ node->queue_delete();
+ }
+ } else {
+ if (p_spawn) {
+ emit_signal(SNAME("network_spawn_request"), p_from, id, parent, name, data);
+ } else {
+ emit_signal(SNAME("network_despawn_request"), p_from, id, parent, name, data);
+ }
+ }
+}
+
bool MultiplayerAPI::_send_confirm_path(Node *p_node, NodePath p_path, PathSentCache *psc, int p_target) {
bool has_all_peers = true;
List<int> peers_to_add; // If one is missing, take note to add it.
@@ -909,6 +975,16 @@ void MultiplayerAPI::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const
void MultiplayerAPI::_add_peer(int p_id) {
connected_peers.insert(p_id);
path_get_cache.insert(p_id, PathGetCache());
+ if (is_network_server()) {
+ for (const KeyValue<ObjectID, ResourceUID::ID> &E : replicated_nodes) {
+ // Only server mode adds to replicated_nodes, no need to check it.
+ Object *obj = ObjectDB::get_instance(E.key);
+ ERR_CONTINUE(!obj);
+ Node *node = Object::cast_to<Node>(obj);
+ ERR_CONTINUE(!node);
+ _send_spawn_despawn(p_id, E.value, node->get_path(), nullptr, 0, true);
+ }
+ }
emit_signal(SNAME("network_peer_connected"), p_id);
}
@@ -924,6 +1000,10 @@ void MultiplayerAPI::_del_peer(int p_id) {
PathSentCache *psc = path_send_cache.getptr(E);
psc->confirmed_peers.erase(p_id);
}
+ if (p_id == 1) {
+ // Erase server replicated nodes, but do not queue them for deletion.
+ replicated_nodes.clear();
+ }
emit_signal(SNAME("network_peer_disconnected"), p_id);
}
@@ -1067,6 +1147,99 @@ bool MultiplayerAPI::is_object_decoding_allowed() const {
return allow_object_decoding;
}
+Error MultiplayerAPI::spawnable_config(const ResourceUID::ID &p_id, SpawnMode p_mode) {
+ ERR_FAIL_COND_V(p_mode < SPAWN_MODE_NONE || p_mode > SPAWN_MODE_CUSTOM, ERR_INVALID_PARAMETER);
+ ERR_FAIL_COND_V(!ResourceUID::get_singleton()->has_id(p_id), ERR_INVALID_PARAMETER);
+#ifdef TOOLS_ENABLED
+ String path = ResourceUID::get_singleton()->get_id_path(p_id);
+ RES res = ResourceLoader::load(path);
+ ERR_FAIL_COND_V(!res->is_class("PackedScene"), ERR_INVALID_PARAMETER);
+#endif
+ if (p_mode == SPAWN_MODE_NONE) {
+ if (spawnables.has(p_id)) {
+ spawnables.erase(p_id);
+ }
+ } else {
+ spawnables[p_id] = p_mode;
+ }
+ return OK;
+}
+
+Error MultiplayerAPI::send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data) {
+ return _send_spawn_despawn(p_peer_id, p_scene_id, p_path, p_data.ptr(), p_data.size(), false);
+}
+
+Error MultiplayerAPI::send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data) {
+ return _send_spawn_despawn(p_peer_id, p_scene_id, p_path, p_data.ptr(), p_data.size(), true);
+}
+
+Error MultiplayerAPI::_send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const uint8_t *p_data, int p_data_len, bool p_spawn) {
+ ERR_FAIL_COND_V(!root_node, ERR_UNCONFIGURED);
+ ERR_FAIL_COND_V_MSG(!spawnables.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
+
+ NodePath rel_path = (root_node->get_path()).rel_path_to(p_path);
+ const Vector<StringName> names = rel_path.get_names();
+ ERR_FAIL_COND_V(names.size() < 2, ERR_INVALID_PARAMETER);
+
+ NodePath parent = NodePath(names.subarray(0, names.size() - 2), false);
+ ERR_FAIL_COND_V_MSG(!root_node->has_node(parent), ERR_INVALID_PARAMETER, "Path not found: " + parent);
+
+ // See if the path is cached.
+ PathSentCache *psc = path_send_cache.getptr(parent);
+ if (!psc) {
+ // Path is not cached, create.
+ path_send_cache[parent] = PathSentCache();
+ psc = path_send_cache.getptr(parent);
+ psc->id = last_send_cache_id++;
+ }
+ _send_confirm_path(root_node->get_node(parent), parent, psc, p_peer_id);
+
+ const CharString cname = String(names[names.size() - 1]).utf8();
+ int nlen = encode_cstring(cname.get_data(), nullptr);
+ MAKE_ROOM(1 + 8 + 4 + 4 + nlen + p_data_len);
+ uint8_t *ptr = packet_cache.ptrw();
+ ptr[0] = p_spawn ? NETWORK_COMMAND_SPAWN : NETWORK_COMMAND_DESPAWN;
+ int ofs = 1;
+ ofs += encode_uint64(p_scene_id, &ptr[ofs]);
+ ofs += encode_uint32(psc->id, &ptr[ofs]);
+ ofs += encode_uint32(nlen, &ptr[ofs]);
+ ofs += encode_cstring(cname.get_data(), &ptr[ofs]);
+ memcpy(&ptr[ofs], p_data, p_data_len);
+ network_peer->set_target_peer(p_peer_id);
+ network_peer->set_transfer_channel(0);
+ network_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_RELIABLE);
+ return network_peer->put_packet(ptr, ofs + p_data_len);
+}
+
+void MultiplayerAPI::scene_enter_exit_notify(const String &p_scene, const Node *p_node, bool p_enter) {
+ if (!has_network_peer()) {
+ return;
+ }
+ ERR_FAIL_COND(!p_node || !p_node->get_parent() || !root_node);
+ NodePath path = (root_node->get_path()).rel_path_to(p_node->get_parent()->get_path());
+ if (path.is_empty()) {
+ return;
+ }
+ ResourceUID::ID id = ResourceLoader::get_resource_uid(p_scene);
+ if (!spawnables.has(id)) {
+ return;
+ }
+ SpawnMode mode = spawnables[id];
+ if (p_enter) {
+ if (mode == SPAWN_MODE_SERVER && is_network_server()) {
+ replicated_nodes[p_node->get_instance_id()] = id;
+ _send_spawn_despawn(0, id, p_node->get_path(), nullptr, 0, true);
+ }
+ emit_signal(SNAME("network_spawnable_added"), id, p_node);
+ } else {
+ if (mode == SPAWN_MODE_SERVER && is_network_server() && replicated_nodes.has(p_node->get_instance_id())) {
+ replicated_nodes.erase(p_node->get_instance_id());
+ _send_spawn_despawn(0, id, p_node->get_path(), nullptr, 0, false);
+ }
+ emit_signal(SNAME("network_spawnable_removed"), id, p_node);
+ }
+}
+
void MultiplayerAPI::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_root_node", "node"), &MultiplayerAPI::set_root_node);
ClassDB::bind_method(D_METHOD("get_root_node"), &MultiplayerAPI::get_root_node);
@@ -1085,6 +1258,9 @@ void MultiplayerAPI::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_refusing_new_network_connections"), &MultiplayerAPI::is_refusing_new_network_connections);
ClassDB::bind_method(D_METHOD("set_allow_object_decoding", "enable"), &MultiplayerAPI::set_allow_object_decoding);
ClassDB::bind_method(D_METHOD("is_object_decoding_allowed"), &MultiplayerAPI::is_object_decoding_allowed);
+ ClassDB::bind_method(D_METHOD("spawnable_config", "scene_id", "spawn_mode"), &MultiplayerAPI::spawnable_config);
+ ClassDB::bind_method(D_METHOD("send_despawn", "peer_id", "scene_id", "path", "data"), &MultiplayerAPI::send_despawn, DEFVAL(PackedByteArray()));
+ ClassDB::bind_method(D_METHOD("send_spawn", "peer_id", "scene_id", "path", "data"), &MultiplayerAPI::send_spawn, DEFVAL(PackedByteArray()));
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_object_decoding"), "set_allow_object_decoding", "is_object_decoding_allowed");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "refuse_new_network_connections"), "set_refuse_new_network_connections", "is_refusing_new_network_connections");
@@ -1098,11 +1274,21 @@ void MultiplayerAPI::_bind_methods() {
ADD_SIGNAL(MethodInfo("connected_to_server"));
ADD_SIGNAL(MethodInfo("connection_failed"));
ADD_SIGNAL(MethodInfo("server_disconnected"));
+ ADD_SIGNAL(MethodInfo("network_despawn", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
+ ADD_SIGNAL(MethodInfo("network_spawn", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
+ ADD_SIGNAL(MethodInfo("network_despawn_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
+ ADD_SIGNAL(MethodInfo("network_spawn_request", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "parent", PROPERTY_HINT_RESOURCE_TYPE, "Node"), PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
+ ADD_SIGNAL(MethodInfo("network_spawnable_added", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
+ ADD_SIGNAL(MethodInfo("network_spawnable_removed", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
BIND_ENUM_CONSTANT(RPC_MODE_DISABLED);
BIND_ENUM_CONSTANT(RPC_MODE_REMOTE);
BIND_ENUM_CONSTANT(RPC_MODE_MASTER);
BIND_ENUM_CONSTANT(RPC_MODE_PUPPET);
+
+ BIND_ENUM_CONSTANT(SPAWN_MODE_NONE);
+ BIND_ENUM_CONSTANT(SPAWN_MODE_SERVER);
+ BIND_ENUM_CONSTANT(SPAWN_MODE_CUSTOM);
}
MultiplayerAPI::MultiplayerAPI() {
diff --git a/core/io/multiplayer_api.h b/core/io/multiplayer_api.h
index 011bc3dde9..9552a0bf35 100644
--- a/core/io/multiplayer_api.h
+++ b/core/io/multiplayer_api.h
@@ -32,6 +32,7 @@
#define MULTIPLAYER_API_H
#include "core/io/multiplayer_peer.h"
+#include "core/io/resource_uid.h"
#include "core/object/ref_counted.h"
class MultiplayerAPI : public RefCounted {
@@ -45,6 +46,12 @@ public:
RPC_MODE_PUPPET, // Using rpc() on it will call method for all puppets
};
+ enum SpawnMode {
+ SPAWN_MODE_NONE,
+ SPAWN_MODE_SERVER,
+ SPAWN_MODE_CUSTOM,
+ };
+
struct RPCConfig {
StringName name;
RPCMode rpc_mode = RPC_MODE_DISABLED;
@@ -82,10 +89,12 @@ private:
};
Ref<MultiplayerPeer> network_peer;
+ Map<ResourceUID::ID, SpawnMode> spawnables;
int rpc_sender_id = 0;
Set<int> connected_peers;
HashMap<NodePath, PathSentCache> path_send_cache;
Map<int, PathGetCache> path_get_cache;
+ Map<ObjectID, ResourceUID::ID> replicated_nodes;
int last_send_cache_id;
Vector<uint8_t> packet_cache;
Node *root_node = nullptr;
@@ -97,6 +106,7 @@ protected:
void _process_packet(int p_from, const uint8_t *p_packet, int p_packet_len);
void _process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len);
void _process_confirm_path(int p_from, const uint8_t *p_packet, int p_packet_len);
+ void _process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn);
Node *_process_get_node(int p_from, const uint8_t *p_packet, uint32_t p_node_target, int p_packet_len);
void _process_rpc(Node *p_node, const uint16_t p_rpc_method_id, int p_from, const uint8_t *p_packet, int p_packet_len, int p_offset);
void _process_raw(int p_from, const uint8_t *p_packet, int p_packet_len);
@@ -113,6 +123,8 @@ public:
NETWORK_COMMAND_SIMPLIFY_PATH,
NETWORK_COMMAND_CONFIRM_PATH,
NETWORK_COMMAND_RAW,
+ NETWORK_COMMAND_SPAWN,
+ NETWORK_COMMAND_DESPAWN,
};
enum NetworkNodeIdCompression {
@@ -154,10 +166,17 @@ public:
void set_allow_object_decoding(bool p_enable);
bool is_object_decoding_allowed() const;
+ Error spawnable_config(const ResourceUID::ID &p_id, SpawnMode p_mode);
+ Error send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data = PackedByteArray());
+ Error send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const PackedByteArray &p_data = PackedByteArray());
+ Error _send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const NodePath &p_path, const uint8_t *p_data, int p_data_len, bool p_spawn);
+ void scene_enter_exit_notify(const String &p_scene, const Node *p_node, bool p_enter);
+
MultiplayerAPI();
~MultiplayerAPI();
};
VARIANT_ENUM_CAST(MultiplayerAPI::RPCMode);
+VARIANT_ENUM_CAST(MultiplayerAPI::SpawnMode);
#endif // MULTIPLAYER_API_H
diff --git a/doc/classes/MultiplayerAPI.xml b/doc/classes/MultiplayerAPI.xml
index 552e0fce4c..b9f50ad02a 100644
--- a/doc/classes/MultiplayerAPI.xml
+++ b/doc/classes/MultiplayerAPI.xml
@@ -66,6 +66,34 @@
Sends the given raw [code]bytes[/code] to a specific peer identified by [code]id[/code] (see [method MultiplayerPeer.set_target_peer]). Default ID is [code]0[/code], i.e. broadcast to all peers.
</description>
</method>
+ <method name="send_despawn">
+ <return type="int" enum="Error" />
+ <argument index="0" name="peer_id" type="int" />
+ <argument index="1" name="scene_id" type="int" />
+ <argument index="2" name="path" type="NodePath" />
+ <argument index="3" name="data" type="PackedByteArray" default="PackedByteArray()" />
+ <description>
+ Sends a despawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant SPAWN_MODE_SERVER] (see [method spawnable_config]) and the request is sent by the server (see [method is_network_server]), the receiving peer(s) will automatically queue for deletion the node at [code]path[/code] and emit the signal [signal network_despawn]. In all other cases no deletion happens, and the signal [signal network_despawn_request] is emitted instead.
+ </description>
+ </method>
+ <method name="send_spawn">
+ <return type="int" enum="Error" />
+ <argument index="0" name="peer_id" type="int" />
+ <argument index="1" name="scene_id" type="int" />
+ <argument index="2" name="path" type="NodePath" />
+ <argument index="3" name="data" type="PackedByteArray" default="PackedByteArray()" />
+ <description>
+ Sends a spawn request for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). If the scene is configured as [constant SPAWN_MODE_SERVER] (see [method spawnable_config]) and the request is sent by the server (see [method is_network_server]), the receiving peer(s) will automatically instantiate that scene, add it to the [SceneTree] at the given [code]path[/code] and emit the signal [signal network_spawn]. In all other cases no instantiation happens, and the signal [signal network_spawn_request] is emitted instead.
+ </description>
+ </method>
+ <method name="spawnable_config">
+ <return type="int" enum="Error" />
+ <argument index="0" name="scene_id" type="int" />
+ <argument index="1" name="spawn_mode" type="int" enum="MultiplayerAPI.SpawnMode" />
+ <description>
+ Configures the MultiplayerAPI to track instances of the [PackedScene] idenfied by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication. See [enum SpawnMode] for the possible configurations.
+ </description>
+ </method>
</methods>
<members>
<member name="allow_object_decoding" type="bool" setter="set_allow_object_decoding" getter="is_object_decoding_allowed" default="false">
@@ -94,6 +122,25 @@
Emitted when this MultiplayerAPI's [member network_peer] fails to establish a connection to a server. Only emitted on clients.
</description>
</signal>
+ <signal name="network_despawn">
+ <argument index="0" name="id" type="int" />
+ <argument index="1" name="scene_id" type="int" />
+ <argument index="2" name="node" type="Node" />
+ <argument index="3" name="data" type="PackedByteArray" />
+ <description>
+ Emitted on a client before deleting a local Node upon receiving a despawn request from the server.
+ </description>
+ </signal>
+ <signal name="network_despawn_request">
+ <argument index="0" name="id" type="int" />
+ <argument index="1" name="scene_id" type="int" />
+ <argument index="2" name="parent" type="Node" />
+ <argument index="3" name="name" type="String" />
+ <argument index="4" name="data" type="PackedByteArray" />
+ <description>
+ Emitted when a network despawn request has been received from a client, or for a [PackedScene] that has been configured as [constant SPAWN_MODE_CUSTOM].
+ </description>
+ </signal>
<signal name="network_peer_connected">
<argument index="0" name="id" type="int" />
<description>
@@ -113,6 +160,39 @@
Emitted when this MultiplayerAPI's [member network_peer] receive a [code]packet[/code] with custom data (see [method send_bytes]). ID is the peer ID of the peer that sent the packet.
</description>
</signal>
+ <signal name="network_spawn">
+ <argument index="0" name="id" type="int" />
+ <argument index="1" name="scene_id" type="int" />
+ <argument index="2" name="node" type="Node" />
+ <argument index="3" name="data" type="PackedByteArray" />
+ <description>
+ Emitted on a client after a new Node is instantiated locally and added to the SceneTree upon receiving a spawn request from the server.
+ </description>
+ </signal>
+ <signal name="network_spawn_request">
+ <argument index="0" name="id" type="int" />
+ <argument index="1" name="scene_id" type="int" />
+ <argument index="2" name="parent" type="Node" />
+ <argument index="3" name="name" type="String" />
+ <argument index="4" name="data" type="PackedByteArray" />
+ <description>
+ Emitted when a network spawn request has been received from a client, or for a [PackedScene] that has been configured as [constant SPAWN_MODE_CUSTOM].
+ </description>
+ </signal>
+ <signal name="network_spawnable_added">
+ <argument index="0" name="scene_id" type="int" />
+ <argument index="1" name="node" type="Node" />
+ <description>
+ Emitted when an instance of a [PackedScene] that has been configured for networking enters the [SceneTree]. See [method spawnable_config].
+ </description>
+ </signal>
+ <signal name="network_spawnable_removed">
+ <argument index="0" name="scene_id" type="int" />
+ <argument index="1" name="node" type="Node" />
+ <description>
+ Emitted when an instance of a [PackedScene] that has been configured for networking leaves the [SceneTree]. See [method spawnable_config].
+ </description>
+ </signal>
<signal name="server_disconnected">
<description>
Emitted when this MultiplayerAPI's [member network_peer] disconnects from server. Only emitted on clients.
@@ -132,5 +212,14 @@
<constant name="RPC_MODE_PUPPET" value="3" enum="RPCMode">
Used with [method Node.rpc_config] to set a method to be called or a property to be changed only on puppets for this node. Analogous to the [code]puppet[/code] keyword. Only accepts calls or property changes from the node's network master, see [method Node.set_network_master].
</constant>
+ <constant name="SPAWN_MODE_NONE" value="0" enum="SpawnMode">
+ Used with [method spawnable_config] to identify a [PackedScene] that should not be replicated.
+ </constant>
+ <constant name="SPAWN_MODE_SERVER" value="1" enum="SpawnMode">
+ Used with [method spawnable_config] to identify a [PackedScene] that should be automatically replicated from server to clients.
+ </constant>
+ <constant name="SPAWN_MODE_CUSTOM" value="2" enum="SpawnMode">
+ Used with [method spawnable_config] to identify a [PackedScene] that can be manually replicated among peers.
+ </constant>
</constants>
</class>
diff --git a/doc/classes/ResourceLoader.xml b/doc/classes/ResourceLoader.xml
index 3a3ce189d5..81e825194b 100644
--- a/doc/classes/ResourceLoader.xml
+++ b/doc/classes/ResourceLoader.xml
@@ -34,6 +34,13 @@
Returns the list of recognized extensions for a resource type.
</description>
</method>
+ <method name="get_resource_uid">
+ <return type="int" />
+ <argument index="0" name="path" type="String" />
+ <description>
+ Returns the ID associated with a given resource path, or [code]-1[/code] when no such ID exists.
+ </description>
+ </method>
<method name="has_cached">
<return type="bool" />
<argument index="0" name="path" type="String" />
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index f1e5574351..14cd76ab7d 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -116,6 +116,9 @@ void Node::_notification(int p_notification) {
memdelete(data.path_cache);
data.path_cache = nullptr;
}
+ if (data.filename.length()) {
+ get_multiplayer()->scene_enter_exit_notify(data.filename, this, false);
+ }
} break;
case NOTIFICATION_PATH_CHANGED: {
if (data.path_cache) {
@@ -147,6 +150,10 @@ void Node::_notification(int p_notification) {
get_script_instance()->call(SceneStringNames::get_singleton()->_ready);
}
+ if (data.filename.length()) {
+ ERR_FAIL_COND(!is_inside_tree());
+ get_multiplayer()->scene_enter_exit_notify(data.filename, this, true);
+ }
} break;
case NOTIFICATION_POSTINITIALIZE: {
diff --git a/scene/main/node.h b/scene/main/node.h
index 0a88553ea1..7175d4c740 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -202,6 +202,7 @@ protected:
static String _get_name_num_separator();
friend class SceneState;
+ friend class MultiplayerAPI;
void _add_child_nocheck(Node *p_child, const StringName &p_name);
void _set_owner_nocheck(Node *p_owner);