diff --git a/Source/Engine/Networking/NetworkMessage.h b/Source/Engine/Networking/NetworkMessage.h
index aa610f058..4d0bc4330 100644
--- a/Source/Engine/Networking/NetworkMessage.h
+++ b/Source/Engine/Networking/NetworkMessage.h
@@ -83,13 +83,26 @@ public:
/// Should be of the same length as length or longer.
///
/// The minimal amount of bytes that the buffer contains.
- FORCE_INLINE void ReadBytes(uint8* bytes, const int numBytes)
+ FORCE_INLINE void ReadBytes(uint8* bytes, const int32 numBytes)
{
ASSERT(Position + numBytes < BufferSize);
Platform::MemoryCopy(bytes, Buffer + Position, numBytes);
Position += numBytes;
}
+ ///
+ /// Skips bytes from the message.
+ ///
+ /// Amount of bytes to skip.
+ /// Pointer to skipped data beginning.
+ FORCE_INLINE void* SkipBytes(const int32 numBytes)
+ {
+ ASSERT(Position + numBytes < BufferSize);
+ byte* result = Buffer + Position;
+ Position += numBytes;
+ return result;
+ }
+
template
FORCE_INLINE void WriteStructure(const T& data)
{
@@ -225,7 +238,7 @@ public:
///
FORCE_INLINE Guid ReadGuid()
{
- Guid value = Guid();
+ Guid value;
ReadBytes((uint8*)&value, sizeof(Guid));
return value;
}
diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp
index 7a32f767d..f08f13da1 100644
--- a/Source/Engine/Networking/NetworkReplicator.cpp
+++ b/Source/Engine/Networking/NetworkReplicator.cpp
@@ -15,6 +15,7 @@
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/HashSet.h"
#include "Engine/Core/Collections/Dictionary.h"
+#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Core/Types/DataContainer.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Engine/EngineService.h"
@@ -51,11 +52,16 @@ PACK_STRUCT(struct NetworkMessageObjectReplicate
PACK_STRUCT(struct NetworkMessageObjectSpawn
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawn;
+ uint32 OwnerClientId;
+ Guid PrefabId;
+ uint16 ItemsCount;
+ });
+
+PACK_STRUCT(struct NetworkMessageObjectSpawnItem
+ {
Guid ObjectId;
Guid ParentId;
- Guid PrefabId;
Guid PrefabObjectID;
- uint32 OwnerClientId;
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
});
@@ -135,6 +141,11 @@ struct SpawnItem
NetworkObjectRole Role;
};
+struct SpawnGroup
+{
+ Array> Items;
+};
+
struct DespawnItem
{
Guid Id;
@@ -309,35 +320,54 @@ FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name)
buffer[name.Length()] = 0;
}
-void SendObjectSpawnMessage(const NetworkReplicatedObject& item, ScriptingObject* obj)
+void SendObjectSpawnMessage(const SpawnGroup& group, const Array& clients)
{
- NetworkMessageObjectSpawn msgData;
- msgData.ObjectId = item.ObjectId;
- msgData.ParentId = item.ParentId;
const bool isClient = NetworkManager::IsClient();
- if (isClient)
- {
- // Remap local client object ids into server ids
- IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
- IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId);
- }
- msgData.PrefabId = Guid::Empty;
- msgData.PrefabObjectID = Guid::Empty;
- auto* objScene = ScriptingObject::Cast(obj);
- if (objScene && objScene->HasPrefabLink())
- {
- msgData.PrefabId = objScene->GetPrefabID();
- msgData.PrefabObjectID = objScene->GetPrefabObjectID();
- }
- msgData.OwnerClientId = item.OwnerClientId;
- GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname);
auto* peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
+ NetworkMessageObjectSpawn msgData;
+ msgData.ItemsCount = group.Items.Count();
+ {
+ // The first object is a root of the group (eg. prefab instance root actor)
+ SpawnItem* e = group.Items[0];
+ ScriptingObject* obj = e->Object.Get();
+ msgData.OwnerClientId = e->OwnerClientId;
+ auto* objScene = ScriptingObject::Cast(obj);
+ msgData.PrefabId = objScene && objScene->HasPrefabLink() ? objScene->GetPrefabID() : Guid::Empty;
+
+ // Setup clients that should receive this spawn message
+ auto it = Objects.Find(obj->GetID());
+ auto& item = it->Item;
+ BuildCachedTargets(clients, item.TargetClientIds);
+ }
msg.WriteStructure(msgData);
+ for (SpawnItem* e : group.Items)
+ {
+ ScriptingObject* obj = e->Object.Get();
+ auto it = Objects.Find(obj->GetID());
+ auto& item = it->Item;
+
+ // Add object into spawn message
+ NetworkMessageObjectSpawnItem msgDataItem;
+ msgDataItem.ObjectId = item.ObjectId;
+ msgDataItem.ParentId = item.ParentId;
+ if (isClient)
+ {
+ // Remap local client object ids into server ids
+ IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
+ IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
+ }
+ msgDataItem.PrefabObjectID = Guid::Empty;
+ auto* objScene = ScriptingObject::Cast(obj);
+ if (objScene && objScene->HasPrefabLink())
+ msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
+ GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
+ msg.WriteStructure(msgDataItem);
+ }
if (isClient)
- peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
else
- peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
}
void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkClient* excludedClient = nullptr)
@@ -394,6 +424,44 @@ SceneObject* FindPrefabObject(Actor* a, const Guid& prefabObjectId)
return result;
}
+void SetupObjectSpawnGroupItem(ScriptingObject* obj, Array>& spawnGroups, SpawnItem& spawnItem)
+{
+ // Check if can fit this object into any of the existing groups (eg. script which can be spawned with parent actor)
+ SpawnGroup* group = nullptr;
+ for (auto& g : spawnGroups)
+ {
+ ScriptingObject* groupRoot = g.Items[0]->Object.Get();
+ if (IsParentOf(obj, groupRoot))
+ {
+ // Reuse existing group (append)
+ g.Items.Add(&spawnItem);
+ group = &g;
+ break;
+ }
+ }
+ if (group)
+ return;
+
+ // Check if can override any of the existing groups (eg. actor which should be spawned before scripts)
+ for (auto& g : spawnGroups)
+ {
+ ScriptingObject* groupRoot = g.Items[0]->Object.Get();
+ if (IsParentOf(groupRoot, obj))
+ {
+ // Reuse existing group (as a root)
+ g.Items.Insert(0, &spawnItem);
+ group = &g;
+ break;
+ }
+ }
+ if (group)
+ return;
+
+ // Create new group
+ group = &spawnGroups.AddOne();
+ group->Items.Add(&spawnItem);
+}
+
#if !COMPILE_WITHOUT_CSHARP
#include "Engine/Scripting/ManagedCLR/MUtils.h"
@@ -814,14 +882,29 @@ void NetworkInternal::NetworkReplicatorUpdate()
// Sync any previously spawned objects with late-joining clients
PROFILE_CPU_NAMED("NewClients");
// TODO: try iterative loop over several frames to reduce both server and client perf-spikes in case of large amount of spawned objects
+ ChunkedArray spawnItems;
+ Array> spawnGroups;
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (!obj || !item.Spawned)
continue;
- BuildCachedTargets(NewClients, item.TargetClientIds);
- SendObjectSpawnMessage(item, obj);
+
+ // Setup spawn item for this object
+ auto& spawnItem = spawnItems.AddOne();
+ spawnItem.Object = obj;
+ spawnItem.Targets.Link(item.TargetClientIds);
+ spawnItem.OwnerClientId = item.OwnerClientId;
+ spawnItem.Role = item.Role;
+
+ SetupObjectSpawnGroupItem(obj, spawnGroups, spawnItem);
+ }
+
+ // Groups of objects to spawn
+ for (SpawnGroup& g : spawnGroups)
+ {
+ SendObjectSpawnMessage(g, NewClients);
}
NewClients.Clear();
}
@@ -865,9 +948,11 @@ void NetworkInternal::NetworkReplicatorUpdate()
if (SpawnQueue.Count() != 0)
{
PROFILE_CPU_NAMED("SpawnQueue");
+
+ // Propagate hierarchical ownership from spawned parent to spawned child objects (eg. spawned script and spawned actor with set hierarchical ownership on actor which should affect script too)
+ // TODO: maybe we can propagate ownership within spawn groups only?
for (SpawnItem& e : SpawnQueue)
{
- // Propagate hierarchical ownership from spawned parent to spawned child objects (eg. spawned script and spawned actor with set hierarchical ownership on actor which should affect script too)
if (e.HasOwnership && e.HierarchicalOwnership)
{
for (auto& q : SpawnQueue)
@@ -881,6 +966,10 @@ void NetworkInternal::NetworkReplicatorUpdate()
}
}
}
+
+ // Batch spawned objects into groups (eg. player actor with scripts and child actors merged as a single spawn message)
+ // That's because NetworkReplicator::SpawnObject can be called in separate for different actors/scripts of a single prefab instance but we want to spawn it at once over the network
+ Array> spawnGroups;
for (SpawnItem& e : SpawnQueue)
{
ScriptingObject* obj = e.Object.Get();
@@ -913,11 +1002,16 @@ void NetworkInternal::NetworkReplicatorUpdate()
MISSING_CODE("Sending TargetClientIds over to server for partial object replication.");
item.TargetClientIds = MoveTemp(e.Targets);
}
-
- NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Spawn object ID={}", item.ToString());
- BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds);
- SendObjectSpawnMessage(item, obj);
item.Spawned = true;
+ NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Spawn object ID={}", item.ToString());
+
+ SetupObjectSpawnGroupItem(obj, spawnGroups, e);
+ }
+
+ // Spawn groups of objects
+ for (SpawnGroup& g : spawnGroups)
+ {
+ SendObjectSpawnMessage(g, NetworkManager::Clients);
}
SpawnQueue.Clear();
}
@@ -1085,102 +1179,135 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
{
NetworkMessageObjectSpawn msgData;
event.Message.ReadStructure(msgData);
+ auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
+ if (msgData.ItemsCount == 0)
+ return;
ScopeLock lock(ObjectsLock);
- NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.ObjectTypeName);
- if (e)
+
+ // Check if that object has been already spawned
+ auto& rootItem = msgDataItems[0];
+ NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
+ if (root)
{
- auto& item = *e;
- item.Spawned = true;
- if (NetworkManager::IsClient())
+ // Object already exists locally so just synchronize the ownership (and mark as spawned)
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
{
- // Server always knows the best so update ownership of the existing object
- item.OwnerClientId = msgData.OwnerClientId;
- if (item.Role == NetworkObjectRole::OwnedAuthoritative)
- item.Role = NetworkObjectRole::Replicated;
- }
- else if (item.OwnerClientId != msgData.OwnerClientId)
- {
- // Other client spawned object with a different owner
- // TODO: send reply message to inform about proper object ownership that client
- }
- }
- else
- {
- // Recreate object locally
- ScriptingObject* obj = nullptr;
- const NetworkReplicatedObject* parent = ResolveObject(msgData.ParentId);
- if (msgData.PrefabId.IsValid())
- {
- Actor* prefabInstance = nullptr;
- Actor* parentActor = parent && parent->Object && parent->Object->Is() ? parent->Object.As() : nullptr;
- if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
+ auto& msgDataItem = msgDataItems[i];
+ NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
+ auto& item = *e;
+ item.Spawned = true;
+ if (NetworkManager::IsClient())
{
- // Reuse parent object as prefab instance
- prefabInstance = parentActor;
+ // Server always knows the best so update ownership of the existing object
+ item.OwnerClientId = msgData.OwnerClientId;
+ if (item.Role == NetworkObjectRole::OwnedAuthoritative)
+ item.Role = NetworkObjectRole::Replicated;
}
- else if (parentActor = Scripting::TryFindObject(msgData.ParentId))
+ else if (item.OwnerClientId != msgData.OwnerClientId)
{
- // Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
- for (Actor* child : parentActor->Children)
+ // Other client spawned object with a different owner
+ // TODO: send reply message to inform about proper object ownership that client
+ }
+ }
+ return;
+ }
+
+ // Recreate object locally (spawn only root)
+ ScriptingObject* obj = nullptr;
+ Actor* prefabInstance = nullptr;
+ if (msgData.PrefabId.IsValid())
+ {
+ const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
+ Actor* parentActor = parent && parent->Object && parent->Object->Is() ? parent->Object.As() : nullptr;
+ if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
+ {
+ // Reuse parent object as prefab instance
+ prefabInstance = parentActor;
+ }
+ else if (parentActor = Scripting::TryFindObject(rootItem.ParentId))
+ {
+ // Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
+ for (Actor* child : parentActor->Children)
+ {
+ if (child->GetPrefabID() == msgData.PrefabId)
{
- if (child->GetPrefabID() == msgData.PrefabId)
+ if (Objects.Contains(child->GetID()))
{
- if (Objects.Contains(child->GetID()))
+ obj = FindPrefabObject(child, rootItem.PrefabObjectID);
+ if (Objects.Contains(obj->GetID()))
{
- obj = FindPrefabObject(child, msgData.PrefabObjectID);
- if (Objects.Contains(obj->GetID()))
- {
- // Other instance with already spawned network object
- obj = nullptr;
- }
- else
- {
- // Reuse already spawned object within a parent
- prefabInstance = child;
- break;
- }
+ // Other instance with already spawned network object
+ obj = nullptr;
+ }
+ else
+ {
+ // Reuse already spawned object within a parent
+ prefabInstance = child;
+ break;
}
}
}
}
+ }
+ if (!prefabInstance)
+ {
+ // Spawn prefab
+ auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
+ if (!prefab)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
+ return;
+ }
+ prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
if (!prefabInstance)
{
- // Spawn prefab
- auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
- if (!prefab)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
- return;
- }
- prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
- if (!prefabInstance)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
- return;
- }
- }
- if (!obj)
- obj = FindPrefabObject(prefabInstance, msgData.PrefabObjectID);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgData.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
- Delete(prefabInstance);
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
return;
}
}
- else
+ if (!obj)
+ obj = FindPrefabObject(prefabInstance, rootItem.PrefabObjectID);
+ if (!obj)
{
- // Spawn object
- const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgData.ObjectTypeName);
- obj = ScriptingObject::NewObject(objectType);
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", rootItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
+ Delete(prefabInstance);
+ return;
+ }
+ }
+ else
+ {
+ // Spawn object
+ if (msgData.ItemsCount != 1)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Only prefab object spawning can contain more than one object (for type {})", String(rootItem.ObjectTypeName));
+ return;
+ }
+ const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
+ obj = ScriptingObject::NewObject(objectType);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
+ return;
+ }
+ }
+
+ // Setup all newly spawned objects
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ if (i != 0)
+ {
+ obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
if (!obj)
{
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgData.ObjectTypeName));
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
+ Delete(prefabInstance);
return;
}
}
if (!obj->IsRegistered())
obj->RegisterObject();
+ const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
// Add object to the list
NetworkReplicatedObject item;
@@ -1200,24 +1327,27 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
Objects.Add(MoveTemp(item));
// Boost future lookups by using indirection
- NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgData.ObjectId, item.ToString(), obj->GetType().ToString());
- IdsRemappingTable.Add(msgData.ObjectId, item.ObjectId);
+ NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString());
+ IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId);
// Automatic parenting for scene objects
auto sceneObject = ScriptingObject::Cast(obj);
if (sceneObject)
{
- if (parent && parent->Object.Get() && parent->Object->Is())
+ if (parent && parent
+ ->
+ Object.Get() && parent->Object->Is()
+ )
sceneObject->SetParent(parent->Object.As());
- else if (auto* parentActor = Scripting::TryFindObject(msgData.ParentId))
+ else if (auto* parentActor = Scripting::TryFindObject(msgDataItem.ParentId))
sceneObject->SetParent(parentActor);
}
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkSpawn();
-
- // TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
}
+
+ // TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
}
void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)