diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index d1c2a57d7..de710cb7d 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -310,6 +310,10 @@ void Asset::ChangeID(const Guid& newId) if (!IsVirtual()) CRASH; + // ID has to be unique + if (Content::GetAsset(newId) != nullptr) + CRASH; + const Guid oldId = _id; ManagedScriptingObject::ChangeID(newId); Content::onAssetChangeId(this, oldId, newId); diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 5f1432d31..cc91e48ca 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -934,13 +934,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou // Loaded scene objects list CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); - const int32 objectsCount = (int32)data.Size(); - sceneObjects->Resize(objectsCount); + const int32 dataCount = (int32)data.Size(); + sceneObjects->Resize(dataCount); sceneObjects->At(0) = scene; // Spawn all scene objects SceneObjectsFactory::Context context(modifier.Value); - context.Async = JobSystem::GetThreadsCount() > 1 && objectsCount > 10; + context.Async = JobSystem::GetThreadsCount() > 1 && dataCount > 10; { PROFILE_CPU_NAMED("Spawn"); SceneObject** objects = sceneObjects->Get(); @@ -963,12 +963,12 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou } else SceneObjectsFactory::HandleObjectDeserializationError(stream); - }, objectsCount - 1); + }, dataCount - 1); ScenesLock.Lock(); } else { - for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene + for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene { auto& stream = data[i]; auto obj = SceneObjectsFactory::Spawn(context, stream); @@ -1012,13 +1012,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou SceneObjectsFactory::Deserialize(context, obj, data[i]); idMapping = nullptr; } - }, objectsCount - 1); + }, dataCount - 1); ScenesLock.Lock(); } else { Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); - for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene + for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene { auto& objData = data[i]; auto obj = objects[i]; @@ -1049,7 +1049,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou PROFILE_CPU_NAMED("Initialize"); SceneObject** objects = sceneObjects->Get(); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { SceneObject* obj = objects[i]; if (obj) diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index 56bf6a1e2..25a2bd086 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -87,17 +87,12 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId) const auto result = GetDefaultInstance(); if (!result) return nullptr; - if (objectId.IsValid()) { SceneObject* object; if (ObjectsCache.TryGet(objectId, object)) - { - // Actor or Script return object; - } } - return result; } diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index d014f58d7..d99a89e39 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -94,8 +94,8 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac LOG(Warning, "Waiting for prefab asset be loaded failed. {0}", prefab->ToString()); return nullptr; } - const int32 objectsCount = prefab->ObjectsCount; - if (objectsCount == 0) + const int32 dataCount = prefab->ObjectsCount; + if (dataCount == 0) { LOG(Warning, "Prefab has no objects. {0}", prefab->ToString()); return nullptr; @@ -107,7 +107,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac // Prepare CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); - sceneObjects->Resize(objectsCount); + sceneObjects->Resize(dataCount); CollectionPoolCache::ScopeCache modifier = Cache::ISerializeModifier.Get(); modifier->EngineBuild = prefab->DataEngineBuild; modifier->IdsMapping.EnsureCapacity(prefab->ObjectsIds.Count() * 4); @@ -126,7 +126,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac // Deserialize prefab objects auto prevIdMapping = Scripting::ObjectsLookupIdMapping.Get(); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = SceneObjectsFactory::Spawn(context, stream); @@ -145,7 +145,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); } - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = sceneObjects->At(i); @@ -154,20 +154,29 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac } Scripting::ObjectsLookupIdMapping.Set(prevIdMapping); - // Pick prefab root object - if (sceneObjects->IsEmpty()) + // Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it) + if (withSynchronization) { - LOG(Warning, "No valid objects in prefab. {0}", prefab->ToString()); - return nullptr; + // TODO: resave and force sync scenes during game cooking so this step could be skipped in game + SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData); } + + // Pick prefab root object Actor* root = nullptr; const Guid prefabRootObjectId = prefab->GetRootObjectId(); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount && !root; i++) { if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId) - { root = dynamic_cast(sceneObjects->At(i)); - break; + } + if (!root) + { + // Fallback to the first actor that has no parent + for (int32 i = 0; i < sceneObjects->Count() && !root; i++) + { + SceneObject* obj = sceneObjects->At(i); + if (obj && !obj->GetParent()) + root = dynamic_cast(obj); } } if (!root) @@ -176,13 +185,6 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac return nullptr; } - // Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it) - if (withSynchronization) - { - // TODO: resave and force sync scenes during game cooking so this step could be skipped in game - SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData); - } - // Prepare parent linkage for prefab root actor if (root->_parent) root->_parent->Children.Remove(root); @@ -264,7 +266,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac } // Link objects to prefab (only deserialized from prefab data) - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = sceneObjects->At(i); diff --git a/Source/Engine/Level/Prefabs/PrefabManager.h b/Source/Engine/Level/Prefabs/PrefabManager.h index e5600bac4..ad990da9f 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.h +++ b/Source/Engine/Level/Prefabs/PrefabManager.h @@ -89,7 +89,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = true); /// /// Spawns the instance of the prefab objects. If parent actor is specified then created actors are fully initialized (OnLoad event and BeginPlay is called if parent actor is already during gameplay). @@ -100,7 +100,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = true); #if USE_EDITOR diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index df30257f9..eeac38ccf 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -138,7 +138,7 @@ SceneObject* SceneObjectsFactory::Spawn(Context& context, const ISerializable::D auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); return nullptr; } if (prefab->WaitForLoaded()) @@ -264,7 +264,7 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); return; } if (prefab->WaitForLoaded()) @@ -424,9 +424,6 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn ASSERT(count <= data.SceneObjects.Count()); for (int32 i = 0; i < count; i++) { - const SceneObject* obj = data.SceneObjects[i]; - if (!obj) - continue; const auto& stream = data.Data[i]; Guid prefabObjectId, prefabId; if (!JsonTools::GetGuidIfValid(prefabObjectId, stream, "PrefabObjectID")) @@ -436,14 +433,15 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn Guid parentId = JsonTools::GetGuid(stream, "ParentID"); for (int32 j = i - 1; j >= 0; j--) { - // Find instance ID of the parent to this object (use data in json for relationship) + // Find ID of the parent to this object (use data in json for relationship) if (parentId == JsonTools::GetGuid(data.Data[j], "ID") && data.SceneObjects[j]) { parentId = data.SceneObjects[j]->GetID(); break; } } - const Guid id = obj->GetID(); + const SceneObject* obj = data.SceneObjects[i]; + const Guid id = obj ? obj->GetID() : JsonTools::GetGuid(stream, "ID"); auto prefab = Content::LoadAsync(prefabId); // Check if it's parent is in the same prefab @@ -459,6 +457,8 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn auto& e = context.Instances.AddOne(); e.Prefab = prefab; e.RootId = id; + e.RootIndex = i; + e.StatIndex = i; } context.ObjectToInstance[id] = index; @@ -490,6 +490,85 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); data.InitialCount = data.SceneObjects.Count(); + // Recreate any missing prefab root objects that were deleted (eg. spawned prefab got its root changed and deleted so old prefab instance needs to respawn it) + for (int32 instanceIndex = 0; instanceIndex < context.Instances.Count(); instanceIndex++) + { + PrefabInstance& instance = context.Instances[instanceIndex]; + SceneObject* root = data.SceneObjects[instance.RootIndex]; + if (!root && instance.Prefab) + { + instance.FixRootParent = true; + + // Check if current prefab root existed in the deserialized data + const auto& oldRootData = data.Data[instance.RootIndex]; + const Guid oldRootId = JsonTools::GetGuid(oldRootData, "ID"); + const Guid prefabObjectId = JsonTools::GetGuid(oldRootData, "PrefabObjectID"); + const Guid prefabRootId = instance.Prefab->GetRootObjectId(); + Guid id; + int32 idInstance = -1; + bool syncNewRoot = false; + if (instance.IdsMapping.TryGet(prefabRootId, id) && context.ObjectToInstance.TryGet(id, idInstance) && idInstance == instanceIndex) + { + // Update the missing root with the valid object from this prefab instance + LOG(Warning, "Changed prefab instance root from ID={0}, PrefabObjectID={1} to ID={2}, PrefabObjectID={3} ({4})", instance.RootId, prefabObjectId, id, prefabRootId, instance.Prefab->ToString()); + } + else + { + LOG(Warning, "Missing prefab instance root (ID={0}, PrefabObjectID={1}, {2})", instance.RootId, prefabObjectId, instance.Prefab->ToString()); + + // Get prefab object data from the prefab + const ISerializable::DeserializeStream* prefabData; + if (!instance.Prefab->ObjectsDataCache.TryGet(prefabRootId, prefabData)) + { + LOG(Warning, "Missing object {1} data in prefab {0}.", instance.Prefab->ToString(), prefabObjectId); + continue; + } + + // Map prefab object ID to the new prefab object instance + id = Guid::New(); + data.Modifier->IdsMapping[prefabRootId] = id; + + // Create prefab instance (recursive prefab loading to support nested prefabs) + root = Spawn(context, *prefabData); + if (!root) + { + LOG(Warning, "Failed to create object {1} from prefab {0}.", instance.Prefab->ToString(), prefabRootId); + continue; + } + + // Register object + root->RegisterObject(); + data.SceneObjects.Add(root); + auto& newObj = data.NewObjects.AddOne(); + newObj.Prefab = instance.Prefab; + newObj.PrefabData = prefabData; + newObj.PrefabObjectId = prefabRootId; + newObj.Id = id; + context.ObjectToInstance[id] = instanceIndex; + syncNewRoot = true; + } + + // Update prefab root info + instance.RootId = id; + instance.RootIndex = data.SceneObjects.Find(Scripting::FindObject(id)); + CHECK(instance.RootIndex != -1); + + // Remap removed prefab root into the current root (can be different type but is needed for proper hierarchy linkage) + instance.IdsMapping[prefabObjectId] = id; + instance.IdsMapping[prefabRootId] = id; + instance.IdsMapping[oldRootId] = id; + data.Modifier->IdsMapping[prefabObjectId] = id; + data.Modifier->IdsMapping[prefabRootId] = id; + data.Modifier->IdsMapping[oldRootId] = id; + + // Add any sub-objects that are missing (in case new root was created) + if (syncNewRoot) + { + SynchronizeNewPrefabInstances(context, data, instance.Prefab, (Actor*)root, prefabRootId, instance.RootIndex, oldRootData); + } + } + } + // Check all actors with prefab linkage for adding missing objects for (int32 i = 0; i < data.InitialCount; i++) { @@ -504,13 +583,12 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab continue; if (!JsonTools::GetGuidIfValid(actorId, stream, "ID")) continue; - const Guid actorParentId = JsonTools::GetGuid(stream, "ParentID"); // Load prefab auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); continue; } if (prefab->WaitForLoaded()) @@ -519,66 +597,7 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab continue; } - // Check for RemovedObjects list - const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects"); - - // Check if the given actor has new children or scripts added (inside the prefab that it uses) - // TODO: consider caching prefab objects structure maybe to boost this logic? - for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it) - { - // Use only objects that are linked to the current actor - const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID"); - if (parentId != actorPrefabObjectId) - continue; - - // Skip if object was marked to be removed per instance - const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID"); - if (removedObjects != stream.MemberEnd()) - { - auto& list = removedObjects->value; - const int32 size = static_cast(list.Size()); - bool removed = false; - for (int32 j = 0; j < size; j++) - { - if (JsonTools::GetGuid(list[j]) == prefabObjectId) - { - removed = true; - break; - } - } - if (removed) - continue; - } - - // Use only objects that are missing - bool spawned = false; - for (int32 j = i + 1; j < data.InitialCount; j++) - { - const auto& jData = data.Data[j]; - const Guid jParentId = JsonTools::GetGuid(jData, "ParentID"); - //if (jParentId == actorParentId) - // break; - //if (jParentId != actorId) - // continue; - const Guid jPrefabObjectId = JsonTools::GetGuid(jData, "PrefabObjectID"); - if (jPrefabObjectId != prefabObjectId) - continue; - - // This object exists in the saved scene objects list - spawned = true; - break; - } - if (spawned) - continue; - - // Map prefab object id to this actor's prefab instance so the new objects gets added to it - context.SetupIdsMapping(actor, data.Modifier); - data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID(); - Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); - - // Create instance (including all children) - SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId); - } + SynchronizeNewPrefabInstances(context, data, prefab, actor, actorPrefabObjectId, i, stream); } Scripting::ObjectsLookupIdMapping.Set(nullptr); @@ -605,7 +624,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); continue; } if (prefab->WaitForLoaded()) @@ -626,7 +645,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn // Invalid connection object found! LOG(Info, "Object {0} has invalid parent object {4} -> {5} (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", obj->GetSceneObjectId(), prefabObjectId, prefab->GetID(), prefab->GetPath(), parentPrefabObjectId, actualParentPrefabId); - // Map actual prefab object id to the current scene objects collection + // Map actual prefab object ID to the current scene objects collection context.SetupIdsMapping(obj, data.Modifier); data.Modifier->IdsMapping.TryGet(actualParentPrefabId, actualParentPrefabId); @@ -646,15 +665,12 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn if (i != 0) { const auto defaultInstance = prefab->GetDefaultInstance(obj->GetPrefabObjectID()); - if (defaultInstance) - { - obj->SetOrderInParent(defaultInstance->GetOrderInParent()); - } + const int32 order = defaultInstance ? defaultInstance->GetOrderInParent() : -1; + if (order != -1) + obj->SetOrderInParent(order); } } - Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); - // Synchronize new prefab objects for (int32 i = 0; i < data.NewObjects.Count(); i++) { @@ -662,20 +678,97 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn auto& newObj = data.NewObjects[i]; // Deserialize object with prefab data + Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); Deserialize(context, obj, *(ISerializable::DeserializeStream*)newObj.PrefabData); obj->LinkPrefab(newObj.Prefab->GetID(), newObj.PrefabObjectId); // Preserve order in parent (values from prefab are used) const auto defaultInstance = newObj.Prefab->GetDefaultInstance(newObj.PrefabObjectId); - if (defaultInstance) + const int32 order = defaultInstance ? defaultInstance->GetOrderInParent() : -1; + if (order != -1) + obj->SetOrderInParent(order); + } + + // Setup hierarchy for the prefab instances (ensure any new objects are connected) + for (const auto& instance : context.Instances) + { + const auto& prefabStartData = data.Data[instance.StatIndex]; + Guid prefabStartParentId; + if (instance.FixRootParent && JsonTools::GetGuidIfValid(prefabStartParentId, prefabStartData, "ParentID")) { - obj->SetOrderInParent(defaultInstance->GetOrderInParent()); + auto* root = data.SceneObjects[instance.RootIndex]; + const auto rootParent = Scripting::FindObject(prefabStartParentId); + root->SetParent(rootParent, false); } } Scripting::ObjectsLookupIdMapping.Set(nullptr); } +void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream) +{ + // Check for RemovedObjects list + const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects"); + + // Check if the given actor has new children or scripts added (inside the prefab that it uses) + // TODO: consider caching prefab objects structure maybe to boost this logic? + for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it) + { + // Use only objects that are linked to the current actor + const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID"); + if (parentId != actorPrefabObjectId) + continue; + + // Skip if object was marked to be removed per instance + const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID"); + if (removedObjects != stream.MemberEnd()) + { + auto& list = removedObjects->value; + const int32 size = static_cast(list.Size()); + bool removed = false; + for (int32 j = 0; j < size; j++) + { + if (JsonTools::GetGuid(list[j]) == prefabObjectId) + { + removed = true; + break; + } + } + if (removed) + continue; + } + + // Use only objects that are missing + bool spawned = false; + int32 childSearchStart = i + 1; // Objects are serialized with parent followed by its children + int32 instanceIndex = -1; + if (context.ObjectToInstance.TryGet(actor->GetID(), instanceIndex) && context.Instances[instanceIndex].Prefab == prefab) + { + // Start searching from the beginning of that prefab instance (eg. in case prefab objects were reordered) + childSearchStart = Math::Min(childSearchStart, context.Instances[instanceIndex].StatIndex); + } + for (int32 j = childSearchStart; j < data.InitialCount; j++) + { + if (JsonTools::GetGuid(data.Data[j], "PrefabObjectID") == prefabObjectId) + { + // This object exists in the saved scene objects list + spawned = true; + break; + } + } + if (spawned) + continue; + + // Map prefab object ID to this actor's prefab instance so the new objects gets added to it + context.SetupIdsMapping(actor, data.Modifier); + data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID(); + Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); + + // Create instance (including all children) + SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId); + } +} + void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId) { PROFILE_CPU_NAMED("SynchronizeNewPrefabInstance"); diff --git a/Source/Engine/Level/SceneObjectsFactory.h b/Source/Engine/Level/SceneObjectsFactory.h index 86fd5364a..e152c21b2 100644 --- a/Source/Engine/Level/SceneObjectsFactory.h +++ b/Source/Engine/Level/SceneObjectsFactory.h @@ -15,8 +15,11 @@ class FLAXENGINE_API SceneObjectsFactory public: struct PrefabInstance { + int32 StatIndex; + int32 RootIndex; Guid RootId; Prefab* Prefab; + bool FixRootParent = false; Dictionary IdsMapping; }; @@ -70,6 +73,8 @@ public: struct PrefabSyncData { friend SceneObjectsFactory; + friend class PrefabManager; + // The created scene objects. Collection can be modified (eg. for spawning missing objects). Array& SceneObjects; // The scene objects data. @@ -124,5 +129,6 @@ public: static void SynchronizePrefabInstances(Context& context, PrefabSyncData& data); private: + static void SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream); static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId); }; diff --git a/Source/Engine/Serialization/JsonWriter.cpp b/Source/Engine/Serialization/JsonWriter.cpp index 35e4412d4..27ecb672b 100644 --- a/Source/Engine/Serialization/JsonWriter.cpp +++ b/Source/Engine/Serialization/JsonWriter.cpp @@ -481,7 +481,7 @@ void JsonWriter::SceneObject(::SceneObject* obj) } else { - LOG(Warning, "Missing prefab with id={0}.", obj->GetPrefabID()); + LOG(Warning, "Missing prefab {0}.", obj->GetPrefabID()); } } diff --git a/Source/Engine/Tests/TestPrefabs.cpp b/Source/Engine/Tests/TestPrefabs.cpp index 211fea930..ec3eb4af6 100644 --- a/Source/Engine/Tests/TestPrefabs.cpp +++ b/Source/Engine/Tests/TestPrefabs.cpp @@ -412,4 +412,149 @@ TEST_CASE("Prefabs") Content::DeleteAsset(prefabNested); Content::DeleteAsset(prefabBase); } + SECTION("Test Loading Nested Prefab After Changing and Deleting Root") + { + // https://github.com/FlaxEngine/FlaxEngine/issues/2050 + + // Create base prefab with 1 object + AssetReference prefabBase = Content::CreateVirtualAsset(); + REQUIRE(prefabBase); + Guid id; + Guid::Parse("3b3334524c696dcfa93cabacd2a4f404", id); + prefabBase->ChangeID(id); + auto prefabBaseInit = prefabBase->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"," + "\"TypeName\": \"FlaxEngine.DirectionalLight\"," + "\"Name\": \"New Root\"" + "}," + "{" + "\"ID\": \"f8fbee1349f749396ab6c2ad34f3afec\"," + "\"TypeName\": \"FlaxEngine.Camera\"," + "\"Name\": \"Child 1\"," + "\"ParentID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"" + "}," + "{" + "\"ID\": \"5632561847cf96fe2e8919848b7eca79\"," + "\"TypeName\": \"FlaxEngine.EmptyActor\"," + "\"Name\": \"Child 1.Child\"," + "\"ParentID\": \"f8fbee1349f749396ab6c2ad34f3afec\"" + "}," + "{" + "\"ID\": \"4e4f3a1847cf96fe2e8919848b7eca79\"," + "\"TypeName\": \"FlaxEngine.UICanvas\"," + "\"Name\": \"Child 2\"," + "\"ParentID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"" + "}" + "]"); + REQUIRE(!prefabBaseInit); + + // Create nested prefab but with 'old' state where root object is different + AssetReference prefabNested1 = Content::CreateVirtualAsset(); + REQUIRE(prefabNested1); + Guid::Parse("671447e947cbd2deea018a8377636ce6", id); + prefabNested1->ChangeID(id); + auto prefabNestedInit1 = prefabNested1->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"589bcfaa4bd1a53435129480e5bbdb3b\"," + "\"Name\": \"Old Root\"" + "}," + "{" + "\"ID\": \"1a6228d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"f8fbee1349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"" + "}," + "{" + "\"ID\": \"1212124f4d913e58eb35ab8b0b7e2eef\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"Name\": \"New Root\"" + "}," + "{" + "\"ID\": \"468028d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"2468902349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"Name\": \"Old Child\"" + "}" + "]"); + REQUIRE(!prefabNestedInit1); + + // Create nested prefab but with 'old' state where root object is different and doesn't exist anymore + AssetReference prefabNested2 = Content::CreateVirtualAsset(); + REQUIRE(prefabNested2); + Guid::Parse("b71447e947cbd2deea018a8377636ce6", id); + prefabNested2->ChangeID(id); + auto prefabNestedInit2 = prefabNested2->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"589bcfaa4bd1a53435129480e5bbdb3b\"," + "\"Name\": \"Old Root\"" + "}," + "{" + "\"ID\": \"1a6228d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"f8fbee1349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"" + "}," + "{" + "\"ID\": \"468028d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"2468902349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"Name\": \"Old Child\"" + "}" + "]"); + REQUIRE(!prefabNestedInit2); + + // Spawn test instances of both prefabs + ScriptingObjectReference instanceBase = PrefabManager::SpawnPrefab(prefabBase); + ScriptingObjectReference instanceNested1 = PrefabManager::SpawnPrefab(prefabNested1); + ScriptingObjectReference instanceNested2 = PrefabManager::SpawnPrefab(prefabNested2); + + // Verify scenario + REQUIRE(instanceBase); + REQUIRE(instanceBase->GetName() == TEXT("New Root")); + REQUIRE(instanceBase->GetChildrenCount() == 2); + REQUIRE(instanceBase->Children[0]->GetName() == TEXT("Child 1")); + REQUIRE(instanceBase->Children[0]->GetChildrenCount() == 1); + REQUIRE(instanceBase->Children[1]->GetName() == TEXT("Child 2")); + REQUIRE(instanceBase->Children[1]->GetChildrenCount() == 0); + REQUIRE(instanceBase->Children[0]->Children[0]->GetName() == TEXT("Child 1.Child")); + REQUIRE(instanceBase->Children[0]->Children[0]->GetChildrenCount() == 0); + REQUIRE(instanceNested1); + REQUIRE(instanceNested1->GetName() == TEXT("New Root")); + REQUIRE(instanceNested1->GetChildrenCount() == 2); + REQUIRE(instanceNested1->Children[0]->GetName() == TEXT("Child 1")); + REQUIRE(instanceNested1->Children[0]->GetChildrenCount() == 1); + REQUIRE(instanceNested1->Children[1]->GetName() == TEXT("Child 2")); + REQUIRE(instanceNested1->Children[1]->GetChildrenCount() == 0); + REQUIRE(instanceNested1->Children[0]->Children[0]->GetName() == TEXT("Child 1.Child")); + REQUIRE(instanceNested1->Children[0]->Children[0]->GetChildrenCount() == 0); + REQUIRE(instanceNested2); + REQUIRE(instanceNested2->GetName() == TEXT("New Root")); + REQUIRE(instanceNested2->GetChildrenCount() == 2); + REQUIRE(instanceNested2->Children[0]->GetName() == TEXT("Child 1")); + REQUIRE(instanceNested2->Children[0]->GetChildrenCount() == 1); + REQUIRE(instanceNested2->Children[1]->GetName() == TEXT("Child 2")); + REQUIRE(instanceNested2->Children[1]->GetChildrenCount() == 0); + REQUIRE(instanceNested2->Children[0]->Children[0]->GetName() == TEXT("Child 1.Child")); + REQUIRE(instanceNested2->Children[0]->Children[0]->GetChildrenCount() == 0); + + // Cleanup + instanceNested2->DeleteObject(); + instanceNested1->DeleteObject(); + instanceBase->DeleteObject(); + Content::DeleteAsset(prefabNested2); + Content::DeleteAsset(prefabNested1); + Content::DeleteAsset(prefabBase); + } }