1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-14 13:41:12 +00:00

[Net] Basic extensible MultiplayerAPI spawn/despawn.

`PackedScene`s can be configured to be spawnable via a new
`MultiplayerAPI.spawnable_config` method.
They can be configured either to be spawned automatically when coming
from the server or to always require verification.
Another method, `MultiplayerAPI.send_spawn` lets you request a spawn on
the remote peers.
When a peer receive a spawn request:
- If it comes from the server and the scene is configured as
  `SPAWN_MODE_SERVER`:
  - Spawn the scene (instantiate it, add it to tree).
  - Emit signal `network_spawn`.
- Else:
  - Emit signal `network_spawn_request`.

In a similar way, `despawn`s are handled automatically in
`SPAWN_MODE_SERVER`.

In `SPAWN_MODE_SERVER`, when a new client connects it will also receive,
from the server all the spawned (and not yet despawned) instances.
This commit is contained in:
Fabio Alessandrelli
2021-07-30 02:14:37 +02:00
parent 9798d08ac2
commit 9fcf3b5a9c
5 changed files with 302 additions and 0 deletions

View File

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