Merge pull request #51788 from Faless/mp/4.x_replicator_sync

[Net] MultiplayerReplicator state sync.
This commit is contained in:
Fabio Alessandrelli 2021-08-30 00:48:10 +02:00 committed by GitHub
commit 838a449d64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 389 additions and 11 deletions

View file

@ -140,6 +140,9 @@ void MultiplayerAPI::poll() {
break; // It's also possible that a packet or RPC caused a disconnection, so also check here.
}
}
if (network_peer.is_valid() && network_peer->get_connection_status() == MultiplayerPeer::CONNECTION_CONNECTED) {
replicator->poll();
}
}
void MultiplayerAPI::clear() {
@ -326,6 +329,9 @@ void MultiplayerAPI::_process_packet(int p_from, const uint8_t *p_packet, int p_
case NETWORK_COMMAND_DESPAWN: {
replicator->process_spawn_despawn(p_from, p_packet, p_packet_len, false);
} break;
case NETWORK_COMMAND_SYNC: {
replicator->process_sync(p_from, p_packet, p_packet_len);
} break;
}
}

View file

@ -74,6 +74,7 @@ public:
NETWORK_COMMAND_RAW,
NETWORK_COMMAND_SPAWN,
NETWORK_COMMAND_DESPAWN,
NETWORK_COMMAND_SYNC, // This is the max we can have. We should optmize simplify/confirm, possibly spawn/despawn.
};
enum NetworkNodeIdCompression {

View file

@ -38,6 +38,140 @@
if (packet_cache.size() < m_amount) \
packet_cache.resize(m_amount);
Error MultiplayerReplicator::_sync_all_default(const ResourceUID::ID &p_scene_id, int p_peer) {
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
SceneConfig &cfg = replications[p_scene_id];
int full_size = 0;
bool same_size = true;
int last_size = 0;
bool all_raw = true;
struct EncodeInfo {
int size = 0;
bool raw = false;
List<Variant> state;
};
Map<ObjectID, struct EncodeInfo> state;
if (tracked_objects.has(p_scene_id)) {
for (const ObjectID &obj_id : tracked_objects[p_scene_id]) {
Object *obj = ObjectDB::get_instance(obj_id);
if (obj) {
struct EncodeInfo info;
Error err = _get_state(cfg.sync_properties, obj, info.state);
ERR_CONTINUE(err);
err = _encode_state(info.state, nullptr, info.size, &info.raw);
ERR_CONTINUE(err);
state[obj_id] = info;
full_size += info.size;
if (last_size && info.size != last_size) {
same_size = false;
}
all_raw = all_raw && info.raw;
last_size = info.size;
}
}
}
// Default implementation do not send empty updates.
if (!full_size) {
return OK;
}
#ifdef DEBUG_ENABLED
if (full_size > 4096 && cfg.sync_interval) {
WARN_PRINT_ONCE(vformat("The timed state update for scene %d is big (%d bytes) consider optimizing it", p_scene_id));
}
#endif
if (same_size) {
// This is fast and small. Should we allow more than 256 objects per type?
// This costs us 1 byte.
MAKE_ROOM(SYNC_CMD_OFFSET + 1 + 2 + 2 + full_size);
} else {
MAKE_ROOM(SYNC_CMD_OFFSET + 1 + 2 + state.size() * 2 + full_size);
}
int ofs = 0;
uint8_t *ptr = packet_cache.ptrw();
ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC + ((same_size ? 1 : 0) << MultiplayerAPI::BYTE_ONLY_OR_NO_ARGS_SHIFT);
ofs = 1;
ofs += encode_uint64(p_scene_id, &ptr[ofs]);
ptr[ofs] = cfg.sync_recv++;
ofs += 1;
ofs += encode_uint16(state.size(), &ptr[ofs]);
if (same_size) {
ofs += encode_uint16(last_size + (all_raw ? 1 << 15 : 0), &ptr[ofs]);
}
for (const ObjectID &obj_id : tracked_objects[p_scene_id]) {
if (!state.has(obj_id)) {
continue;
}
struct EncodeInfo &info = state[obj_id];
Object *obj = ObjectDB::get_instance(obj_id);
ERR_CONTINUE(!obj);
int size = 0;
if (!same_size) {
// We need to encode the size of every object.
ofs += encode_uint16(info.size + (info.raw ? 1 << 15 : 0), &ptr[ofs]);
}
Error err = _encode_state(info.state, &ptr[ofs], size, &info.raw);
ERR_CONTINUE(err);
ofs += size;
}
Ref<MultiplayerPeer> network_peer = multiplayer->get_network_peer();
network_peer->set_target_peer(p_peer);
network_peer->set_transfer_channel(0);
network_peer->set_transfer_mode(MultiplayerPeer::TRANSFER_MODE_UNRELIABLE);
return network_peer->put_packet(ptr, ofs);
}
void MultiplayerReplicator::_process_default_sync(const ResourceUID::ID &p_id, const uint8_t *p_packet, int p_packet_len) {
ERR_FAIL_COND_MSG(p_packet_len < SYNC_CMD_OFFSET + 5, "Invalid spawn packet received");
ERR_FAIL_COND_MSG(!replications.has(p_id), "Invalid spawn ID received " + itos(p_id));
SceneConfig &cfg = replications[p_id];
ERR_FAIL_COND_MSG(cfg.mode != REPLICATION_MODE_SERVER || multiplayer->is_network_server(), "The defualt implementation only allows sync packets from the server");
const bool same_size = ((p_packet[0] & 64) >> MultiplayerAPI::BYTE_ONLY_OR_NO_ARGS_SHIFT) == 1;
int ofs = SYNC_CMD_OFFSET;
int time = p_packet[ofs];
// Skip old update.
if (time < cfg.sync_recv && cfg.sync_recv - time < 127) {
return;
}
cfg.sync_recv = time;
ofs += 1;
int count = decode_uint16(&p_packet[ofs]);
ofs += 2;
#ifdef DEBUG_ENABLED
ERR_FAIL_COND(!tracked_objects.has(p_id) || tracked_objects[p_id].size() != count);
#else
if (!tracked_objects.has(p_id) || tracked_objects[p_id].size() != count) {
return;
}
#endif
int data_size = 0;
bool raw = false;
if (same_size) {
// This is fast and optimized.
data_size = decode_uint16(&p_packet[ofs]);
raw = (data_size & (1 << 15)) != 0;
data_size = data_size & ~(1 << 15);
ofs += 2;
ERR_FAIL_COND(p_packet_len - ofs < data_size * count);
}
for (const ObjectID &obj_id : tracked_objects[p_id]) {
Object *obj = ObjectDB::get_instance(obj_id);
ERR_CONTINUE(!obj);
if (!same_size) {
// This is slow and wasteful.
data_size = decode_uint16(&p_packet[ofs]);
raw = (data_size & (1 << 15)) != 0;
data_size = data_size & ~(1 << 15);
ofs += 2;
ERR_FAIL_COND(p_packet_len - ofs < data_size);
}
int size = 0;
Error err = _decode_state(cfg.sync_properties, obj, &p_packet[ofs], data_size, size, raw);
ofs += data_size;
ERR_CONTINUE(err);
ERR_CONTINUE(size != data_size);
}
}
Error MultiplayerReplicator::_send_default_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, Object *p_obj, const NodePath &p_path, bool p_spawn) {
ERR_FAIL_COND_V(p_spawn && !p_obj, ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
@ -136,6 +270,7 @@ void MultiplayerReplicator::_process_default_spawn_despawn(int p_from, const Res
Node *node = scene->instantiate();
ERR_FAIL_COND(!node);
replicated_nodes[node->get_instance_id()] = p_scene_id;
_track(p_scene_id, node);
int size;
_decode_state(cfg.properties, node, &p_packet[ofs], p_packet_len - ofs, size, is_raw);
parent->_add_child_nocheck(node, name);
@ -145,6 +280,7 @@ void MultiplayerReplicator::_process_default_spawn_despawn(int p_from, const Res
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("despawned"), p_scene_id, node);
_untrack(p_scene_id, node);
replicated_nodes.erase(node->get_instance_id());
node->queue_delete();
}
@ -197,6 +333,37 @@ void MultiplayerReplicator::process_spawn_despawn(int p_from, const uint8_t *p_p
}
}
void MultiplayerReplicator::process_sync(int p_from, const uint8_t *p_packet, int p_packet_len) {
ERR_FAIL_COND_MSG(p_packet_len < SPAWN_CMD_OFFSET, "Invalid spawn packet received");
ResourceUID::ID id = decode_uint64(&p_packet[1]);
ERR_FAIL_COND_MSG(!replications.has(id), "Invalid spawn ID received " + itos(id));
const SceneConfig &cfg = replications[id];
if (cfg.on_sync_receive.is_valid()) {
Array objs;
if (tracked_objects.has(id)) {
objs.resize(tracked_objects[id].size());
int idx = 0;
for (const ObjectID &obj_id : tracked_objects[id]) {
objs[idx++] = ObjectDB::get_instance(obj_id);
}
}
PackedByteArray pba;
pba.resize(p_packet_len - SPAWN_CMD_OFFSET);
if (pba.size()) {
memcpy(pba.ptrw(), p_packet, p_packet_len - SPAWN_CMD_OFFSET);
}
Variant args[4] = { p_from, id, objs, pba };
Variant *argp[4] = { args, &args[1], &args[2], &args[3] };
Callable::CallError ce;
Variant ret;
cfg.on_sync_receive.call((const Variant **)argp, 4, ret, ce);
ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, "Custom sync function failed");
} else {
ERR_FAIL_COND_MSG(p_from != 1, "Default sync implementation only allow syncing from server to client");
_process_default_sync(id, p_packet, p_packet_len);
}
}
Error MultiplayerReplicator::_get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant) {
ERR_FAIL_COND_V_MSG(!p_obj, ERR_INVALID_PARAMETER, "Cannot encode null object");
for (const StringName &prop : p_properties) {
@ -306,6 +473,21 @@ Error MultiplayerReplicator::spawn_config(const ResourceUID::ID &p_id, Replicati
return OK;
}
Error MultiplayerReplicator::sync_config(const ResourceUID::ID &p_id, uint64_t p_interval, const TypedArray<StringName> &p_props, const Callable &p_on_send, const Callable &p_on_recv) {
ERR_FAIL_COND_V(!ResourceUID::get_singleton()->has_id(p_id), ERR_INVALID_PARAMETER);
ERR_FAIL_COND_V_MSG(p_on_send.is_valid() != p_on_recv.is_valid(), ERR_INVALID_PARAMETER, "Send and receive custom callables must be both valid or both empty");
ERR_FAIL_COND_V(!replications.has(p_id), ERR_UNCONFIGURED);
SceneConfig &cfg = replications[p_id];
ERR_FAIL_COND_V_MSG(p_interval && cfg.mode != REPLICATION_MODE_SERVER && !p_on_send.is_valid(), ERR_INVALID_PARAMETER, "Timed updates in custom mode are only allowed if custom callbacks are also specified");
for (int i = 0; i < p_props.size(); i++) {
cfg.sync_properties.push_back(p_props[i]);
}
cfg.on_sync_send = p_on_send;
cfg.on_sync_receive = p_on_recv;
cfg.sync_interval = p_interval * 1000;
return OK;
}
Error MultiplayerReplicator::_send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, bool p_spawn) {
int data_size = 0;
int is_raw = false;
@ -337,6 +519,7 @@ Error MultiplayerReplicator::_send_spawn_despawn(int p_peer_id, const ResourceUI
}
Error MultiplayerReplicator::send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, const NodePath &p_path) {
ERR_FAIL_COND_V(!multiplayer->has_network_peer(), ERR_UNCONFIGURED);
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.on_spawn_despawn_send.is_valid()) {
@ -357,6 +540,7 @@ Error MultiplayerReplicator::send_despawn(int p_peer_id, const ResourceUID::ID &
}
Error MultiplayerReplicator::send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, const NodePath &p_path) {
ERR_FAIL_COND_V(!multiplayer->has_network_peer(), ERR_UNCONFIGURED);
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.on_spawn_despawn_send.is_valid()) {
@ -408,13 +592,14 @@ Error MultiplayerReplicator::despawn(ResourceUID::ID p_scene_id, Object *p_obj,
return _spawn_despawn(p_scene_id, p_obj, p_peer, false);
}
PackedByteArray MultiplayerReplicator::encode_state(const ResourceUID::ID &p_scene_id, const Object *p_obj) {
PackedByteArray MultiplayerReplicator::encode_state(const ResourceUID::ID &p_scene_id, const Object *p_obj, bool p_initial) {
PackedByteArray state;
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), state, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
int len = 0;
List<Variant> state_vars;
Error err = _get_state(cfg.properties, p_obj, state_vars);
const List<StringName> props = p_initial ? cfg.properties : cfg.sync_properties;
Error err = _get_state(props, p_obj, state_vars);
ERR_FAIL_COND_V_MSG(err != OK, state, "Unable to retrieve object state.");
err = _encode_state(state_vars, nullptr, len);
ERR_FAIL_COND_V_MSG(err != OK, state, "Unable to encode object state.");
@ -423,11 +608,12 @@ PackedByteArray MultiplayerReplicator::encode_state(const ResourceUID::ID &p_sce
return state;
}
Error MultiplayerReplicator::decode_state(const ResourceUID::ID &p_scene_id, Object *p_obj, const PackedByteArray p_data) {
Error MultiplayerReplicator::decode_state(const ResourceUID::ID &p_scene_id, Object *p_obj, const PackedByteArray p_data, bool p_initial) {
ERR_FAIL_COND_V_MSG(!replications.has(p_scene_id), ERR_INVALID_PARAMETER, vformat("Spawnable not found: %d", p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
const List<StringName> props = p_initial ? cfg.properties : cfg.sync_properties;
int size;
return _decode_state(cfg.properties, p_obj, p_data.ptr(), p_data.size(), size);
return _decode_state(props, p_obj, p_data.ptr(), p_data.size(), size);
}
void MultiplayerReplicator::scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter) {
@ -448,12 +634,14 @@ void MultiplayerReplicator::scene_enter_exit_notify(const String &p_scene, Node
if (p_enter) {
if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_network_server()) {
replicated_nodes[p_node->get_instance_id()] = id;
_track(id, p_node);
spawn(id, p_node, 0);
}
emit_signal(SNAME("replicated_instance_added"), id, p_node);
} else {
if (cfg.mode == REPLICATION_MODE_SERVER && multiplayer->is_network_server() && replicated_nodes.has(p_node->get_instance_id())) {
replicated_nodes.erase(p_node->get_instance_id());
_untrack(id, p_node);
despawn(id, p_node, 0);
}
emit_signal(SNAME("replicated_instance_removed"), id, p_node);
@ -471,18 +659,119 @@ void MultiplayerReplicator::spawn_all(int p_peer) {
}
}
void MultiplayerReplicator::poll() {
for (KeyValue<ResourceUID::ID, SceneConfig> &E : replications) {
if (!E.value.sync_interval) {
continue;
}
if (E.value.mode == REPLICATION_MODE_SERVER && !multiplayer->is_network_server()) {
continue;
}
uint64_t time = OS::get_singleton()->get_ticks_usec();
if (E.value.sync_last + E.value.sync_interval <= time) {
sync_all(E.key, 0);
E.value.sync_last = time;
}
// Handle wrapping.
if (E.value.sync_last > time) {
E.value.sync_last = time;
}
}
}
void MultiplayerReplicator::track(const ResourceUID::ID &p_scene_id, Object *p_obj) {
ERR_FAIL_COND(!replications.has(p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
ERR_FAIL_COND_MSG(cfg.mode == REPLICATION_MODE_SERVER, "Manual object tracking is not allowed in server mode.");
_track(p_scene_id, p_obj);
}
void MultiplayerReplicator::_track(const ResourceUID::ID &p_scene_id, Object *p_obj) {
ERR_FAIL_COND(!p_obj);
ERR_FAIL_COND(!replications.has(p_scene_id));
if (!tracked_objects.has(p_scene_id)) {
tracked_objects[p_scene_id] = List<ObjectID>();
}
tracked_objects[p_scene_id].push_back(p_obj->get_instance_id());
}
void MultiplayerReplicator::untrack(const ResourceUID::ID &p_scene_id, Object *p_obj) {
ERR_FAIL_COND(!replications.has(p_scene_id));
const SceneConfig &cfg = replications[p_scene_id];
ERR_FAIL_COND_MSG(cfg.mode == REPLICATION_MODE_SERVER, "Manual object tracking is not allowed in server mode.");
_untrack(p_scene_id, p_obj);
}
void MultiplayerReplicator::_untrack(const ResourceUID::ID &p_scene_id, Object *p_obj) {
ERR_FAIL_COND(!p_obj);
ERR_FAIL_COND(!replications.has(p_scene_id));
if (tracked_objects.has(p_scene_id)) {
tracked_objects[p_scene_id].erase(p_obj->get_instance_id());
}
}
Error MultiplayerReplicator::sync_all(const ResourceUID::ID &p_scene_id, int p_peer) {
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
if (!tracked_objects.has(p_scene_id)) {
return OK;
}
const SceneConfig &cfg = replications[p_scene_id];
if (cfg.on_sync_send.is_valid()) {
Array objs;
if (tracked_objects.has(p_scene_id)) {
objs.resize(tracked_objects[p_scene_id].size());
int idx = 0;
for (const ObjectID &obj_id : tracked_objects[p_scene_id]) {
objs[idx++] = ObjectDB::get_instance(obj_id);
}
}
Variant args[3] = { p_scene_id, objs, p_peer };
Variant *argp[3] = { args, &args[1], &args[2] };
Callable::CallError ce;
Variant ret;
cfg.on_sync_send.call((const Variant **)argp, 3, ret, ce);
ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, FAILED, "Custom sync function failed");
return OK;
} else if (cfg.sync_properties.size()) {
return _sync_all_default(p_scene_id, p_peer);
}
return OK;
}
Error MultiplayerReplicator::send_sync(int p_peer_id, const ResourceUID::ID &p_scene_id, PackedByteArray p_data, MultiplayerPeer::TransferMode p_transfer_mode, int p_channel) {
ERR_FAIL_COND_V(!multiplayer->has_network_peer(), ERR_UNCONFIGURED);
ERR_FAIL_COND_V(!replications.has(p_scene_id), ERR_INVALID_PARAMETER);
const SceneConfig &cfg = replications[p_scene_id];
ERR_FAIL_COND_V_MSG(!cfg.on_sync_send.is_valid(), ERR_UNCONFIGURED, "Sending raw sync messages is only available with custom functions");
MAKE_ROOM(SYNC_CMD_OFFSET + p_data.size());
uint8_t *ptr = packet_cache.ptrw();
ptr[0] = MultiplayerAPI::NETWORK_COMMAND_SYNC;
encode_uint64(p_scene_id, &ptr[1]);
Ref<MultiplayerPeer> network_peer = multiplayer->get_network_peer();
network_peer->set_target_peer(p_peer_id);
network_peer->set_transfer_channel(p_channel);
network_peer->set_transfer_mode(p_transfer_mode);
return network_peer->put_packet(ptr, SYNC_CMD_OFFSET + p_data.size());
}
void MultiplayerReplicator::clear() {
tracked_objects.clear();
replicated_nodes.clear();
}
void MultiplayerReplicator::_bind_methods() {
ClassDB::bind_method(D_METHOD("spawn_config", "scene_id", "spawn_mode", "properties", "custom_send", "custom_receive"), &MultiplayerReplicator::spawn_config, DEFVAL(TypedArray<StringName>()), DEFVAL(Callable()), DEFVAL(Callable()));
ClassDB::bind_method(D_METHOD("sync_config", "scene_id", "interval", "properties", "custom_send", "custom_receive"), &MultiplayerReplicator::sync_config, DEFVAL(TypedArray<StringName>()), DEFVAL(Callable()), DEFVAL(Callable()));
ClassDB::bind_method(D_METHOD("despawn", "scene_id", "object", "peer_id"), &MultiplayerReplicator::despawn, DEFVAL(0));
ClassDB::bind_method(D_METHOD("spawn", "scene_id", "object", "peer_id"), &MultiplayerReplicator::spawn, DEFVAL(0));
ClassDB::bind_method(D_METHOD("send_despawn", "peer_id", "scene_id", "data", "path"), &MultiplayerReplicator::send_despawn, DEFVAL(Variant()), DEFVAL(NodePath()));
ClassDB::bind_method(D_METHOD("send_spawn", "peer_id", "scene_id", "data", "path"), &MultiplayerReplicator::send_spawn, DEFVAL(Variant()), DEFVAL(NodePath()));
ClassDB::bind_method(D_METHOD("encode_state", "scene_id", "object"), &MultiplayerReplicator::encode_state);
ClassDB::bind_method(D_METHOD("decode_state", "scene_id", "object", "data"), &MultiplayerReplicator::decode_state);
ClassDB::bind_method(D_METHOD("send_sync", "peer_id", "scene_id", "data", "transfer_mode", "channel"), &MultiplayerReplicator::send_sync, DEFVAL(MultiplayerPeer::TRANSFER_MODE_RELIABLE), DEFVAL(0));
ClassDB::bind_method(D_METHOD("sync_all", "scene_id", "peer_id"), &MultiplayerReplicator::sync_all, DEFVAL(0));
ClassDB::bind_method(D_METHOD("track", "scene_id", "object"), &MultiplayerReplicator::track);
ClassDB::bind_method(D_METHOD("untrack", "scene_id", "object"), &MultiplayerReplicator::untrack);
ClassDB::bind_method(D_METHOD("encode_state", "scene_id", "object", "initial"), &MultiplayerReplicator::encode_state, DEFVAL(true));
ClassDB::bind_method(D_METHOD("decode_state", "scene_id", "object", "data", "initial"), &MultiplayerReplicator::decode_state, DEFVAL(true));
ADD_SIGNAL(MethodInfo("despawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));
ADD_SIGNAL(MethodInfo("spawned", PropertyInfo(Variant::INT, "scene_id"), PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node")));

View file

@ -32,6 +32,8 @@
#define MULTIPLAYER_REPLICATOR_H
#include "core/io/multiplayer_api.h"
#include "core/templates/hash_map.h"
#include "core/variant/typed_array.h"
class MultiplayerReplicator : public Object {
@ -40,6 +42,7 @@ class MultiplayerReplicator : public Object {
public:
enum {
SPAWN_CMD_OFFSET = 9,
SYNC_CMD_OFFSET = 9,
};
enum ReplicationMode {
@ -50,9 +53,15 @@ public:
struct SceneConfig {
ReplicationMode mode;
uint64_t sync_interval = 0;
uint64_t sync_last = 0;
uint8_t sync_recv = 0;
List<StringName> properties;
List<StringName> sync_properties;
Callable on_spawn_despawn_send;
Callable on_spawn_despawn_receive;
Callable on_sync_send;
Callable on_sync_receive;
};
protected:
@ -63,31 +72,52 @@ private:
Vector<uint8_t> packet_cache;
Map<ResourceUID::ID, SceneConfig> replications;
Map<ObjectID, ResourceUID::ID> replicated_nodes;
HashMap<ResourceUID::ID, List<ObjectID>> tracked_objects;
// Encoding
Error _get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant);
Error _encode_state(const List<Variant> &p_variants, uint8_t *p_buffer, int &r_len, bool *r_raw = nullptr);
Error _decode_state(const List<StringName> &p_cfg, Object *p_obj, const uint8_t *p_buffer, int p_len, int &r_len, bool p_raw = false);
Error _get_state(const List<StringName> &p_properties, const Object *p_obj, List<Variant> &r_variant);
// Spawn
Error _spawn_despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer, bool p_spawn);
Error _send_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data, bool p_spawn);
void _process_default_spawn_despawn(int p_from, const ResourceUID::ID &p_scene_id, const uint8_t *p_packet, int p_packet_len, bool p_spawn);
Error _send_default_spawn_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, Object *p_obj, const NodePath &p_path, bool p_spawn);
// Sync
void _process_default_sync(const ResourceUID::ID &p_id, const uint8_t *p_packet, int p_packet_len);
Error _sync_all_default(const ResourceUID::ID &p_scene_id, int p_peer);
void _track(const ResourceUID::ID &p_scene_id, Object *p_object);
void _untrack(const ResourceUID::ID &p_scene_id, Object *p_object);
public:
void clear();
// Encoding
PackedByteArray encode_state(const ResourceUID::ID &p_scene_id, const Object *p_node, bool p_initial);
Error decode_state(const ResourceUID::ID &p_scene_id, Object *p_node, PackedByteArray p_data, bool p_initial);
// Spawn
Error spawn_config(const ResourceUID::ID &p_id, ReplicationMode p_mode, const TypedArray<StringName> &p_props = TypedArray<StringName>(), const Callable &p_on_send = Callable(), const Callable &p_on_recv = Callable());
Error spawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer = 0);
Error despawn(ResourceUID::ID p_scene_id, Object *p_obj, int p_peer = 0);
Error send_despawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data = Variant(), const NodePath &p_path = NodePath());
Error send_spawn(int p_peer_id, const ResourceUID::ID &p_scene_id, const Variant &p_data = Variant(), const NodePath &p_path = NodePath());
PackedByteArray encode_state(const ResourceUID::ID &p_scene_id, const Object *p_node);
Error decode_state(const ResourceUID::ID &p_scene_id, Object *p_node, PackedByteArray p_data);
// Sync
Error sync_config(const ResourceUID::ID &p_id, uint64_t p_interval, const TypedArray<StringName> &p_props = TypedArray<StringName>(), const Callable &p_on_send = Callable(), const Callable &p_on_recv = Callable());
Error sync_all(const ResourceUID::ID &p_scene_id, int p_peer);
Error send_sync(int p_peer_id, const ResourceUID::ID &p_scene_id, PackedByteArray p_data, MultiplayerPeer::TransferMode p_mode, int p_channel);
void track(const ResourceUID::ID &p_scene_id, Object *p_object);
void untrack(const ResourceUID::ID &p_scene_id, Object *p_object);
// Used by MultiplayerAPI
void spawn_all(int p_peer);
void process_spawn_despawn(int p_from, const uint8_t *p_packet, int p_packet_len, bool p_spawn);
void process_sync(int p_from, const uint8_t *p_packet, int p_packet_len);
void scene_enter_exit_notify(const String &p_scene, Node *p_node, bool p_enter);
void poll();
MultiplayerReplicator(MultiplayerAPI *p_multiplayer) {
multiplayer = p_multiplayer;

View file

@ -12,6 +12,7 @@
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<argument index="2" name="data" type="PackedByteArray" />
<argument index="3" name="initial" type="bool" default="true" />
<description>
Decode the given [code]data[/code] representing a spawnable state into [code]object[/code] using the configuration associated with the provided [code]scene_id[/code]. This function is called automatically when a client receives a server spawn for a scene with [constant REPLICATION_MODE_SERVER]. See [method spawn_config].
Tip: You may find this function useful in servers when parsing spawn requests from clients, or when implementing your own logic with [constant REPLICATION_MODE_CUSTOM].
@ -23,12 +24,14 @@
<argument index="1" name="object" type="Object" />
<argument index="2" name="peer_id" type="int" default="0" />
<description>
Request a despawn for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code]. This will either trigger the default behaviour, or invoke the custom spawn/despawn callables specified in [method spawn_config]. See [method send_despawn] for the default behavior.
</description>
</method>
<method name="encode_state">
<return type="PackedByteArray" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<argument index="2" name="initial" type="bool" default="true" />
<description>
Encode the given [code]object[/code] using the configuration associated with the provided [code]scene_id[/code]. This function is called automatically when the server spawns scenes with [constant REPLICATION_MODE_SERVER]. See [method spawn_config].
Tip: You may find this function useful when requesting spawns from clients to server, or when implementing your own logic with [constant REPLICATION_MODE_CUSTOM].
@ -54,12 +57,24 @@
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 REPLICATION_MODE_SERVER] (see [method spawn_config]) and the request is sent by the server (see [method MultiplayerAPI.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 spawned]. In all other cases no instantiation happens, and the signal [signal spawn_requested] is emitted instead.
</description>
</method>
<method name="send_sync">
<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="data" type="PackedByteArray" />
<argument index="3" name="transfer_mode" type="int" enum="MultiplayerPeer.TransferMode" default="2" />
<argument index="4" name="channel" type="int" default="0" />
<description>
Sends a sync request for the instances of the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code] (see [method MultiplayerPeer.set_target_peer]). This function can only be called manually when overriding the send and receive sync functions (see [method sync_config]).
</description>
</method>
<method name="spawn">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<argument index="2" name="peer_id" type="int" default="0" />
<description>
Request a spawn for the scene identified by [code]scene_id[/code] to the given [code]peer_id[/code]. This will either trigger the default behaviour, or invoke the custom spawn/despawn callables specified in [method spawn_config]. See [method send_spawn] for the default behavior.
</description>
</method>
<method name="spawn_config">
@ -70,10 +85,47 @@
<argument index="3" name="custom_send" type="Callable" />
<argument index="4" name="custom_receive" type="Callable" />
<description>
Configures the MultiplayerAPI to track instances of the [PackedScene] identified by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication. When [code]mode[/code] is [constant REPLICATION_MODE_SERVER], the specified [code]properties[/code] will also be replicated to clients during the initial spawn.
Configures the MultiplayerReplicator to track instances of the [PackedScene] identified by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication. When [code]mode[/code] is [constant REPLICATION_MODE_SERVER], the specified [code]properties[/code] will also be replicated to clients during the initial spawn. You can optionally specify a [code]custom_send[/code] and a [code]custom_receive[/code] to override the default behaviour and customize the spawn/despawn proecess.
Tip: You can use a custom property in the scene main script to return a customly optimized state representation.
</description>
</method>
<method name="sync_all">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="peer_id" type="int" default="0" />
<description>
Manually request a sync for all the instances of the scene identified by [code]scene_id[/code]. This function will trigger the default sync behaviour, or call your send custom send callable if specified in [method sync_config].
Note: The default implementation only allow syncing from server to clients.
</description>
</method>
<method name="sync_config">
<return type="int" enum="Error" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="interval" type="int" />
<argument index="2" name="properties" type="StringName[]" default="[]" />
<argument index="3" name="custom_send" type="Callable" />
<argument index="4" name="custom_receive" type="Callable" />
<description>
Configures the MultiplayerReplicator to sync instances of the [PackedScene] identified by [code]scene_id[/code] (see [method ResourceLoader.get_resource_uid]) for the purpose of network replication at the desired [code]interval[/code] (in milliseconds). The specified [code]properties[/code] will be part of the state sync. You can optionally specify a [code]custom_send[/code] and a [code]custom_receive[/code] to override the default behaviour and customize the syncronization proecess.
Tip: You can use a custom property in the scene main script to return a customly optimized state representation (having a single property that returns a PackedByteArray is higly recommended when dealing with many instances).
</description>
</method>
<method name="track">
<return type="void" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<description>
Track the given [code]object[/code] as an instance of the scene identified by [code]scene_id[/code]. This object will be passed to your custom sync callables (see [method sync_config]). Tracking and untracking is automatic in [constant REPLICATION_MODE_SERVER].
</description>
</method>
<method name="untrack">
<return type="void" />
<argument index="0" name="scene_id" type="int" />
<argument index="1" name="object" type="Object" />
<description>
Untrack the given [code]object[/code]. This object will no longer be passed to your custom sync callables (see [method sync_config]). Tracking and untracking is automatic in [constant REPLICATION_MODE_SERVER].
</description>
</method>
</methods>
<signals>
<signal name="despawn_requested">