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)