diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 32293c8d1..d28800b33 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -1445,6 +1445,9 @@ namespace FlaxEditor [MethodImpl(MethodImplOptions.InternalCall)] internal static extern void Internal_LoadAsset(ref Guid id); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern bool Internal_CanSetToRoot(IntPtr prefab, IntPtr newRoot); + #endregion } } diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index fc160f217..16d962cba 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -26,13 +26,14 @@ #include "Engine/ContentImporters/CreateCollisionData.h" #include "Engine/ContentImporters/CreateJson.h" #include "Engine/Level/Level.h" +#include "Engine/Level/Actor.h" +#include "Engine/Level/Prefabs/Prefab.h" #include "Engine/Core/Config/GameSettings.h" #include "Engine/Core/Cache.h" #include "Engine/CSG/CSGBuilder.h" #include "Engine/Debug/DebugLog.h" #include "Engine/Debug/Exceptions/JsonParseException.h" #include "Engine/Audio/AudioClip.h" -#include "Engine/Level/Actor.h" #include "Engine/Engine/Engine.h" #include "Engine/Utilities/Encryption.h" #include "Engine/Navigation/Navigation.h" @@ -43,6 +44,7 @@ #include "Engine/Input/Keyboard.h" #include "Engine/Threading/Threading.h" #include "FlaxEngine.Gen.h" +#include "Engine/Serialization/JsonTools.h" #include Guid ManagedEditor::ObjectID(0x91970b4e, 0x99634f61, 0x84723632, 0x54c776af); @@ -1040,6 +1042,28 @@ public: Content::LoadAsync(*id); } + static bool CanSetToRoot(Prefab* prefab, Actor* targetActor) + { + // Reference: Prefab::ApplyAll(Actor* targetActor) + if (targetActor->GetPrefabID() != prefab->GetID()) + return false; + if (targetActor->GetPrefabObjectID() != prefab->GetRootObjectId()) + { + const ISerializable::DeserializeStream** newRootDataPtr = prefab->ObjectsDataCache.TryGet(targetActor->GetPrefabObjectID()); + if (!newRootDataPtr || !*newRootDataPtr) + return false; + const ISerializable::DeserializeStream& newRootData = **newRootDataPtr; + Guid prefabId, prefabObjectID; + if (JsonTools::GetGuidIfValid(prefabId, newRootData, "PrefabID") && JsonTools::GetGuidIfValid(prefabObjectID, newRootData, "PrefabObjectID")) + { + const auto nestedPrefab = Content::Load(prefabId); + if (nestedPrefab && nestedPrefab->GetRootObjectId() != prefabObjectID) + return false; + } + } + return true; + } + static void InitRuntime() { ADD_INTERNAL_CALL("FlaxEditor.Editor::IsDevInstance", &IsDevInstance); @@ -1086,6 +1110,7 @@ public: ADD_INTERNAL_CALL("FlaxEditor.Editor::Internal_EvaluateVisualScriptLocal", &EvaluateVisualScriptLocal); ADD_INTERNAL_CALL("FlaxEditor.Editor::Internal_DeserializeSceneObject", &DeserializeSceneObject); ADD_INTERNAL_CALL("FlaxEditor.Editor::Internal_LoadAsset", &LoadAsset); + ADD_INTERNAL_CALL("FlaxEditor.Editor::Internal_CanSetToRoot", &CanSetToRoot); } }; diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index a37d88b61..b5276edea 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -103,7 +103,7 @@ namespace FlaxEditor.Windows.Assets b.Enabled = hasSthSelected && !isRootSelected; b = contextMenu.AddButton("Set Root", SetRoot); - b.Enabled = isSingleActorSelected && !isRootSelected && hasPrefabLink; + b.Enabled = isSingleActorSelected && !isRootSelected && hasPrefabLink && Editor.Internal_CanSetToRoot(FlaxEngine.Object.GetUnmanagedPtr(Asset), FlaxEngine.Object.GetUnmanagedPtr(((ActorNode)Selection[0]).Actor)); // Prefab options diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 48ecc7e7c..649cca9c8 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -63,6 +63,22 @@ struct AutoActorCleanup } }; +namespace +{ + Actor* FindActorWithPrefabObjectId(Actor* a, const Guid& prefabObjectId) + { + if (a->GetPrefabObjectID() == prefabObjectId) + return a; + for (auto c : a->Children) + { + auto r = FindActorWithPrefabObjectId(c, prefabObjectId); + if (r) + return r; + } + return nullptr; + } +}; + /// /// The temporary data container for the prefab instance to restore its local changes after prefab synchronization. /// @@ -77,10 +93,16 @@ public: PrefabInstanceData(const PrefabInstanceData&) { +#if BUILD_DEBUG + CRASH; +#endif } PrefabInstanceData& operator=(const PrefabInstanceData&) { +#if BUILD_DEBUG + CRASH; +#endif return *this; } @@ -133,24 +155,26 @@ public: /// Synchronizes the prefab instances by applying changes from the diff data and restoring the local changes captured by SerializePrefabInstances. /// /// The prefab instances data. - /// The scene objects (temporary cache). + /// The new default instance of the prefab. + /// The scene objects (temporary cache from defaultInstance). /// The prefab asset identifier. /// The hash table that maps the prefab object id to json data for the given prefab object. /// The collection of the new prefab objects ids added to prefab during changes synchronization or modifications apply. /// True if failed, otherwise false. - static bool SynchronizePrefabInstances(Array& prefabInstancesData, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, const IdToDataLookupType& prefabObjectIdToDiffData, const Array& newPrefabObjectIds); + static bool SynchronizePrefabInstances(Array& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, const IdToDataLookupType& prefabObjectIdToDiffData, const Array& newPrefabObjectIds); /// /// Synchronizes the prefab instances by applying changes from the diff data and restoring the local changes captured by SerializePrefabInstances. /// /// The prefab instances data. - /// The scene objects (temporary cache). + /// The new default instance of the prefab. + /// The scene objects (temporary cache from defaultInstance). /// The prefab asset identifier. /// The temporary json buffer (cleared before and after usage). /// Collection with ids of the objects (actors and scripts) from the prefab before changes apply. Used to find new objects or old objects and use this information during changes sync (eg. generate ids for the new objects to prevent ids collisions). /// Collection with ids of the objects (actors and scripts) from the prefab after changes apply. Used to find new objects or old objects and use this information during changes sync (eg. generate ids for the new objects to prevent ids collisions). /// True if failed, otherwise false. - static bool SynchronizePrefabInstances(Array& prefabInstancesData, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds); + static bool SynchronizePrefabInstances(Array& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds); }; void PrefabInstanceData::CollectPrefabInstances(Array& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor) @@ -158,32 +182,28 @@ void PrefabInstanceData::CollectPrefabInstances(Array& prefa ScopeLock lock(PrefabManager::PrefabsReferencesLocker); if (PrefabManager::PrefabsReferences.ContainsKey(prefabId)) { - // Skip the input targetActor and prefab defaultInstance if exists auto& instances = PrefabManager::PrefabsReferences[prefabId]; int32 usedCount = 0; for (int32 instanceIndex = 0; instanceIndex < instances.Count(); instanceIndex++) { const auto instance = instances[instanceIndex]; - if (instance != defaultInstance && instance != targetActor) + if (instance != defaultInstance && targetActor != instance && !targetActor->HasActorInHierarchy(instance)) usedCount++; } prefabInstancesData.Resize(usedCount); - int32 dataIndex = 0; for (int32 instanceIndex = 0; instanceIndex < instances.Count(); instanceIndex++) { // Skip default instance because it will be recreated, skip input actor because it needs just to be linked - const auto instance = instances[instanceIndex]; - if (instance != defaultInstance && instance != targetActor) + Actor* instance = instances[instanceIndex]; + if (instance != defaultInstance && targetActor != instance && !targetActor->HasActorInHierarchy(instance)) { auto& data = prefabInstancesData[dataIndex++]; data.TargetActor = instance; data.OrderInParent = instance->GetOrderInParent(); -#if BUILD_DEBUG // Check if actor has not been deleted (the memory access exception could fire) ASSERT(data.TargetActor->GetID() == data.TargetActor->GetID()); -#endif } } } @@ -240,7 +260,7 @@ void PrefabInstanceData::SerializePrefabInstances(Array& pre tmpBuffer.Clear(); } -bool PrefabInstanceData::SynchronizePrefabInstances(Array& prefabInstancesData, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, const IdToDataLookupType& prefabObjectIdToDiffData, const Array& newPrefabObjectIds) +bool PrefabInstanceData::SynchronizePrefabInstances(Array& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, const IdToDataLookupType& prefabObjectIdToDiffData, const Array& newPrefabObjectIds) { for (int32 instanceIndex = 0; instanceIndex < prefabInstancesData.Count(); instanceIndex++) { @@ -248,6 +268,21 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array& p ISerializeModifierCacheType modifier = Cache::ISerializeModifier.Get(); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); + // If prefab object root was changed during changes apply then update the TargetActor to point a valid object + Actor* oldTargetActor = instance.TargetActor; + Actor* newTargetActor = FindActorWithPrefabObjectId(instance.TargetActor, defaultInstance->GetID()); + if (!newTargetActor) + { + LOG(Error, "Missing root object {0} for prefab instance {1}", defaultInstance->ToString(), oldTargetActor->ToString()); + } + else if (oldTargetActor != newTargetActor) + { + LOG(Info, "Changing root object of prefab instance from {0} to {1}", oldTargetActor->ToString(), newTargetActor->ToString()); + newTargetActor->SetParent(oldTargetActor->GetParent(), true, false); + oldTargetActor->SetParent(newTargetActor, true, false); + instance.TargetActor = newTargetActor; + } + // Get scene objects in the prefab instance sceneObjects->Clear(); SceneQuery::GetAllSerializableSceneObjects(instance.TargetActor, *sceneObjects.Value); @@ -267,7 +302,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array& p // Remove object LOG(Info, "Removing object {0} from instance {1} (prefab: {2})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), prefabId); - dynamic_cast(obj)->DeleteObject(); + obj->DeleteObject(); obj->SetParent(nullptr); sceneObjects.Value->RemoveAtKeepOrder(i); @@ -305,7 +340,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array& p // Remove object removed from the prefab LOG(Info, "Removing prefab instance object {0} from instance {1} (prefab object: {2}, prefab: {3})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), obj->GetPrefabObjectID(), prefabId); - dynamic_cast(obj)->DeleteObject(); + obj->DeleteObject(); obj->SetParent(nullptr); sceneObjects.Value->RemoveAtKeepOrder(i); @@ -360,10 +395,22 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array& p int32 dataIndex; if (instance.PrefabInstanceIdToDataIndex.TryGet(obj->GetSceneObjectId(), dataIndex)) { - obj->Deserialize(instance.Data[dataIndex], modifier.Value); + auto& data = instance.Data[dataIndex]; +#if 0 + if (oldTargetActor != newTargetActor && (obj == oldTargetActor || obj == newTargetActor)) + { + // Prevent from changing parent of the new/old instance roots when changing prefab root instance (already did it) + data.RemoveMember("ParentID"); + } +#else + // Preserve hierarchy (values from prefab are used) + data.RemoveMember("ParentID"); +#endif + + obj->Deserialize(data, modifier.Value); // Preserve order in parent (values from prefab are used) - if (i != 0) + if (obj != instance.TargetActor) { auto prefab = Content::Load(prefabId); const auto defaultInstance = prefab ? prefab->GetDefaultInstance(obj->GetPrefabObjectID()) : nullptr; @@ -443,7 +490,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array& p return false; } -bool PrefabInstanceData::SynchronizePrefabInstances(Array& prefabInstancesData, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds) +bool PrefabInstanceData::SynchronizePrefabInstances(Array& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds) { // Fully serialize default instance scene objects (accumulate all prefab and nested prefabs changes into a single linear list of objects) rapidjson_flax::Document defaultInstanceData; @@ -518,7 +565,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(Array& p } // Process prefab instances to synchronize changes - return SynchronizePrefabInstances(prefabInstancesData, sceneObjects, prefabId, prefabObjectIdToDiffData, newPrefabObjectIds); + return SynchronizePrefabInstances(prefabInstancesData, defaultInstance, sceneObjects, prefabId, prefabObjectIdToDiffData, newPrefabObjectIds); } bool FindCyclicReferences(Actor* actor, const Guid& prefabRootId) @@ -541,7 +588,7 @@ bool Prefab::ApplyAll(Actor* targetActor) PROFILE_CPU(); const auto startTime = DateTime::NowUTC(); - // Validate input + // Perform validation if (!IsLoaded()) { Log::Exception(TEXT("Cannot apply changes on not loaded prefab asset.")); @@ -557,17 +604,33 @@ bool Prefab::ApplyAll(Actor* targetActor) Log::Exception(TEXT("Cannot apply changes to the prefab. Prefab instance root object has link to the other prefab.")); return true; } - if (targetActor->GetPrefabObjectID() != GetRootObjectId()) - { - LOG(Warning, "Applying prefab changes with modified root object. Root object id: {0}, new root: {1}", GetRootObjectId().ToString(), targetActor->ToString()); - } - - // Ensure to have default instance created if (GetDefaultInstance() == nullptr) { LOG(Warning, "Failed to create default prefab instance for the prefab asset."); return true; } + if (targetActor->GetPrefabObjectID() != GetRootObjectId()) + { + LOG(Warning, "Applying prefab changes with modified root object. Root object id: {0}, new root: {1} (prefab object id: {2})", GetRootObjectId().ToString(), targetActor->ToString(), targetActor->GetPrefabObjectID()); + SceneObject* newRootDefault = GetDefaultInstance(targetActor->GetPrefabObjectID()); + const ISerializable::DeserializeStream** newRootDataPtr = ObjectsDataCache.TryGet(targetActor->GetPrefabObjectID()); + if (!newRootDefault || !newRootDataPtr || !*newRootDataPtr) + { + LOG(Error, "Cannot change the prefab root object to the actor that is not yet added to the prefab."); + return true; + } + const ISerializable::DeserializeStream& newRootData = **newRootDataPtr; + Guid prefabId, prefabObjectID; + if (JsonTools::GetGuidIfValid(prefabId, newRootData, "PrefabID") && JsonTools::GetGuidIfValid(prefabObjectID, newRootData, "PrefabObjectID")) + { + const auto nestedPrefab = Content::Load(prefabId); + if (nestedPrefab && nestedPrefab->GetRootObjectId() != prefabObjectID) + { + LOG(Error, "Cannot change the prefab root object is from other nested prefab (excluding root of that nested prefab prefab)."); + return true; + } + } + } // Prevent cyclic references { @@ -813,8 +876,6 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr { obj->Deserialize(diffDataDocument[dataIndex], modifier.Value); - sceneObjects->Add(obj); - // Synchronize order of the scene objects with the serialized data (eg. user reordered actors in prefab editor and applied changes) if (i != 0) { @@ -834,7 +895,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr // Remove object removed from the prefab LOG(Info, "Removing object {0} from prefab default instance", obj->GetSceneObjectId()); - dynamic_cast(obj)->DeleteObject(); + obj->DeleteObject(); obj->SetParent(nullptr); } } @@ -900,6 +961,14 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr auto root = dynamic_cast(sceneObjects.Value->At(0)); if (root && root->_parent) { + // When changing prefab root the target actor is a new root so try to find it in the objects + int32 targetActorIdx = oldObjectsIds.Find(targetActor->GetPrefabObjectID()); + if (targetActorIdx > 0 && dynamic_cast(sceneObjects.Value->At(targetActorIdx))) + { + root = dynamic_cast(sceneObjects.Value->At(targetActorIdx)); + } + + // Try using the first actor without a parent as a new ro0t for (int32 i = 1; i < sceneObjects->Count(); i++) { SceneObject* obj = sceneObjects.Value->At(i); @@ -918,6 +987,11 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr root->_parent = nullptr; } } + if (!root) + { + LOG(Warning, "No valid objects in prefab."); + return true; + } // Link objects hierarchy for (int32 i = 0; i < sceneObjects->Count(); i++) @@ -948,7 +1022,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr // Refresh all prefab instances (using the cached data) LOG(Info, "Reloading prefab instances"); - if (PrefabInstanceData::SynchronizePrefabInstances(prefabInstancesData, sceneObjects, prefabId, dataBuffer, oldObjectsIds, ObjectsIds)) + if (PrefabInstanceData::SynchronizePrefabInstances(prefabInstancesData, defaultInstance, sceneObjects, prefabId, dataBuffer, oldObjectsIds, ObjectsIds)) return true; // Link the input objects to the prefab objects (prefab and instance are even, only links for the new objects added to prefab are required) diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index 7cd62913d..1691d8720 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -281,7 +281,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, DictionaryAdd(prefabObjectId, dynamic_cast(obj)); + objectsCache->Add(prefabObjectId, obj); obj->LinkPrefab(prefabId, prefabObjectId); } diff --git a/Source/Engine/Serialization/JsonTools.cpp b/Source/Engine/Serialization/JsonTools.cpp index 1a8da00eb..bb4ed4c6b 100644 --- a/Source/Engine/Serialization/JsonTools.cpp +++ b/Source/Engine/Serialization/JsonTools.cpp @@ -5,6 +5,7 @@ #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Types/CommonValue.h" #include "Engine/Core/Types/DateTime.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Scripting/ScriptingObjectReference.h" #include "Engine/Utilities/Encryption.h" @@ -35,38 +36,12 @@ void ChangeIds(rapidjson_flax::Value& obj, rapidjson_flax::Document& document, c // Optimized version: char buffer[32] = { - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0', - '0' + // @formatter:off + '0','0','0','0','0','0','0','0','0','0', + '0','0','0','0','0','0','0','0','0','0', + '0','0','0','0','0','0','0','0','0','0', + '0','0' + // @formatter:on }; static const char* digits = "0123456789abcdef"; uint32 n = value.A; @@ -102,6 +77,7 @@ void JsonTools::ChangeIds(Document& doc, const Dictionary& mapping) { if (mapping.IsEmpty()) return; + PROFILE_CPU(); ::ChangeIds(doc, doc, mapping); }