From 954cf3eb5b42027d8ad51ab58d1a6cf8235708aa Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 23 May 2025 17:57:14 -0500 Subject: [PATCH 01/23] Add option to show verticies and triangles of collision model in a collider data window. --- .../Windows/Assets/CollisionDataWindow.cs | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Windows/Assets/CollisionDataWindow.cs b/Source/Editor/Windows/Assets/CollisionDataWindow.cs index 3f265eea9..4b10be943 100644 --- a/Source/Editor/Windows/Assets/CollisionDataWindow.cs +++ b/Source/Editor/Windows/Assets/CollisionDataWindow.cs @@ -7,6 +7,7 @@ using FlaxEditor.Content.Create; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.CustomEditors.Elements; +using FlaxEditor.GUI; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Viewport.Previews; using FlaxEngine; @@ -171,13 +172,52 @@ namespace FlaxEditor.Windows.Assets } private readonly SplitPanel _split; - private readonly ModelBasePreview _preview; + private readonly CollisionDataPreview _preview; private readonly CustomEditorPresenter _propertiesPresenter; private readonly PropertiesProxy _properties; private Model _collisionWiresModel; private StaticModel _collisionWiresShowActor; private bool _updateWireMesh; + private class CollisionDataPreview : ModelBasePreview + { + public bool ShowCollisionData = false; + public Model CollisionWireModel; + + /// + public CollisionDataPreview(bool useWidgets) + : base(useWidgets) + { + ViewportCamera = new FPSCamera(); + Task.ViewFlags &= ~ViewFlags.Sky & ~ViewFlags.Bloom & ~ViewFlags.EyeAdaptation; + } + + /// + public override void Draw() + { + base.Draw(); + + if (ShowCollisionData && CollisionWireModel != null) + { + var lods = CollisionWireModel.LODs; + var lod = lods[0]; + int triangleCount = 0, vertexCount = 0; + for (int meshIndex = 0; meshIndex < lod.Meshes.Length; meshIndex++) + { + var mesh = lod.Meshes[meshIndex]; + triangleCount += mesh.TriangleCount; + vertexCount += mesh.VertexCount; + } + + var text = string.Format("\nTriangles: {0:N0}\nVertices: {1:N0}", triangleCount, vertexCount); + var font = Style.Current.FontMedium; + var pos = new Float2(10, 50); + Render2D.DrawText(font, text, new Rectangle(pos + Float2.One, Size), Color.Black); + Render2D.DrawText(font, text, new Rectangle(pos, Size), Color.White); + } + } + } + /// public CollisionDataWindow(Editor editor, AssetItem item) : base(editor, item) @@ -185,6 +225,12 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.CenterView64, () => _preview.ResetCamera()).LinkTooltip("Show whole collision"); + var infoButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Info64).LinkTooltip("Show Collision Data"); + infoButton.Clicked += () => + { + _preview.ShowCollisionData = !_preview.ShowCollisionData; + infoButton.Checked = _preview.ShowCollisionData; + }; _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/physics/colliders/collision-data.html")).LinkTooltip("See documentation to learn more"); // Split Panel @@ -197,12 +243,10 @@ namespace FlaxEditor.Windows.Assets }; // Model preview - _preview = new ModelBasePreview(true) + _preview = new CollisionDataPreview(true) { - ViewportCamera = new FPSCamera(), Parent = _split.Panel1 }; - _preview.Task.ViewFlags &= ~ViewFlags.Sky & ~ViewFlags.Bloom & ~ViewFlags.EyeAdaptation; // Asset properties _propertiesPresenter = new CustomEditorPresenter(null); @@ -252,6 +296,7 @@ namespace FlaxEditor.Windows.Assets } _collisionWiresShowActor.Model = _collisionWiresModel; _collisionWiresShowActor.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WiresDebugMaterial)); + _preview.CollisionWireModel = _collisionWiresModel; _preview.Asset = FlaxEngine.Content.LoadAsync(_asset.Options.Model); } From 71991ff8c73e5f3c76004a4b4ea35bed3e512be9 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Tue, 3 Jun 2025 15:25:45 -0500 Subject: [PATCH 02/23] Show added and removed actors in prefab diff view. --- .../CustomEditors/Dedicated/ActorEditor.cs | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index 540b602ec..837e7ad3b 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -240,6 +240,12 @@ namespace FlaxEditor.CustomEditors.Dedicated node.TextColor = Color.OrangeRed; node.Text = Utilities.Utils.GetPropertyNameUI(removed.PrefabObject.GetType().Name); } + // Removed Actor + else if (editor is RemovedActorDummy removedActor) + { + node.TextColor = Color.OrangeRed; + node.Text = $"{removedActor.PrefabObject.Name} ({Utilities.Utils.GetPropertyNameUI(removedActor.PrefabObject.GetType().Name)})"; + } // Actor or Script else if (editor.Values[0] is SceneObject sceneObject) { @@ -295,11 +301,35 @@ namespace FlaxEditor.CustomEditors.Dedicated // Not used } } + + private class RemovedActorDummy : CustomEditor + { + /// + /// The removed prefab object (from the prefab default instance). + /// + public Actor PrefabObject; + + /// + /// The prefab instance's parent. + /// + public Actor ParentActor; + + /// + /// The order of the removed actor in the parent. + /// + public int OrderInParent; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + // Not used + } + } private TreeNode ProcessDiff(CustomEditor editor, bool skipIfNotModified = true) { - // Special case for new Script added to actor - if (editor.Values[0] is Script script && !script.HasPrefabLink) + // Special case for new Script or child actor added to actor + if ((editor.Values[0] is Script script && !script.HasPrefabLink) || (editor.Values[0] is Actor a && !a.HasPrefabLink)) return CreateDiffNode(editor); // Skip if no change detected @@ -359,6 +389,41 @@ namespace FlaxEditor.CustomEditors.Dedicated } } + // Compare child actors for removed actors. + if (editor is ActorEditor && editor.Values.HasReferenceValue && editor.Values.ReferenceValue is Actor prefabObjectActor) + { + var thisActor = editor.Values[0] as Actor; + for (int i = 0; i < prefabObjectActor.ChildrenCount; i++) + { + var prefabActorChild = prefabObjectActor.Children[i]; + if (thisActor == null) + continue; + bool isRemoved = true; + for (int j = 0; j < thisActor.ChildrenCount; j++) + { + var actorChild = thisActor.Children[j]; + if (actorChild.PrefabObjectID == prefabActorChild.PrefabObjectID) + { + isRemoved = false; + break; + } + } + if (isRemoved) + { + var dummy = new RemovedActorDummy + { + PrefabObject = prefabActorChild, + ParentActor = thisActor, + OrderInParent = prefabActorChild.OrderInParent, + }; + var child = CreateDiffNode(dummy); + if (result == null) + result = CreateDiffNode(editor); + result.AddChild(child); + } + } + } + return result; } @@ -459,6 +524,25 @@ namespace FlaxEditor.CustomEditors.Dedicated return; } + // Special case for reverting removed Actors + if (editor is RemovedActorDummy removedActor) + { + Editor.Log("Reverting removed actor changes to prefab (adding it)"); + + var parentActor = removedActor.ParentActor; + var restored = parentActor.AddChild(removedActor.PrefabObject.GetType()); + var prefabId = parentActor.PrefabID; + var prefabObjectId = removedActor.PrefabObject.PrefabObjectID; + string data = JsonSerializer.Serialize(removedActor.PrefabObject); + JsonSerializer.Deserialize(restored, data); + if (Presenter.Owner is PropertiesWindow propertiesWindow) + Editor.Instance.SceneEditing.Spawn(restored, parentActor, removedActor.OrderInParent); + else if (Presenter.Owner is PrefabWindow prefabWindow) + prefabWindow.Spawn(restored, parentActor, removedActor.OrderInParent); + Actor.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(restored), ref prefabId, ref prefabObjectId); + return; + } + // Special case for new Script added to actor if (editor.Values[0] is Script script && !script.HasPrefabLink) { @@ -470,6 +554,27 @@ namespace FlaxEditor.CustomEditors.Dedicated return; } + + // Special case for new Actor added to actor + if (editor.Values[0] is Actor a && !a.HasPrefabLink) + { + Editor.Log("Reverting added actor changes to prefab (removing it)"); + + // TODO: Keep previous selection. + if (Presenter.Owner is PropertiesWindow propertiesWindow) + { + var editorInstance = Editor.Instance.SceneEditing; + editorInstance.Select(a); + editorInstance.Delete(); + } + else if (Presenter.Owner is PrefabWindow prefabWindow) + { + prefabWindow.Select(prefabWindow.Graph.Root.Find(a)); + prefabWindow.Delete(); + } + + return; + } editor.RevertToReferenceValue(); } From 6b78b498f7426e1627af5e100c6f71ded8c4623f Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 4 Jun 2025 10:17:41 -0500 Subject: [PATCH 03/23] Use direct count from internal call. --- .../Windows/Assets/CollisionDataWindow.cs | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/Source/Editor/Windows/Assets/CollisionDataWindow.cs b/Source/Editor/Windows/Assets/CollisionDataWindow.cs index 4b10be943..8fa32f890 100644 --- a/Source/Editor/Windows/Assets/CollisionDataWindow.cs +++ b/Source/Editor/Windows/Assets/CollisionDataWindow.cs @@ -182,7 +182,14 @@ namespace FlaxEditor.Windows.Assets private class CollisionDataPreview : ModelBasePreview { public bool ShowCollisionData = false; - public Model CollisionWireModel; + private int _verticesCount = 0; + private int _trianglesCount = 0; + + public void SetVerticesAndTriangleCount(int verticesCount, int triangleCount) + { + _verticesCount = verticesCount; + _trianglesCount = triangleCount; + } /// public CollisionDataPreview(bool useWidgets) @@ -197,19 +204,9 @@ namespace FlaxEditor.Windows.Assets { base.Draw(); - if (ShowCollisionData && CollisionWireModel != null) + if (ShowCollisionData) { - var lods = CollisionWireModel.LODs; - var lod = lods[0]; - int triangleCount = 0, vertexCount = 0; - for (int meshIndex = 0; meshIndex < lod.Meshes.Length; meshIndex++) - { - var mesh = lod.Meshes[meshIndex]; - triangleCount += mesh.TriangleCount; - vertexCount += mesh.VertexCount; - } - - var text = string.Format("\nTriangles: {0:N0}\nVertices: {1:N0}", triangleCount, vertexCount); + var text = string.Format("\nTriangles: {0:N0}\nVertices: {1:N0}\nMemory Size: {2:N0} bytes", _trianglesCount, _verticesCount, Asset.MemoryUsage); var font = Style.Current.FontMedium; var pos = new Float2(10, 50); Render2D.DrawText(font, text, new Rectangle(pos + Float2.One, Size), Color.Black); @@ -284,7 +281,7 @@ namespace FlaxEditor.Windows.Assets _collisionWiresModel = FlaxEngine.Content.CreateVirtualAsset(); _collisionWiresModel.SetupLODs(new[] { 1 }); } - Editor.Internal_GetCollisionWires(FlaxEngine.Object.GetUnmanagedPtr(Asset), out var triangles, out var indices, out var _, out var _); + Editor.Internal_GetCollisionWires(FlaxEngine.Object.GetUnmanagedPtr(Asset), out var triangles, out var indices, out var triangleCount, out var indicesCount); if (triangles != null && indices != null) _collisionWiresModel.LODs[0].Meshes[0].UpdateMesh(triangles, indices); else @@ -296,7 +293,7 @@ namespace FlaxEditor.Windows.Assets } _collisionWiresShowActor.Model = _collisionWiresModel; _collisionWiresShowActor.SetMaterial(0, FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WiresDebugMaterial)); - _preview.CollisionWireModel = _collisionWiresModel; + _preview.SetVerticesAndTriangleCount(triangleCount, indicesCount / 3); _preview.Asset = FlaxEngine.Content.LoadAsync(_asset.Options.Model); } From c5bfc6bc3d426f46065890f4ed01f75dec94db7f Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Sun, 6 Jul 2025 21:28:01 +0200 Subject: [PATCH 04/23] add option to add sphere in add colliders menu --- .../Editor/SceneGraph/Actors/StaticModelNode.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index a7a3ed9b3..a91ef7399 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -141,6 +141,8 @@ namespace FlaxEditor.SceneGraph.Actors b.TooltipText = "Add a box collider to every selected model that will auto resize based on the model bounds."; b = menu.ContextMenu.AddButton("Sphere", () => OnAddCollider(window, CreateSphere)); b.TooltipText = "Add a sphere collider to every selected model that will auto resize based on the model bounds."; + b = menu.ContextMenu.AddButton("Capsule", () => OnAddCollider(window, CreateCapsule)); + b.TooltipText = "Add a capsule collider to every selected model that will auto resize based on the model bounds."; b = menu.ContextMenu.AddButton("Convex", () => OnAddCollider(window, CreateConvex)); b.TooltipText = "Generate and add a convex collider for every selected model."; b = menu.ContextMenu.AddButton("Triangle Mesh", () => OnAddCollider(window, CreateTriangle)); @@ -267,6 +269,20 @@ namespace FlaxEditor.SceneGraph.Actors spawner(collider); } + private void CreateCapsule(StaticModel actor, Spawner spawner, bool singleNode) + { + var collider = new CapsuleCollider + { + Transform = actor.Transform, + Position = actor.Box.Center, + + // Size the sphere to best fit the actor + Radius = (float)actor.Sphere.Radius / Mathf.Max((float)actor.Scale.MaxValue, 0.0001f) * 0.707f, + Height = 100f, + }; + spawner(collider); + } + private void CreateConvex(StaticModel actor, Spawner spawner, bool singleNode) { CreateMeshCollider(actor, spawner, singleNode, CollisionDataType.ConvexMesh); From 83374164dbf3aefcdbdc14a416df5e120c4797af Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Sun, 6 Jul 2025 21:31:26 +0200 Subject: [PATCH 05/23] haha I did not copy paste that comment --- Source/Editor/SceneGraph/Actors/StaticModelNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index a91ef7399..e95364c2d 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -276,7 +276,7 @@ namespace FlaxEditor.SceneGraph.Actors Transform = actor.Transform, Position = actor.Box.Center, - // Size the sphere to best fit the actor + // Size the capsule to best fit the actor Radius = (float)actor.Sphere.Radius / Mathf.Max((float)actor.Scale.MaxValue, 0.0001f) * 0.707f, Height = 100f, }; From 1fb6586dff6c37a245f40865adbc0f214a7121ca Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 19 Jul 2025 16:09:33 -0500 Subject: [PATCH 06/23] Add collection item duplication. --- .../CustomEditors/Editors/CollectionEditor.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index cfc11d5a5..50b3e47d5 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -70,7 +70,9 @@ namespace FlaxEditor.CustomEditors.Editors menu.ItemsContainer.RemoveChildren(); menu.AddButton("Copy", linkedEditor.Copy); - var b = menu.AddButton("Paste", linkedEditor.Paste); + var b = menu.AddButton("Duplicate", () => Editor.Duplicate(Index)); + b.Enabled = linkedEditor.CanPaste && !Editor._readOnly; + b = menu.AddButton("Paste", linkedEditor.Paste); b.Enabled = linkedEditor.CanPaste && !Editor._readOnly; menu.AddSeparator(); @@ -404,8 +406,10 @@ namespace FlaxEditor.CustomEditors.Editors var menu = new ContextMenu(); menu.AddButton("Copy", linkedEditor.Copy); + var b = menu.AddButton("Duplicate", () => Editor.Duplicate(Index)); + b.Enabled = linkedEditor.CanPaste && !Editor._readOnly; var paste = menu.AddButton("Paste", linkedEditor.Paste); - paste.Enabled = linkedEditor.CanPaste; + paste.Enabled = linkedEditor.CanPaste && !Editor._readOnly; if (_canReorder) { @@ -741,6 +745,25 @@ namespace FlaxEditor.CustomEditors.Editors cloned[srcIndex] = tmp; SetValue(cloned); } + + /// + /// Duplicates the list item. + /// + /// The index to duplicate. + public void Duplicate(int index) + { + if (IsSetBlocked) + return; + + var count = Count; + Resize(count + 1); + RefreshInternal(); // Force update values. + Shift(count, index + 1); + RefreshInternal(); // Force update values. + var cloned = CloneValues(); + cloned[index + 1] = Utilities.Utils.CloneValue(cloned[index]); + SetValue(cloned); + } /// /// Shifts the specified item at the given index and moves it through the list to the other item. It supports undo. From b36be959472214b076b2fccb94846f982d332cf3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 26 Aug 2025 14:59:10 +0200 Subject: [PATCH 07/23] Fix Editor undo on collection properties that return different object on get #3256 --- Source/Editor/Utilities/MemberComparison.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Editor/Utilities/MemberComparison.cs b/Source/Editor/Utilities/MemberComparison.cs index 3eee029d2..e8d02bece 100644 --- a/Source/Editor/Utilities/MemberComparison.cs +++ b/Source/Editor/Utilities/MemberComparison.cs @@ -61,6 +61,7 @@ namespace FlaxEditor.Utilities /// The value. public void SetMemberValue(object instance, object value) { + var originalInstance = instance; var finalMember = MemberPath.GetLastMember(ref instance); var type = finalMember.Type; @@ -92,6 +93,12 @@ namespace FlaxEditor.Utilities } finalMember.SetValue(instance, value); + + if (instance != originalInstance && finalMember.Index != null) + { + // Set collection back to the parent object (in case of properties that always return a new object like 'Spline.SplineKeyframes') + finalMember.Member.SetValue(originalInstance, instance); + } } /// From bcedb05a2c719d0914a9ce8c2bc259657124a902 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 26 Aug 2025 17:55:32 +0200 Subject: [PATCH 08/23] Fix regression from 6a8553a2777f8dd7fa2ffa9e7ec4db070252c462 on local lights --- Source/Shaders/Lighting.hlsl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Shaders/Lighting.hlsl b/Source/Shaders/Lighting.hlsl index 9b14db5ed..4d6e474fa 100644 --- a/Source/Shaders/Lighting.hlsl +++ b/Source/Shaders/Lighting.hlsl @@ -122,10 +122,6 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f // Calculate shadow ShadowSample shadow = GetShadow(lightData, gBuffer, shadowMask); -#if !LIGHTING_NO_DIRECTIONAL - // Directional shadowing - shadow.SurfaceShadow *= NoL; -#endif // Calculate attenuation if (isRadial) @@ -139,6 +135,11 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f shadow.TransmissionShadow *= attenuation; } +#if !LIGHTING_NO_DIRECTIONAL + // Directional shadowing + shadow.SurfaceShadow *= NoL; +#endif + BRANCH if (shadow.SurfaceShadow + shadow.TransmissionShadow > 0) { From d314d5b32492ec085ed97a443552de78fc0ed697 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 26 Aug 2025 18:05:42 +0200 Subject: [PATCH 09/23] Format memory usage to human-readable format #3495 --- Source/Editor/Windows/Assets/CollisionDataWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/Assets/CollisionDataWindow.cs b/Source/Editor/Windows/Assets/CollisionDataWindow.cs index 8fa32f890..fc8a5659d 100644 --- a/Source/Editor/Windows/Assets/CollisionDataWindow.cs +++ b/Source/Editor/Windows/Assets/CollisionDataWindow.cs @@ -206,7 +206,7 @@ namespace FlaxEditor.Windows.Assets if (ShowCollisionData) { - var text = string.Format("\nTriangles: {0:N0}\nVertices: {1:N0}\nMemory Size: {2:N0} bytes", _trianglesCount, _verticesCount, Asset.MemoryUsage); + var text = string.Format("\nTriangles: {0:N0}\nVertices: {1:N0}\nMemory Size: {2}", _trianglesCount, _verticesCount, Utilities.Utils.FormatBytesCount(Asset.MemoryUsage)); var font = Style.Current.FontMedium; var pos = new Float2(10, 50); Render2D.DrawText(font, text, new Rectangle(pos + Float2.One, Size), Color.Black); From 824b49dd883443c7a20384de4aa83bedb43f3483 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Tue, 26 Aug 2025 20:25:02 -0500 Subject: [PATCH 10/23] Better duplication of collection. --- .../CustomEditors/Editors/CollectionEditor.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 50b3e47d5..a0ee8d3dc 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -756,13 +756,22 @@ namespace FlaxEditor.CustomEditors.Editors return; var count = Count; - Resize(count + 1); - RefreshInternal(); // Force update values. - Shift(count, index + 1); - RefreshInternal(); // Force update values. - var cloned = CloneValues(); - cloned[index + 1] = Utilities.Utils.CloneValue(cloned[index]); - SetValue(cloned); + var newValues = Allocate(count + 1); + var oldValues = (IList)Values[0]; + + for (int i = 0; i <= index; i++) + { + newValues[i] = oldValues[i]; + } + + newValues[index + 1] = Utilities.Utils.CloneValue(oldValues[index]); + + for (int i = index + 1; i < count; i++) + { + newValues[i + 1] = oldValues[i]; + } + + SetValue(newValues); } /// From a3073321cfeac2040da3bd3f7e41c5c54bd10783 Mon Sep 17 00:00:00 2001 From: frank Date: Thu, 28 Aug 2025 17:24:06 +0800 Subject: [PATCH 11/23] fix scaling for TextRender --- Source/Engine/UI/TextRender.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index bc7b88647..d27f197bb 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -167,7 +167,7 @@ void TextRender::UpdateLayout() // Pick a font (remove DPI text scale as the text is being placed in the world) auto font = Font->CreateFont(_size); - float scale = 1.0f / FontManager::FontScale; + float scale = _layoutOptions.Scale / FontManager::FontScale; // Prepare FontTextureAtlas* fontAtlas = nullptr; From f3d375e35668d8064e266e99d6a32d318f0e6a92 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 Aug 2025 22:26:50 +0200 Subject: [PATCH 12/23] Fix prefab root name and transform being changed when applying local changes of sub-object #3235 --- Source/Editor/Modules/PrefabsModule.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Modules/PrefabsModule.cs b/Source/Editor/Modules/PrefabsModule.cs index 617202062..f7b984056 100644 --- a/Source/Editor/Modules/PrefabsModule.cs +++ b/Source/Editor/Modules/PrefabsModule.cs @@ -254,18 +254,29 @@ namespace FlaxEditor.Modules PrefabApplying?.Invoke(prefab, instance); // When applying changes to prefab from actor in level ignore it's root transformation (see ActorEditor.ProcessDiff) + Actor prefabRoot = null; var originalTransform = instance.LocalTransform; var originalName = instance.Name; - if (instance.IsPrefabRoot && instance.HasScene) + if (instance.HasScene) { - instance.LocalTransform = prefab.GetDefaultInstance().Transform; - instance.Name = prefab.GetDefaultInstance().Name; + prefabRoot = instance.GetPrefabRoot(); + if (prefabRoot != null && prefabRoot.IsPrefabRoot && instance.HasScene) + { + var defaultInstance = prefab.GetDefaultInstance(); + originalTransform = prefabRoot.LocalTransform; + originalName = prefabRoot.Name; + prefabRoot.LocalTransform = defaultInstance.Transform; + prefabRoot.Name = defaultInstance.Name; + } } // Call backend var failed = PrefabManager.Internal_ApplyAll(FlaxEngine.Object.GetUnmanagedPtr(instance)); - instance.LocalTransform = originalTransform; - instance.Name = originalName; + if (prefabRoot != null) + { + prefabRoot.LocalTransform = originalTransform; + prefabRoot.Name = originalName; + } if (failed) throw new Exception("Failed to apply the prefab. See log to learn more."); From ef7c7f2d30b7b77f992d6a0cfabdabe4c53ef26c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 Aug 2025 22:52:46 +0200 Subject: [PATCH 13/23] Fix script fields prefab diff showing and reverting with undo #3594 --- .../CustomEditors/Dedicated/ActorEditor.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index 540b602ec..62985fe30 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -10,8 +10,6 @@ using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Tree; using FlaxEditor.Scripting; -using FlaxEditor.Windows; -using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; @@ -304,7 +302,7 @@ namespace FlaxEditor.CustomEditors.Dedicated // Skip if no change detected var isRefEdited = editor.Values.IsReferenceValueModified; - if (!isRefEdited && skipIfNotModified) + if (!isRefEdited && skipIfNotModified && editor is not ScriptsEditor) return null; TreeNode result = null; @@ -438,6 +436,15 @@ namespace FlaxEditor.CustomEditors.Dedicated Presenter.BuildLayoutOnUpdate(); } + private static void GetAllPrefabObjects(List objects, Actor actor) + { + objects.Add(actor); + objects.AddRange(actor.Scripts); + var children = actor.Children; + foreach (var child in children) + GetAllPrefabObjects(objects, child); + } + private void OnDiffRevert(CustomEditor editor) { // Special case for removed Script from actor @@ -471,7 +478,23 @@ namespace FlaxEditor.CustomEditors.Dedicated return; } - editor.RevertToReferenceValue(); + if (Presenter.Undo != null && Presenter.Undo.Enabled) + { + var thisActor = (Actor)Values[0]; + var rootActor = thisActor.IsPrefabRoot ? thisActor : thisActor.GetPrefabRoot(); + var prefabObjects = new List(); + GetAllPrefabObjects(prefabObjects, rootActor); + using (new UndoMultiBlock(Presenter.Undo, prefabObjects, "Revert to Prefab")) + { + editor.RevertToReferenceValue(); + editor.RefreshInternal(); + } + } + else + { + editor.RevertToReferenceValue(); + editor.RefreshInternal(); + } } } } From d47ac95681be596fdd0ef19603a2d37ac62e8601 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 Aug 2025 23:48:25 +0200 Subject: [PATCH 14/23] Modernize the code to use unified scene access #3513 --- .../CustomEditors/CustomEditorPresenter.cs | 5 ++++ .../CustomEditors/Dedicated/ActorEditor.cs | 26 ++++++------------- .../Editor/SceneGraph/ISceneEditingContext.cs | 17 +++++++++++- Source/Editor/SceneGraph/RootNode.cs | 2 +- .../Windows/Assets/BehaviorTreeWindow.cs | 3 +++ .../Windows/Assets/PrefabWindow.Hierarchy.cs | 16 +++++++++--- Source/Editor/Windows/Assets/PrefabWindow.cs | 3 +++ Source/Editor/Windows/PropertiesWindow.cs | 3 +++ Source/Editor/Windows/SceneEditorWindow.cs | 12 +++++++++ 9 files changed, 64 insertions(+), 23 deletions(-) diff --git a/Source/Editor/CustomEditors/CustomEditorPresenter.cs b/Source/Editor/CustomEditors/CustomEditorPresenter.cs index 1030abfda..ccf712904 100644 --- a/Source/Editor/CustomEditors/CustomEditorPresenter.cs +++ b/Source/Editor/CustomEditors/CustomEditorPresenter.cs @@ -63,6 +63,11 @@ namespace FlaxEditor.CustomEditors /// Indication of if the properties window is locked on specific objects. /// public bool LockSelection { get; set; } + + /// + /// Gets the scene editing context. + /// + public ISceneEditingContext SceneContext { get; } } /// diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index 5b3701789..ecdc1f7db 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -1,8 +1,5 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; -using System.Collections.Generic; -using System.Linq; using FlaxEditor.Actions; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.CustomEditors.Elements; @@ -10,10 +7,14 @@ using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Tree; using FlaxEditor.Scripting; +using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; using FlaxEngine.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; namespace FlaxEditor.CustomEditors.Dedicated { @@ -542,10 +543,7 @@ namespace FlaxEditor.CustomEditors.Dedicated var prefabObjectId = removedActor.PrefabObject.PrefabObjectID; string data = JsonSerializer.Serialize(removedActor.PrefabObject); JsonSerializer.Deserialize(restored, data); - if (Presenter.Owner is PropertiesWindow propertiesWindow) - Editor.Instance.SceneEditing.Spawn(restored, parentActor, removedActor.OrderInParent); - else if (Presenter.Owner is PrefabWindow prefabWindow) - prefabWindow.Spawn(restored, parentActor, removedActor.OrderInParent); + Presenter.Owner.SceneContext.Spawn(restored, parentActor, removedActor.OrderInParent); Actor.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(restored), ref prefabId, ref prefabObjectId); return; } @@ -568,17 +566,9 @@ namespace FlaxEditor.CustomEditors.Dedicated Editor.Log("Reverting added actor changes to prefab (removing it)"); // TODO: Keep previous selection. - if (Presenter.Owner is PropertiesWindow propertiesWindow) - { - var editorInstance = Editor.Instance.SceneEditing; - editorInstance.Select(a); - editorInstance.Delete(); - } - else if (Presenter.Owner is PrefabWindow prefabWindow) - { - prefabWindow.Select(prefabWindow.Graph.Root.Find(a)); - prefabWindow.Delete(); - } + var context = Presenter.Owner.SceneContext; + context.Select(SceneGraph.SceneGraphFactory.FindNode(a.ID)); + context.DeleteSelection(); return; } diff --git a/Source/Editor/SceneGraph/ISceneEditingContext.cs b/Source/Editor/SceneGraph/ISceneEditingContext.cs index d480ceba5..cc750d854 100644 --- a/Source/Editor/SceneGraph/ISceneEditingContext.cs +++ b/Source/Editor/SceneGraph/ISceneEditingContext.cs @@ -1,8 +1,9 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System.Collections.Generic; using FlaxEditor.SceneGraph; using FlaxEditor.Viewport; +using FlaxEngine; +using System.Collections.Generic; namespace FlaxEditor { @@ -39,9 +40,23 @@ namespace FlaxEditor /// void RenameSelection(); + /// + /// Deletes selected objects. + /// + void DeleteSelection(); + /// /// Focuses selected objects. /// void FocusSelection(); + + /// + /// Spawns the specified actor to the game (with undo). + /// + /// The actor. + /// The parent actor. Set null as default. + /// The order under the parent to put the spawned actor. + /// True if automatically select the spawned actor, otherwise false. + void Spawn(Actor actor, Actor parent = null, int orderInParent = -1, bool autoSelect = true); } } diff --git a/Source/Editor/SceneGraph/RootNode.cs b/Source/Editor/SceneGraph/RootNode.cs index 1a3e47be8..71a9dc83a 100644 --- a/Source/Editor/SceneGraph/RootNode.cs +++ b/Source/Editor/SceneGraph/RootNode.cs @@ -175,7 +175,7 @@ namespace FlaxEditor.SceneGraph public List Selection => SceneContext.Selection; /// - /// Gets the list of selected scene graph nodes in the editor context. + /// Gets the scene editing context. /// public abstract ISceneEditingContext SceneContext { get; } } diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 2b86664f2..03ba26fc8 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -641,5 +641,8 @@ namespace FlaxEditor.Windows.Assets /// public bool LockSelection { get; set; } + + /// + public ISceneEditingContext SceneContext => null; } } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index 24854afb9..0eecc4861 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -430,6 +430,12 @@ namespace FlaxEditor.Windows.Assets } } + /// + public void DeleteSelection() + { + Delete(); + } + /// public void FocusSelection() { @@ -488,7 +494,8 @@ namespace FlaxEditor.Windows.Assets /// The actor. /// The parent. /// The order of the actor under the parent. - public void Spawn(Actor actor, Actor parent, int orderInParent = -1) + /// True if automatically select the spawned actor, otherwise false. + public void Spawn(Actor actor, Actor parent, int orderInParent = -1, bool autoSelect = true) { if (actor == null) throw new ArgumentNullException(nameof(actor)); @@ -514,8 +521,11 @@ namespace FlaxEditor.Windows.Assets // Create undo action var action = new CustomDeleteActorsAction(new List(1) { actorNode }, true); Undo.AddAction(action); - Focus(); - Select(actorNode); + if (autoSelect) + { + Focus(); + Select(actorNode); + } } private void OnTreeRightClick(TreeNode node, Float2 location) diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 44c21d863..7f17053ac 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -91,6 +91,9 @@ namespace FlaxEditor.Windows.Assets } } + /// + public ISceneEditingContext SceneContext => this; + /// /// Gets or sets a value indicating whether use live reloading for the prefab changes (applies prefab changes on modification by auto). /// diff --git a/Source/Editor/Windows/PropertiesWindow.cs b/Source/Editor/Windows/PropertiesWindow.cs index e90003038..03316a221 100644 --- a/Source/Editor/Windows/PropertiesWindow.cs +++ b/Source/Editor/Windows/PropertiesWindow.cs @@ -58,6 +58,9 @@ namespace FlaxEditor.Windows } } + /// + public ISceneEditingContext SceneContext => Editor.Windows.EditWin; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Windows/SceneEditorWindow.cs b/Source/Editor/Windows/SceneEditorWindow.cs index e3c72d069..888cb0888 100644 --- a/Source/Editor/Windows/SceneEditorWindow.cs +++ b/Source/Editor/Windows/SceneEditorWindow.cs @@ -26,12 +26,24 @@ namespace FlaxEditor.Windows FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); } + /// + public void DeleteSelection() + { + Editor.SceneEditing.Delete(); + } + /// public void FocusSelection() { Editor.Windows.EditWin.Viewport.FocusSelection(); } + /// + public void Spawn(Actor actor, Actor parent = null, int orderInParent = -1, bool autoSelect = true) + { + Editor.SceneEditing.Spawn(actor, parent, orderInParent, autoSelect); + } + /// public EditorViewport Viewport => Editor.Windows.EditWin.Viewport; From 5222f1d35cc10e1166b209b94c7007ebe0275f52 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 29 Aug 2025 21:03:04 +0200 Subject: [PATCH 15/23] Add support for parsing inheritance with preprocessor blocks inside it --- .../Bindings/BindingsGenerator.Parsing.cs | 7 + .../Flax.Build/Bindings/BindingsGenerator.cs | 220 +++++++++--------- 2 files changed, 123 insertions(+), 104 deletions(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs index 7841b98c3..ae77cd508 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs @@ -13,6 +13,7 @@ namespace Flax.Build.Bindings public FileInfo File; public Tokenizer Tokenizer; public ApiTypeInfo ScopeInfo; + public NativeCpp.BuildOptions ModuleOptions; public AccessLevel CurrentAccessLevel; public Stack ScopeTypeStack; public Stack ScopeAccessStack; @@ -534,6 +535,12 @@ namespace Flax.Build.Bindings } if (token.Type == TokenType.LeftCurlyBrace) break; + if (token.Type == TokenType.Preprocessor) + { + OnPreProcessorToken(ref context, ref token); + while (token.Type == TokenType.Newline) + token = context.Tokenizer.NextToken(); + } if (token.Type == TokenType.Colon) { token = context.Tokenizer.ExpectToken(TokenType.Colon); diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs index b1c388e29..5a7daf405 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs @@ -245,6 +245,7 @@ namespace Flax.Build.Bindings File = fileInfo, Tokenizer = tokenizer, ScopeInfo = null, + ModuleOptions = moduleOptions, CurrentAccessLevel = AccessLevel.Public, ScopeTypeStack = new Stack(), ScopeAccessStack = new Stack(), @@ -380,110 +381,7 @@ namespace Flax.Build.Bindings // Handle preprocessor blocks if (token.Type == TokenType.Preprocessor) { - token = tokenizer.NextToken(); - switch (token.Value) - { - case "define": - { - token = tokenizer.NextToken(); - var name = token.Value; - var value = string.Empty; - token = tokenizer.NextToken(true); - while (token.Type != TokenType.Newline) - { - value += token.Value; - token = tokenizer.NextToken(true); - } - value = value.Trim(); - context.PreprocessorDefines[name] = value; - break; - } - case "if": - case "elif": - { - // Parse condition - var condition = string.Empty; - token = tokenizer.NextToken(true); - while (token.Type != TokenType.Newline) - { - var tokenValue = token.Value.Trim(); - if (tokenValue.Length == 0) - { - token = tokenizer.NextToken(true); - continue; - } - - // Very simple defines processing - var negate = tokenValue[0] == '!'; - if (negate) - tokenValue = tokenValue.Substring(1); - tokenValue = ReplacePreProcessorDefines(tokenValue, context.PreprocessorDefines); - tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.PublicDefinitions); - tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.PrivateDefinitions); - tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.CompileEnv.PreprocessorDefinitions); - tokenValue = tokenValue.Replace("false", "0"); - tokenValue = tokenValue.Replace("true", "1"); - tokenValue = tokenValue.Replace("||", "|"); - if (tokenValue.Length != 0 && tokenValue != "1" && tokenValue != "0" && tokenValue != "|") - tokenValue = "0"; - if (negate) - tokenValue = tokenValue == "1" ? "0" : "1"; - - condition += tokenValue; - token = tokenizer.NextToken(true); - } - - // Filter condition - bool modified; - do - { - modified = false; - if (condition.Contains("1|1")) - { - condition = condition.Replace("1|1", "1"); - modified = true; - } - if (condition.Contains("1|0")) - { - condition = condition.Replace("1|0", "1"); - modified = true; - } - if (condition.Contains("0|1")) - { - condition = condition.Replace("0|1", "1"); - modified = true; - } - } while (modified); - - // Skip chunk of code of condition fails - if (condition != "1") - { - ParsePreprocessorIf(fileInfo, tokenizer, ref token); - } - - break; - } - case "ifdef": - { - // Parse condition - var define = string.Empty; - token = tokenizer.NextToken(true); - while (token.Type != TokenType.Newline) - { - define += token.Value; - token = tokenizer.NextToken(true); - } - - // Check condition - define = define.Trim(); - if (!context.PreprocessorDefines.ContainsKey(define) && !moduleOptions.CompileEnv.PreprocessorDefinitions.Contains(define)) - { - ParsePreprocessorIf(fileInfo, tokenizer, ref token); - } - - break; - } - } + OnPreProcessorToken(ref context, ref token); } // Scope tracking @@ -514,6 +412,120 @@ namespace Flax.Build.Bindings } } + private static void OnPreProcessorToken(ref ParsingContext context, ref Token token) + { + var tokenizer = context.Tokenizer; + token = tokenizer.NextToken(); + switch (token.Value) + { + case "define": + { + token = tokenizer.NextToken(); + var name = token.Value; + var value = string.Empty; + token = tokenizer.NextToken(true); + while (token.Type != TokenType.Newline) + { + value += token.Value; + token = tokenizer.NextToken(true); + } + value = value.Trim(); + context.PreprocessorDefines[name] = value; + break; + } + case "if": + case "elif": + { + // Parse condition + var condition = string.Empty; + token = tokenizer.NextToken(true); + while (token.Type != TokenType.Newline) + { + var tokenValue = token.Value.Trim(); + if (tokenValue.Length == 0) + { + token = tokenizer.NextToken(true); + continue; + } + + // Very simple defines processing + var negate = tokenValue[0] == '!'; + if (negate) + tokenValue = tokenValue.Substring(1); + tokenValue = ReplacePreProcessorDefines(tokenValue, context.PreprocessorDefines); + tokenValue = ReplacePreProcessorDefines(tokenValue, context.ModuleOptions.PublicDefinitions); + tokenValue = ReplacePreProcessorDefines(tokenValue, context.ModuleOptions.PrivateDefinitions); + tokenValue = ReplacePreProcessorDefines(tokenValue, context.ModuleOptions.CompileEnv.PreprocessorDefinitions); + tokenValue = tokenValue.Replace("false", "0"); + tokenValue = tokenValue.Replace("true", "1"); + tokenValue = tokenValue.Replace("||", "|"); + if (tokenValue.Length != 0 && tokenValue != "1" && tokenValue != "0" && tokenValue != "|") + tokenValue = "0"; + if (negate) + tokenValue = tokenValue == "1" ? "0" : "1"; + + condition += tokenValue; + token = tokenizer.NextToken(true); + } + + // Filter condition + bool modified; + do + { + modified = false; + if (condition.Contains("1|1")) + { + condition = condition.Replace("1|1", "1"); + modified = true; + } + if (condition.Contains("1|0")) + { + condition = condition.Replace("1|0", "1"); + modified = true; + } + if (condition.Contains("0|1")) + { + condition = condition.Replace("0|1", "1"); + modified = true; + } + } while (modified); + + // Skip chunk of code of condition fails + if (condition != "1") + { + ParsePreprocessorIf(context.File, tokenizer, ref token); + } + + break; + } + case "ifdef": + { + // Parse condition + var define = string.Empty; + token = tokenizer.NextToken(true); + while (token.Type != TokenType.Newline) + { + define += token.Value; + token = tokenizer.NextToken(true); + } + + // Check condition + define = define.Trim(); + if (!context.PreprocessorDefines.ContainsKey(define) && !context.ModuleOptions.CompileEnv.PreprocessorDefinitions.Contains(define)) + { + ParsePreprocessorIf(context.File, tokenizer, ref token); + } + + break; + } + case "endif": + { + token = tokenizer.NextToken(true); + break; + } + } + } + private static string ReplacePreProcessorDefines(string text, IEnumerable defines) { foreach (var define in defines) From 9fafb47abbb311b1495c7623773e6ce29f0bd128 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 29 Aug 2025 21:03:44 +0200 Subject: [PATCH 16/23] Fix debug drawing wheeled vehicle in prefab viewport #3591 --- .../Editor/Viewport/PrefabWindowViewport.cs | 5 +- Source/Engine/Debug/DebugDraw.cpp | 8 +-- Source/Engine/Debug/DebugDraw.h | 6 +-- Source/Engine/Level/Scene/SceneRendering.cpp | 5 +- Source/Engine/Level/Scene/SceneRendering.h | 18 +++---- Source/Engine/Physics/Actors/Cloth.cpp | 4 +- Source/Engine/Physics/Actors/Cloth.h | 8 +-- Source/Engine/Physics/Actors/IPhysicsDebug.h | 18 +++++++ .../Engine/Physics/Actors/WheeledVehicle.cpp | 53 ++++++++++--------- Source/Engine/Physics/Actors/WheeledVehicle.h | 10 ++-- Source/Engine/Physics/Colliders/BoxCollider.h | 4 +- .../Physics/Colliders/CapsuleCollider.h | 4 +- .../Physics/Colliders/CharacterController.h | 4 +- Source/Engine/Physics/Colliders/Collider.cpp | 12 +---- Source/Engine/Physics/Colliders/Collider.h | 7 +-- .../Engine/Physics/Colliders/MeshCollider.h | 4 +- .../Engine/Physics/Colliders/SphereCollider.h | 4 +- .../Engine/Physics/Colliders/SplineCollider.h | 4 +- Source/Engine/Physics/Joints/Joint.cpp | 4 +- Source/Engine/Physics/Joints/Joint.h | 8 +-- Source/Engine/Terrain/Terrain.cpp | 4 +- Source/Engine/Terrain/Terrain.h | 8 +-- 22 files changed, 102 insertions(+), 100 deletions(-) create mode 100644 Source/Engine/Physics/Actors/IPhysicsDebug.h diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 8b508eedf..a982f6447 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -663,10 +663,7 @@ namespace FlaxEditor.Viewport if ((view.Flags & ViewFlags.PhysicsDebug) != 0 || view.Mode == ViewMode.PhysicsColliders) { foreach (var actor in _debugDrawActors) - { - if (actor is Collider c && c.IsActiveInHierarchy) - DebugDraw.DrawColliderDebugPhysics(c, renderContext.View); - } + DebugDraw.DrawDebugPhysics(actor, renderContext.View); } // Draw lights debug diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index 2bb921f70..a6709fab3 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -30,6 +30,7 @@ #include "Editor/Editor.h" #include "Engine/Level/Actors/Light.h" #include "Engine/Physics/Colliders/Collider.h" +#include "Engine/Physics/Actors/IPhysicsDebug.h" #endif // Debug draw service configuration @@ -1027,11 +1028,12 @@ void DebugDraw::DrawActorsTree(Actor* actor) #if USE_EDITOR -void DebugDraw::DrawColliderDebugPhysics(Collider* collider, RenderView& view) +void DebugDraw::DrawDebugPhysics(Actor* actor, RenderView& view) { - if (!collider) + if (!actor || !actor->IsActiveInHierarchy()) return; - collider->DrawPhysicsDebug(view); + if (auto* physicsDebug = dynamic_cast(actor)) + physicsDebug->DrawPhysicsDebug(view); } void DebugDraw::DrawLightDebug(Light* light, RenderView& view) diff --git a/Source/Engine/Debug/DebugDraw.h b/Source/Engine/Debug/DebugDraw.h index 3b51c0e13..6ddbbaf39 100644 --- a/Source/Engine/Debug/DebugDraw.h +++ b/Source/Engine/Debug/DebugDraw.h @@ -102,11 +102,11 @@ API_CLASS(Static) class FLAXENGINE_API DebugDraw #if USE_EDITOR /// - /// Draws the physics debug shapes for the given collider. Editor Only + /// Draws the physics debug shapes for the given actor. Editor Only /// - /// The collider to draw. + /// The actor to draw. /// The render view to draw in. - API_FUNCTION() static void DrawColliderDebugPhysics(Collider* collider, RenderView& view); + API_FUNCTION() static void DrawDebugPhysics(Actor* actor, RenderView& view); /// /// Draws the light debug shapes for the given light. Editor Only diff --git a/Source/Engine/Level/Scene/SceneRendering.cpp b/Source/Engine/Level/Scene/SceneRendering.cpp index 445447cd1..8261a39cc 100644 --- a/Source/Engine/Level/Scene/SceneRendering.cpp +++ b/Source/Engine/Level/Scene/SceneRendering.cpp @@ -9,6 +9,7 @@ #include "Engine/Threading/JobSystem.h" #include "Engine/Threading/Threading.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Physics/Actors/IPhysicsDebug.h" ISceneRenderingListener::~ISceneRenderingListener() { @@ -91,10 +92,10 @@ void SceneRendering::Draw(RenderContextBatch& renderContextBatch, DrawCategory c if (EnumHasAnyFlags(view.Flags, ViewFlags::PhysicsDebug) || view.Mode == ViewMode::PhysicsColliders) { PROFILE_CPU_NAMED("PhysicsDebug"); - const PhysicsDebugCallback* physicsDebugData = PhysicsDebug.Get(); + const auto* physicsDebugData = PhysicsDebug.Get(); for (int32 i = 0; i < PhysicsDebug.Count(); i++) { - physicsDebugData[i](view); + physicsDebugData[i]->DrawPhysicsDebug(view); } } diff --git a/Source/Engine/Level/Scene/SceneRendering.h b/Source/Engine/Level/Scene/SceneRendering.h index 043f5079e..cab16c450 100644 --- a/Source/Engine/Level/Scene/SceneRendering.h +++ b/Source/Engine/Level/Scene/SceneRendering.h @@ -11,6 +11,7 @@ class SceneRenderTask; class SceneRendering; +class IPhysicsDebug; struct PostProcessSettings; struct RenderContext; struct RenderContextBatch; @@ -74,7 +75,6 @@ public: class FLAXENGINE_API SceneRendering { #if USE_EDITOR - typedef Function PhysicsDebugCallback; typedef Function LightsDebugCallback; friend class ViewportIconsRendererService; #endif @@ -105,7 +105,7 @@ public: private: #if USE_EDITOR - Array PhysicsDebug; + Array PhysicsDebug; Array LightsDebug; Array ViewportIcons; #endif @@ -149,20 +149,14 @@ public: } #if USE_EDITOR - template - FORCE_INLINE void AddPhysicsDebug(T* obj) + FORCE_INLINE void AddPhysicsDebug(IPhysicsDebug* obj) { - PhysicsDebugCallback f; - f.Bind(obj); - PhysicsDebug.Add(f); + PhysicsDebug.Add(obj); } - template - void RemovePhysicsDebug(T* obj) + FORCE_INLINE void RemovePhysicsDebug(IPhysicsDebug* obj) { - PhysicsDebugCallback f; - f.Bind(obj); - PhysicsDebug.Remove(f); + PhysicsDebug.Remove(obj); } template diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index b184bfbda..48db20065 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -457,7 +457,7 @@ void Cloth::OnEnable() { GetSceneRendering()->AddActor(this, _sceneRenderingKey); #if USE_EDITOR - GetSceneRendering()->AddPhysicsDebug(this); + GetSceneRendering()->AddPhysicsDebug(this); #endif #if WITH_CLOTH if (_cloth) @@ -476,7 +476,7 @@ void Cloth::OnDisable() PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); #endif #if USE_EDITOR - GetSceneRendering()->RemovePhysicsDebug(this); + GetSceneRendering()->RemovePhysicsDebug(this); #endif GetSceneRendering()->RemoveActor(this, _sceneRenderingKey); } diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h index d10c18fea..f83f1c499 100644 --- a/Source/Engine/Physics/Actors/Cloth.h +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -4,6 +4,7 @@ #include "Engine/Level/Actor.h" #include "Engine/Level/Actors/ModelInstanceActor.h" +#include "IPhysicsDebug.h" // Used internally to validate cloth data against invalid nan/inf values #define USE_CLOTH_SANITY_CHECKS 0 @@ -12,6 +13,9 @@ /// Physical simulation actor for cloth objects made of vertices that are simulated as cloth particles with physical properties, forces, and constraints to affect cloth behavior. /// API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Cloth\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API Cloth : public Actor +#if USE_EDITOR + , public IPhysicsDebug +#endif { DECLARE_SCENE_OBJECT(Cloth); @@ -364,9 +368,7 @@ protected: void OnPhysicsSceneChanged(PhysicsScene* previous) override; private: -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view); -#endif + ImplementPhysicsDebug; bool CreateCloth(); void DestroyCloth(); void CalculateInvMasses(Array& invMasses); diff --git a/Source/Engine/Physics/Actors/IPhysicsDebug.h b/Source/Engine/Physics/Actors/IPhysicsDebug.h new file mode 100644 index 000000000..cedd3d822 --- /dev/null +++ b/Source/Engine/Physics/Actors/IPhysicsDebug.h @@ -0,0 +1,18 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Types/BaseTypes.h" + +#if USE_EDITOR +class FLAXENGINE_API IPhysicsDebug +{ +public: + virtual void DrawPhysicsDebug(struct RenderView& view) + { + } +}; +#define ImplementPhysicsDebug void DrawPhysicsDebug(RenderView& view) +#else +#define ImplementPhysicsDebug +#endif diff --git a/Source/Engine/Physics/Actors/WheeledVehicle.cpp b/Source/Engine/Physics/Actors/WheeledVehicle.cpp index 28808be17..35f78f39f 100644 --- a/Source/Engine/Physics/Actors/WheeledVehicle.cpp +++ b/Source/Engine/Physics/Actors/WheeledVehicle.cpp @@ -356,19 +356,19 @@ void WheeledVehicle::Setup() void WheeledVehicle::DrawPhysicsDebug(RenderView& view) { // Wheels shapes - for (const auto& data : _wheelsData) + for (const auto& wheel : _wheels) { - int32 wheelIndex = 0; - for (; wheelIndex < _wheels.Count(); wheelIndex++) - { - if (_wheels[wheelIndex].Collider == data.Collider) - break; - } - if (wheelIndex == _wheels.Count()) - break; - const auto& wheel = _wheels[wheelIndex]; if (wheel.Collider && wheel.Collider->GetParent() == this && !wheel.Collider->GetIsTrigger()) { + WheelData data = { wheel.Collider, wheel.Collider->GetLocalOrientation() }; + for (auto& e : _wheelsData) + { + if (e.Collider == data.Collider) + { + data = e; + break; + } + } const Vector3 currentPos = wheel.Collider->GetPosition(); const Vector3 basePos = currentPos - Vector3(0, data.State.SuspensionOffset, 0); const Quaternion wheelDebugOrientation = GetOrientation() * Quaternion::Euler(-data.State.RotationAngle, data.State.SteerAngle, 0) * Quaternion::Euler(90, 0, 90); @@ -387,25 +387,28 @@ void WheeledVehicle::DrawPhysicsDebug(RenderView& view) void WheeledVehicle::OnDebugDrawSelected() { // Wheels shapes - for (const auto& data : _wheelsData) + for (const auto& wheel : _wheels) { - int32 wheelIndex = 0; - for (; wheelIndex < _wheels.Count(); wheelIndex++) - { - if (_wheels[wheelIndex].Collider == data.Collider) - break; - } - if (wheelIndex == _wheels.Count()) - break; - const auto& wheel = _wheels[wheelIndex]; if (wheel.Collider && wheel.Collider->GetParent() == this && !wheel.Collider->GetIsTrigger()) { + WheelData data = { wheel.Collider, wheel.Collider->GetLocalOrientation() }; + for (auto& e : _wheelsData) + { + if (e.Collider == data.Collider) + { + data = e; + break; + } + } const Vector3 currentPos = wheel.Collider->GetPosition(); const Vector3 basePos = currentPos - Vector3(0, data.State.SuspensionOffset, 0); const Quaternion wheelDebugOrientation = GetOrientation() * Quaternion::Euler(-data.State.RotationAngle, data.State.SteerAngle, 0) * Quaternion::Euler(90, 0, 90); - Transform actorPose = Transform::Identity, shapePose = Transform::Identity; - PhysicsBackend::GetRigidActorPose(_actor, actorPose.Translation, actorPose.Orientation); - PhysicsBackend::GetShapeLocalPose(wheel.Collider->GetPhysicsShape(), shapePose.Translation, shapePose.Orientation); + Transform actorPose = GetTransform(), shapePose = wheel.Collider->GetLocalTransform(); + actorPose.Scale = Float3::One; + if (_actor) + PhysicsBackend::GetRigidActorPose(_actor, actorPose.Translation, actorPose.Orientation); + if (wheel.Collider->GetPhysicsShape()) + PhysicsBackend::GetShapeLocalPose(wheel.Collider->GetPhysicsShape(), shapePose.Translation, shapePose.Orientation); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(basePos, wheel.Radius * 0.07f), Color::Blue * 0.3f, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(currentPos, wheel.Radius * 0.08f), Color::Blue * 0.8f, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(actorPose.LocalToWorld(shapePose.Translation), wheel.Radius * 0.11f), Color::OrangeRed * 0.8f, 0, false); @@ -561,14 +564,14 @@ void WheeledVehicle::BeginPlay(SceneBeginData* data) #endif #if USE_EDITOR - GetSceneRendering()->AddPhysicsDebug(this); + GetSceneRendering()->AddPhysicsDebug(this); #endif } void WheeledVehicle::EndPlay() { #if USE_EDITOR - GetSceneRendering()->RemovePhysicsDebug(this); + GetSceneRendering()->RemovePhysicsDebug(this); #endif #if WITH_VEHICLE diff --git a/Source/Engine/Physics/Actors/WheeledVehicle.h b/Source/Engine/Physics/Actors/WheeledVehicle.h index 037157158..390c0e24a 100644 --- a/Source/Engine/Physics/Actors/WheeledVehicle.h +++ b/Source/Engine/Physics/Actors/WheeledVehicle.h @@ -5,12 +5,16 @@ #include "Engine/Physics/Actors/RigidBody.h" #include "Engine/Physics/Colliders/Collider.h" #include "Engine/Scripting/ScriptingObjectReference.h" +#include "IPhysicsDebug.h" /// /// Representation of the car vehicle that uses wheels. Built on top of the RigidBody with collider representing its chassis shape and wheels. /// /// API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Wheeled Vehicle\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API WheeledVehicle : public RigidBody +#if USE_EDITOR + , public IPhysicsDebug +#endif { friend class PhysicsBackend; friend struct ScenePhysX; @@ -644,13 +648,9 @@ public: /// API_FUNCTION() void Setup(); -private: -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view); -#endif - public: // [Vehicle] + ImplementPhysicsDebug; #if USE_EDITOR void OnDebugDrawSelected() override; #endif diff --git a/Source/Engine/Physics/Colliders/BoxCollider.h b/Source/Engine/Physics/Colliders/BoxCollider.h index 9b9b964a1..c0eeaea5c 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.h +++ b/Source/Engine/Physics/Colliders/BoxCollider.h @@ -58,9 +58,7 @@ public: protected: // [Collider] + ImplementPhysicsDebug; void UpdateBounds() override; void GetGeometry(CollisionShape& collision) override; -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view) override; -#endif }; diff --git a/Source/Engine/Physics/Colliders/CapsuleCollider.h b/Source/Engine/Physics/Colliders/CapsuleCollider.h index 9275b99e1..234916f87 100644 --- a/Source/Engine/Physics/Colliders/CapsuleCollider.h +++ b/Source/Engine/Physics/Colliders/CapsuleCollider.h @@ -62,9 +62,7 @@ public: protected: // [Collider] + ImplementPhysicsDebug; void UpdateBounds() override; void GetGeometry(CollisionShape& collision) override; -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view) override; -#endif }; diff --git a/Source/Engine/Physics/Colliders/CharacterController.h b/Source/Engine/Physics/Colliders/CharacterController.h index 918f5beb9..367b87df1 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.h +++ b/Source/Engine/Physics/Colliders/CharacterController.h @@ -267,13 +267,11 @@ public: protected: // [PhysicsActor] + ImplementPhysicsDebug; void UpdateGeometry() override; void GetGeometry(CollisionShape& collision) override; void BeginPlay(SceneBeginData* data) override; void EndPlay() override; -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view) override; -#endif void OnActiveInTreeChanged() override; void OnEnable() override; void OnDisable() override; diff --git a/Source/Engine/Physics/Colliders/Collider.cpp b/Source/Engine/Physics/Colliders/Collider.cpp index 0ff51e8e6..8249577c2 100644 --- a/Source/Engine/Physics/Colliders/Collider.cpp +++ b/Source/Engine/Physics/Colliders/Collider.cpp @@ -147,7 +147,7 @@ void Collider::OnEnable() if (EnumHasAnyFlags(_staticFlags, StaticFlags::Navigation) && !_isTrigger) GetScene()->Navigation.Actors.Add(this); #if USE_EDITOR - GetSceneRendering()->AddPhysicsDebug(this); + GetSceneRendering()->AddPhysicsDebug(this); #endif PhysicsColliderActor::OnEnable(); @@ -160,7 +160,7 @@ void Collider::OnDisable() if (EnumHasAnyFlags(_staticFlags, StaticFlags::Navigation) && !_isTrigger) GetScene()->Navigation.Actors.Remove(this); #if USE_EDITOR - GetSceneRendering()->RemovePhysicsDebug(this); + GetSceneRendering()->RemovePhysicsDebug(this); #endif } @@ -286,14 +286,6 @@ void Collider::RemoveStaticActor() _staticActor = nullptr; } -#if USE_EDITOR - -void Collider::DrawPhysicsDebug(RenderView& view) -{ -} - -#endif - void Collider::OnMaterialChanged() { // Update the shape material diff --git a/Source/Engine/Physics/Colliders/Collider.h b/Source/Engine/Physics/Colliders/Collider.h index 835d89a22..5fd34f187 100644 --- a/Source/Engine/Physics/Colliders/Collider.h +++ b/Source/Engine/Physics/Colliders/Collider.h @@ -6,6 +6,7 @@ #include "Engine/Content/JsonAsset.h" #include "Engine/Content/JsonAssetReference.h" #include "Engine/Physics/Actors/PhysicsColliderActor.h" +#include "Engine/Physics/Actors/IPhysicsDebug.h" struct RayCastHit; class RigidBody; @@ -16,6 +17,9 @@ class RigidBody; /// /// API_CLASS(Abstract) class FLAXENGINE_API Collider : public PhysicsColliderActor +#if USE_EDITOR + , public IPhysicsDebug +#endif { API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT_ABSTRACT(Collider); @@ -165,9 +169,6 @@ public: void ClosestPoint(const Vector3& point, Vector3& result) const final; bool ContainsPoint(const Vector3& point) const final; -#if USE_EDITOR - virtual void DrawPhysicsDebug(RenderView& view); -#endif protected: // [PhysicsColliderActor] diff --git a/Source/Engine/Physics/Colliders/MeshCollider.h b/Source/Engine/Physics/Colliders/MeshCollider.h index e6b1b7a82..99f2980f6 100644 --- a/Source/Engine/Physics/Colliders/MeshCollider.h +++ b/Source/Engine/Physics/Colliders/MeshCollider.h @@ -37,9 +37,7 @@ public: protected: // [Collider] -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view) override; -#endif + ImplementPhysicsDebug; void UpdateBounds() override; void GetGeometry(CollisionShape& collision) override; }; diff --git a/Source/Engine/Physics/Colliders/SphereCollider.h b/Source/Engine/Physics/Colliders/SphereCollider.h index 3d9df79b3..67addf17b 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.h +++ b/Source/Engine/Physics/Colliders/SphereCollider.h @@ -42,9 +42,7 @@ public: protected: // [Collider] -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view) override; -#endif + ImplementPhysicsDebug; void UpdateBounds() override; void GetGeometry(CollisionShape& collision) override; }; diff --git a/Source/Engine/Physics/Colliders/SplineCollider.h b/Source/Engine/Physics/Colliders/SplineCollider.h index cb8f1d5ee..1e41802e5 100644 --- a/Source/Engine/Physics/Colliders/SplineCollider.h +++ b/Source/Engine/Physics/Colliders/SplineCollider.h @@ -67,9 +67,7 @@ public: protected: // [Collider] -#if USE_EDITOR - void DrawPhysicsDebug(RenderView& view) override; -#endif + ImplementPhysicsDebug; void UpdateBounds() override; void GetGeometry(CollisionShape& collision) override; }; diff --git a/Source/Engine/Physics/Joints/Joint.cpp b/Source/Engine/Physics/Joints/Joint.cpp index 950f2d146..902a58796 100644 --- a/Source/Engine/Physics/Joints/Joint.cpp +++ b/Source/Engine/Physics/Joints/Joint.cpp @@ -298,7 +298,7 @@ void Joint::EndPlay() void Joint::OnEnable() { - GetSceneRendering()->AddPhysicsDebug(this); + GetSceneRendering()->AddPhysicsDebug(this); // Base Actor::OnEnable(); @@ -306,7 +306,7 @@ void Joint::OnEnable() void Joint::OnDisable() { - GetSceneRendering()->RemovePhysicsDebug(this); + GetSceneRendering()->RemovePhysicsDebug(this); // Base Actor::OnDisable(); diff --git a/Source/Engine/Physics/Joints/Joint.h b/Source/Engine/Physics/Joints/Joint.h index 7583e449d..299a66f9e 100644 --- a/Source/Engine/Physics/Joints/Joint.h +++ b/Source/Engine/Physics/Joints/Joint.h @@ -5,6 +5,7 @@ #include "Engine/Level/Actor.h" #include "Engine/Physics/Types.h" #include "Engine/Scripting/ScriptingObjectReference.h" +#include "Engine/Physics/Actors/IPhysicsDebug.h" class IPhysicsActor; @@ -17,6 +18,9 @@ class IPhysicsActor; /// /// API_CLASS(Abstract) class FLAXENGINE_API Joint : public Actor +#if USE_EDITOR + , public IPhysicsDebug +#endif { DECLARE_SCENE_OBJECT_ABSTRACT(Joint); protected: @@ -174,9 +178,7 @@ protected: Vector3 GetTargetPosition() const; Quaternion GetTargetOrientation() const; virtual void* CreateJoint(const struct PhysicsJointDesc& desc) = 0; -#if USE_EDITOR - virtual void DrawPhysicsDebug(RenderView& view); -#endif + ImplementPhysicsDebug; private: void Delete(); diff --git a/Source/Engine/Terrain/Terrain.cpp b/Source/Engine/Terrain/Terrain.cpp index ebbfd70e6..f13d3bcc4 100644 --- a/Source/Engine/Terrain/Terrain.cpp +++ b/Source/Engine/Terrain/Terrain.cpp @@ -845,7 +845,7 @@ void Terrain::OnEnable() GetScene()->Navigation.Actors.Add(this); GetSceneRendering()->AddActor(this, _sceneRenderingKey); #if TERRAIN_USE_PHYSICS_DEBUG - GetSceneRendering()->AddPhysicsDebug(this); + GetSceneRendering()->AddPhysicsDebug(this); #endif void* scene = GetPhysicsScene()->GetPhysicsScene(); for (int32 i = 0; i < _patches.Count(); i++) @@ -866,7 +866,7 @@ void Terrain::OnDisable() GetScene()->Navigation.Actors.Remove(this); GetSceneRendering()->RemoveActor(this, _sceneRenderingKey); #if TERRAIN_USE_PHYSICS_DEBUG - GetSceneRendering()->RemovePhysicsDebug(this); + GetSceneRendering()->RemovePhysicsDebug(this); #endif void* scene = GetPhysicsScene()->GetPhysicsScene(); for (int32 i = 0; i < _patches.Count(); i++) diff --git a/Source/Engine/Terrain/Terrain.h b/Source/Engine/Terrain/Terrain.h index 0b72cd668..5c671629d 100644 --- a/Source/Engine/Terrain/Terrain.h +++ b/Source/Engine/Terrain/Terrain.h @@ -5,6 +5,7 @@ #include "Engine/Content/JsonAssetReference.h" #include "Engine/Content/Assets/MaterialBase.h" #include "Engine/Physics/Actors/PhysicsColliderActor.h" +#include "Engine/Physics/Actors/IPhysicsDebug.h" class Terrain; class TerrainChunk; @@ -38,6 +39,9 @@ struct RenderView; /// /// API_CLASS(Sealed) class FLAXENGINE_API Terrain : public PhysicsColliderActor +#if USE_EDITOR + , public IPhysicsDebug +#endif { DECLARE_SCENE_OBJECT(Terrain); friend Terrain; @@ -441,9 +445,7 @@ public: API_FUNCTION() void DrawChunk(API_PARAM(Ref) const RenderContext& renderContext, API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* material, int32 lodIndex = 0) const; private: -#if TERRAIN_USE_PHYSICS_DEBUG - void DrawPhysicsDebug(RenderView& view); -#endif + ImplementPhysicsDebug; bool DrawSetup(RenderContext& renderContext); void DrawImpl(RenderContext& renderContext, HashSet& drawnChunks); From 91cd1e8065a4dbead6c390f8a75f8a09305ab16a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 29 Aug 2025 21:49:14 +0200 Subject: [PATCH 17/23] Fix regression from ef7c7f2d30b7b77f992d6a0cfabdabe4c53ef26c to ignore unmodified scripts in diff --- Source/Editor/CustomEditors/Dedicated/ActorEditor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index ecdc1f7db..d312f64b2 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -423,6 +423,9 @@ namespace FlaxEditor.CustomEditors.Dedicated } } + if (editor is ScriptsEditor && result != null && result.ChildrenCount == 0) + return null; + return result; } From 848cc38bf1e17aac9c120031594d9c45b2659492 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 29 Aug 2025 22:04:03 +0200 Subject: [PATCH 18/23] Fix reverting prefab object reference in nested prefabs #3255 --- Source/Editor/CustomEditors/CustomEditor.cs | 42 +++++++++++++-------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 0c739739c..ff9a14ea3 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -749,6 +749,15 @@ namespace FlaxEditor.CustomEditors } } + private Actor FindActor(CustomEditor editor) + { + if (editor.Values[0] is Actor actor) + return actor; + if (editor.ParentEditor != null) + return FindActor(editor.ParentEditor); + return null; + } + private Actor FindPrefabRoot(CustomEditor editor) { if (editor.Values[0] is Actor actor) @@ -767,32 +776,35 @@ namespace FlaxEditor.CustomEditors return FindPrefabRoot(actor.Parent); } - private SceneObject FindObjectWithPrefabObjectId(Actor actor, ref Guid prefabObjectId) + private SceneObject FindObjectWithPrefabObjectId(Actor actor, ref Guid prefabObjectId, Actor endPoint) { + var visited = new HashSet(); + return FindObjectWithPrefabObjectId(actor, ref prefabObjectId, endPoint, visited); + } + + private SceneObject FindObjectWithPrefabObjectId(Actor actor, ref Guid prefabObjectId, Actor endPoint, HashSet visited) + { + if (visited.Contains(actor) || actor is Scene || actor == endPoint) + return null; if (actor.PrefabObjectID == prefabObjectId) return actor; for (int i = 0; i < actor.ScriptsCount; i++) { - if (actor.GetScript(i).PrefabObjectID == prefabObjectId) - { - var a = actor.GetScript(i); - if (a != null) - return a; - } + var script = actor.GetScript(i); + if (script != null && script.PrefabObjectID == prefabObjectId) + return script; } for (int i = 0; i < actor.ChildrenCount; i++) { - if (actor.GetChild(i).PrefabObjectID == prefabObjectId) - { - var a = actor.GetChild(i); - if (a != null) - return a; - } + var child = actor.GetChild(i); + if (child != null && child.PrefabObjectID == prefabObjectId) + return child; } - return null; + // Go up in the hierarchy + return FindObjectWithPrefabObjectId(actor.Parent, ref prefabObjectId, endPoint, visited); } /// @@ -826,7 +838,7 @@ namespace FlaxEditor.CustomEditors } var prefabObjectId = referenceSceneObject.PrefabObjectID; - var prefabInstanceRef = FindObjectWithPrefabObjectId(prefabInstanceRoot, ref prefabObjectId); + var prefabInstanceRef = FindObjectWithPrefabObjectId(FindActor(this), ref prefabObjectId, prefabInstanceRoot); if (prefabInstanceRef == null) { Editor.LogWarning("Missing prefab instance reference in the prefab instance. Cannot revert to it."); From 3f7fe635d81857db2bcc6806e4bff1337f8c91d5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 29 Aug 2025 23:04:46 +0200 Subject: [PATCH 19/23] Fix 5222f1d35cc10e1166b209b94c7007ebe0275f52 for inactive preprocessor conditional block --- Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs index ae77cd508..14058aaf5 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs @@ -538,7 +538,7 @@ namespace Flax.Build.Bindings if (token.Type == TokenType.Preprocessor) { OnPreProcessorToken(ref context, ref token); - while (token.Type == TokenType.Newline) + while (token.Type == TokenType.Newline || token.Value == "endif") token = context.Tokenizer.NextToken(); } if (token.Type == TokenType.Colon) From 8fdda1a71a7349dbd2ac90fe3d91135f443efc4b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 30 Aug 2025 23:52:54 +0200 Subject: [PATCH 20/23] Fix scene object reference serialization in C++ to use correct serializer #3255 --- Source/Engine/Serialization/Serialization.cpp | 4 ++-- Source/Engine/Serialization/Serialization.h | 19 ++++++++++++------- .../Engine/Serialization/SerializationFwd.h | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp index 45b03a07c..a3dfc6ffa 100644 --- a/Source/Engine/Serialization/Serialization.cpp +++ b/Source/Engine/Serialization/Serialization.cpp @@ -790,14 +790,14 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Matrix DESERIALIZE_HELPER(stream, "M44", v.M44, 0); } -bool Serialization::ShouldSerialize(const SceneObject* v, const SceneObject* other) +bool Serialization::ShouldSerializeRef(const SceneObject* v, const SceneObject* other) { bool result = v != other; if (result && v && other && v->HasPrefabLink() && other->HasPrefabLink()) { // Special case when saving reference to prefab object and the objects are different but the point to the same prefab object // In that case, skip saving reference as it's defined in prefab (will be populated via IdsMapping during deserialization) - result &= v->GetPrefabObjectID() != other->GetPrefabObjectID(); + result = v->GetPrefabObjectID() != other->GetPrefabObjectID(); } return result; } diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h index b8167e8d2..d3205ad83 100644 --- a/Source/Engine/Serialization/Serialization.h +++ b/Source/Engine/Serialization/Serialization.h @@ -448,15 +448,20 @@ namespace Serialization // Scripting Object - FLAXENGINE_API bool ShouldSerialize(const SceneObject* v, const SceneObject* other); + inline bool ShouldSerializeRef(const ScriptingObject* v, const ScriptingObject* other) + { + return v != other; + } + + FLAXENGINE_API bool ShouldSerializeRef(const SceneObject* v, const SceneObject* other); template - inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const T*& v, const void* otherObj) + inline typename TEnableIf, TNot>>::Value, bool>::Type ShouldSerialize(const T* v, const void* otherObj) { return !otherObj || v != *(const T**)otherObj; } template - inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const T*& v, const void* otherObj) + inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const T* v, const void* otherObj) { stream.Guid(v ? v->GetID() : Guid::Empty); } @@ -470,9 +475,9 @@ namespace Serialization } template - inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const T*& v, const void* otherObj) + inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const T* v, const void* otherObj) { - return !otherObj || ShouldSerialize((const SceneObject*)v, *(const SceneObject**)otherObj); + return !otherObj || ShouldSerializeRef((const SceneObject*)v, *(const SceneObject**)otherObj); } // Scripting Object Reference @@ -480,7 +485,7 @@ namespace Serialization template inline bool ShouldSerialize(const ScriptingObjectReference& v, const void* otherObj) { - return !otherObj || ShouldSerialize(v.Get(), ((ScriptingObjectReference*)otherObj)->Get()); + return !otherObj || ShouldSerializeRef(v.Get(), ((ScriptingObjectReference*)otherObj)->Get()); } template inline void Serialize(ISerializable::SerializeStream& stream, const ScriptingObjectReference& v, const void* otherObj) @@ -501,7 +506,7 @@ namespace Serialization template inline bool ShouldSerialize(const SoftObjectReference& v, const void* otherObj) { - return !otherObj || ShouldSerialize(v.Get(), ((SoftObjectReference*)otherObj)->Get()); + return !otherObj || ShouldSerializeRef(v.Get(), ((SoftObjectReference*)otherObj)->Get()); } template inline void Serialize(ISerializable::SerializeStream& stream, const SoftObjectReference& v, const void* otherObj) diff --git a/Source/Engine/Serialization/SerializationFwd.h b/Source/Engine/Serialization/SerializationFwd.h index 84b51cb39..58ece7fda 100644 --- a/Source/Engine/Serialization/SerializationFwd.h +++ b/Source/Engine/Serialization/SerializationFwd.h @@ -80,7 +80,7 @@ class ISerializeModifier; // Explicit auto-cast for object pointer #define SERIALIZE_OBJ(name) \ - if (Serialization::ShouldSerialize((const ScriptingObject*&)name, other ? &other->name : nullptr)) \ + if (Serialization::ShouldSerialize(name, decltype(name)(other ? &other->name : nullptr))) \ { \ stream.JKEY(#name); \ Serialization::Serialize(stream, (const ScriptingObject*&)name, other ? &other->name : nullptr); \ From 1042ad4e7d06d1feafe5ceb528ea8c3146499a04 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Sep 2025 21:41:36 +0200 Subject: [PATCH 21/23] Fix object ids remapping inside nested prefabs #3255 --- Source/Editor/Modules/SceneModule.cs | 4 + Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 43 ++++- Source/Engine/Level/Prefabs/PrefabManager.cpp | 8 +- Source/Engine/Level/SceneObjectsFactory.cpp | 104 +++++++++-- Source/Engine/Level/SceneObjectsFactory.h | 11 +- Source/Engine/Tests/TestPrefabs.cpp | 165 ++++++++++++++++++ 6 files changed, 313 insertions(+), 22 deletions(-) diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs index 56c420964..1f3490c67 100644 --- a/Source/Editor/Modules/SceneModule.cs +++ b/Source/Editor/Modules/SceneModule.cs @@ -568,6 +568,10 @@ namespace FlaxEditor.Modules return; } + // Skip if already added + if (SceneGraphFactory.Nodes.ContainsKey(actor.ID)) + return; + var node = SceneGraphFactory.BuildActorNode(actor); if (node != null) { diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 4409ae161..0bed02f0b 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -304,6 +304,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI { for (int32 instanceIndex = 0; instanceIndex < prefabInstancesData.Count(); instanceIndex++) { + PROFILE_CPU_NAMED("Instance"); auto& instance = prefabInstancesData[instanceIndex]; ISerializeModifierCacheType modifier = Cache::ISerializeModifier.Get(); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); @@ -377,6 +378,10 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI sceneObjects->Add(obj); } + // Generate nested prefab instances to properly handle Ids Mapping within each nested prefab + SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, instance.Data, modifier.Value); + SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData); + // Apply modifications for (int32 i = existingObjectsCount - 1; i >= 0; i--) { @@ -388,6 +393,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI if (prefabObjectIdToDiffData.TryGet(obj->GetPrefabObjectID(), data)) { // Apply prefab changes + context.SetupIdsMapping(obj, modifier.Value); obj->Deserialize(*(ISerializable::DeserializeStream*)data, modifier.Value); } else @@ -424,7 +430,6 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI for (int32 i = 0; i < sceneObjects->Count(); i++) { SceneObject* obj = sceneObjects->At(i); - int32 dataIndex; if (instance.PrefabInstanceIdToDataIndex.TryGet(obj->GetSceneObjectId(), dataIndex)) { @@ -440,6 +445,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI data.RemoveMember("ParentID"); #endif + context.SetupIdsMapping(obj, modifier.Value); obj->Deserialize(data, modifier.Value); // Preserve order in parent (values from prefab are used) @@ -527,6 +533,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI { if (prefabInstancesData.IsEmpty()) return false; + PROFILE_CPU(); // Fully serialize default instance scene objects (accumulate all prefab and nested prefabs changes into a single linear list of objects) rapidjson_flax::Document defaultInstanceData; @@ -926,7 +933,6 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr // TODO: what if user applied prefab with references to the other objects from scene? clear them or what? JsonTools::ChangeIds(diffDataDocument, objectInstanceIdToPrefabObjectId); } - dataBuffer.Clear(); CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); // Destroy default instance and some cache data in Prefab @@ -1002,6 +1008,32 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr obj->RegisterObject(); } + // Generate nested prefab instances to properly handle Ids Mapping within each nested prefab + rapidjson_flax::Document targetDataDocument; + if (NestedPrefabs.HasItems()) + { + targetDataDocument.Parse(dataBuffer.GetString(), dataBuffer.GetSize()); + SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, targetDataDocument, modifier.Value); + SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData); + + if (context.Instances.HasItems()) + { + // Only main prefab instance is allowed (in case nested prefab was added to this prefab) + for (auto i = context.ObjectToInstance.Begin(); i.IsNotEnd(); ++i) + { + if (i->Value != 0) + context.ObjectToInstance.Remove(i); + } + context.Instances.Resize(1); + + // Trash object mapping to prevent messing up prefab structure when applying hierarchy changes (only nested instances are used) + context.Instances[0].IdsMapping.Clear(); + } + } + + dataBuffer.Clear(); + auto originalIdsMapping = modifier.Value->IdsMapping; + // Deserialize prefab objects and apply modifications for (int32 i = 0; i < ObjectsCount; i++) { @@ -1037,6 +1069,9 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr } } + // Use the initial Ids Mapping (SetupIdsMapping overrides it for instanced prefabs) + modifier.Value->IdsMapping = originalIdsMapping; + // Deserialize new prefab objects newPrefabInstanceIdToDataIndexCounter = 0; for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i) @@ -1283,6 +1318,10 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r ObjectsDataCache.Add(objectId, &objData); ObjectsCount++; + Guid parentID; + if (JsonTools::GetGuidIfValid(parentID, objData, "ParentID")) + ObjectsHierarchyCache[parentID].Add(objectId); + Guid prefabId = JsonTools::GetGuid(objData, "PrefabID"); if (prefabId.IsValid() && !NestedPrefabs.Contains(prefabId)) { diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index d1ee99721..a0b4bb807 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -172,7 +172,9 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const SpawnOptions& options) SceneObjectsFactory::HandleObjectDeserializationError(stream); } SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, data, modifier.Value); - if (options.WithSync) + bool withSync = options.WithSync || prefab->NestedPrefabs.HasItems(); // Nested prefabs needs prefab instances generation for correct IdsMapping if the same prefab exists multiple times + // TODO: let prefab check if has multiple nested prefabs at cook time? + if (withSync) { // Synchronize new prefab instances (prefab may have new objects added so deserialized instances need to synchronize with it) // TODO: resave and force sync prefabs during game cooking so this step could be skipped in game @@ -187,14 +189,14 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const SpawnOptions& options) if (obj) SceneObjectsFactory::Deserialize(context, obj, stream); } - Scripting::ObjectsLookupIdMapping.Set(prevIdMapping); // Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it) - if (options.WithSync) + if (withSync) { // TODO: resave and force sync scenes during game cooking so this step could be skipped in game SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData); } + Scripting::ObjectsLookupIdMapping.Set(prevIdMapping); // Pick prefab root object Actor* root = nullptr; diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index b0f968808..d4e96b9d7 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -101,13 +101,25 @@ ISerializeModifier* SceneObjectsFactory::Context::GetModifier() void SceneObjectsFactory::Context::SetupIdsMapping(const SceneObject* obj, ISerializeModifier* modifier) const { int32 instanceIndex; - if (ObjectToInstance.TryGet(obj->GetID(), instanceIndex) && instanceIndex != modifier->CurrentInstance) + const Guid id = obj->GetID(); + if (ObjectToInstance.TryGet(id, instanceIndex)) { // Apply the current prefab instance objects ids table to resolve references inside a prefab properly - modifier->CurrentInstance = instanceIndex; const auto& instance = Instances[instanceIndex]; - for (const auto& e : instance.IdsMapping) - modifier->IdsMapping[e.Key] = e.Value; + if (instanceIndex != modifier->CurrentInstance) + { + modifier->CurrentInstance = instanceIndex; + for (const auto& e : instance.IdsMapping) + modifier->IdsMapping[e.Key] = e.Value; + } + int32 nestedIndex; + if (instance.ObjectToNested.TryGet(id, nestedIndex)) + { + // Each nested prefab has own object ids mapping that takes precedence + const auto& nested = instance.Nested[nestedIndex]; + for (const auto& e : nested.IdsMapping) + modifier->IdsMapping[e.Key] = e.Value; + } } } @@ -499,6 +511,34 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn prefab = Content::LoadAsync(prefabId); if (prefab && !prefab->WaitForLoaded()) { + // If prefab instance contains multiple nested prefabs, then need to build separate Ids remapping for each of them to prevent collisions + int32 nestedIndex = -1; + if (prefabInstance.Nested.HasItems()) + { + // Check only the last instance if the current object belongs to it + for (int32 j = prefabInstance.Nested.Count() - 1; j >= 0; j--) + { + const auto& e = prefabInstance.Nested[j]; + if (e.Prefab == prefab && e.RootObjectId != prefabObjectId) + { + nestedIndex = j; + break; + } + } + } + if (nestedIndex == -1) + { + nestedIndex = prefabInstance.Nested.Count(); + auto& e = prefabInstance.Nested.AddOne(); + e.Prefab = prefab; + e.RootObjectId = prefabObjectId; + } + + // Map this object into this instance to inherit all objects from it when looking up via IdsMapping + prefabInstance.ObjectToNested[id] = nestedIndex; + auto& nestedInstance = prefabInstance.Nested[nestedIndex]; + nestedInstance.IdsMapping[prefabObjectId] = id; + // Map prefab object ID to the deserialized instance ID prefabInstance.IdsMapping[prefabObjectId] = id; goto NESTED_PREFAB_WALK; @@ -787,23 +827,20 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab if (spawned) continue; - // Map prefab object ID to this actor's prefab instance so the new objects gets added to it + // Map prefab object ID to this actor's prefab instance so the new objects get added to it context.SetupIdsMapping(actor, data.Modifier); data.Modifier->IdsMapping[actorPrefabObjectId] = actorId; Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); // Create instance (including all children) - SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId); + SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId, actor->GetID()); } } -void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId) +void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId, const Guid& nestedInstanceId) { PROFILE_CPU_NAMED("SynchronizeNewPrefabInstance"); - // Missing object found! - LOG(Info, "Actor {0} has missing child object (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", actor->ToString(), prefabObjectId, prefab->GetID(), prefab->GetPath()); - // Get prefab object data from the prefab const ISerializable::DeserializeStream* prefabData; if (!prefab->ObjectsDataCache.TryGet(prefabObjectId, prefabData)) @@ -823,6 +860,7 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabS LOG(Warning, "Failed to create object {1} from prefab {0}.", prefab->ToString(), prefabObjectId); return; } + LOG(Info, "Actor {0} has missing child object '{4}' (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", actor->ToString(), prefabObjectId, prefab->GetID(), prefab->GetPath(), child->GetType().ToString()); // Register object child->RegisterObject(); @@ -839,17 +877,51 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabS context.ObjectToInstance[id] = instanceIndex; auto& prefabInstance = context.Instances[instanceIndex]; prefabInstance.IdsMapping[prefabObjectId] = id; + + // Check if it's a nested prefab + const ISerializable::DeserializeStream* nestedPrefabData; + Guid nestedPrefabObjectId; + if (prefab->ObjectsDataCache.TryGet(prefabObjectId, nestedPrefabData) && JsonTools::GetGuidIfValid(nestedPrefabObjectId, *nestedPrefabData, "PrefabObjectID")) + { + // Try reusing parent nested instance (or make a new one) + int32 nestedIndex = -1; + if (!prefabInstance.ObjectToNested.TryGet(nestedInstanceId, nestedIndex)) + { + if (auto* nestedPrefab = Content::LoadAsync(JsonTools::GetGuid(*prefabData, "PrefabID"))) + { + if (prefabInstance.Nested.HasItems()) + { + // Check only the last instance if the current object belongs to it + const auto& e = prefabInstance.Nested.Last(); + if (e.Prefab == nestedPrefab && e.RootObjectId != nestedPrefabObjectId) + nestedIndex = prefabInstance.Nested.Count() - 1; + } + if (nestedIndex == -1) + { + nestedIndex = prefabInstance.Nested.Count(); + auto& e = prefabInstance.Nested.AddOne(); + e.Prefab = nestedPrefab; + e.RootObjectId = nestedPrefabObjectId; + } + } + } + if (nestedIndex != -1) + { + // Insert into nested instance + prefabInstance.ObjectToNested[id] = nestedIndex; + auto& nestedInstance = prefabInstance.Nested[nestedIndex]; + nestedInstance.IdsMapping[nestedPrefabObjectId] = id; + } + } } // Use loop to add even more objects to added objects (prefab can have one new object that has another child, we need to add that child) - // TODO: prefab could cache lookup object id -> children ids - for (auto q = prefab->ObjectsDataCache.Begin(); q.IsNotEnd(); ++q) + const auto* hierarchy = prefab->ObjectsHierarchyCache.TryGet(prefabObjectId); + if (hierarchy) { - Guid qParentId; - if (JsonTools::GetGuidIfValid(qParentId, *q->Value, "ParentID") && qParentId == prefabObjectId) + for (const Guid& e : *hierarchy) { - const Guid qPrefabObjectId = JsonTools::GetGuid(*q->Value, "ID"); - SynchronizeNewPrefabInstance(context, data, prefab, actor, qPrefabObjectId); + SynchronizeNewPrefabInstance(context, data, prefab, actor, e, id); } } } diff --git a/Source/Engine/Level/SceneObjectsFactory.h b/Source/Engine/Level/SceneObjectsFactory.h index ab4138ea1..2bd6c6b21 100644 --- a/Source/Engine/Level/SceneObjectsFactory.h +++ b/Source/Engine/Level/SceneObjectsFactory.h @@ -13,6 +13,13 @@ class FLAXENGINE_API SceneObjectsFactory { public: + struct NestedPrefabInstance + { + Prefab* Prefab; + Guid RootObjectId; + Dictionary IdsMapping; + }; + struct PrefabInstance { int32 StatIndex; @@ -21,6 +28,8 @@ public: Prefab* Prefab; bool FixRootParent = false; Dictionary IdsMapping; + Array Nested; + Dictionary ObjectToNested; }; struct Context @@ -133,5 +142,5 @@ public: 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); + static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId, const Guid& nestedInstanceId); }; diff --git a/Source/Engine/Tests/TestPrefabs.cpp b/Source/Engine/Tests/TestPrefabs.cpp index 5e19a5fa0..6389f1cf7 100644 --- a/Source/Engine/Tests/TestPrefabs.cpp +++ b/Source/Engine/Tests/TestPrefabs.cpp @@ -687,6 +687,171 @@ TEST_CASE("Prefabs") instanceA->DeleteObject(); instanceB->DeleteObject(); instanceC->DeleteObject(); + } + SECTION("Test Loading Nested Prefab With Multiple Instances of Nested Prefab") + { + // https://github.com/FlaxEngine/FlaxEngine/issues/3255 + // Create Prefab C with basic cross-object references + AssetReference prefabC = Content::CreateVirtualAsset(); + REQUIRE(prefabC); + Guid id; + Guid::Parse("cccbe4b0416be0777a6ce59e8788b10f", id); + prefabC->ChangeID(id); + auto prefabCInit = prefabC->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"aac6b9644492fbca1a6ab0a7904a557e\"," + "\"TypeName\": \"FlaxEngine.ExponentialHeightFog\"," + "\"Name\": \"Prefab C.Root\"," + "\"DirectionalInscatteringLight\": \"bbb6b9644492fbca1a6ab0a7904a557e\"" + "}," + "{" + "\"ID\": \"bbb6b9644492fbca1a6ab0a7904a557e\"," + "\"TypeName\": \"FlaxEngine.DirectionalLight\"," + "\"ParentID\": \"aac6b9644492fbca1a6ab0a7904a557e\"," + "\"Name\": \"Prefab C.Light\"" + "}" + "]"); + REQUIRE(!prefabCInit); + + // Create Prefab B with two nested Prefab C attached to the root + AssetReference prefabB = Content::CreateVirtualAsset(); + REQUIRE(prefabB); + SCOPE_EXIT{ Content::DeleteAsset(prefabB); }; + Guid::Parse("bbb744714f746e31855f41815612d14b", id); + prefabB->ChangeID(id); + auto prefabBInit = prefabB->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"244274a04cc60d56a2f024bfeef5772d\"," + "\"TypeName\": \"FlaxEngine.SpotLight\"," + "\"Name\": \"Prefab B.Root\"" + "}," + "{" + "\"ID\": \"1111f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"aac6b9644492fbca1a6ab0a7904a557e\"," + "\"ParentID\": \"244274a04cc60d56a2f024bfeef5772d\"" + "}," + "{" + "\"ID\": \"2221f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"bbb6b9644492fbca1a6ab0a7904a557e\"," + "\"ParentID\": \"1111f1094f430733333f8280e78dfcc3\"" + "}," + "{" + "\"ID\": \"3331f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"aac6b9644492fbca1a6ab0a7904a557e\"," + "\"ParentID\": \"244274a04cc60d56a2f024bfeef5772d\"" + "}," + "{" + "\"ID\": \"4441f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"cccbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"bbb6b9644492fbca1a6ab0a7904a557e\"," + "\"ParentID\": \"3331f1094f430733333f8280e78dfcc3\"" + "}" + "]"); + REQUIRE(!prefabBInit); + + // Create Prefab A as variant of Prefab B (no local changes, just object remapped) + AssetReference prefabA = Content::CreateVirtualAsset(); + REQUIRE(prefabA); + SCOPE_EXIT{ Content::DeleteAsset(prefabA); }; + Guid::Parse("aaa744714f746e31855f41815612d14b", id); + prefabA->ChangeID(id); + auto prefabAInit = prefabA->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"123274a04cc60d56a2f024bfeef5772d\"," + "\"PrefabID\": \"bbb744714f746e31855f41815612d14b\"," + "\"PrefabObjectID\": \"244274a04cc60d56a2f024bfeef5772d\"," + "\"Name\": \"Prefab A.Root\"" + "}," + "{" + "\"ID\": \"1211f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"bbb744714f746e31855f41815612d14b\"," + "\"PrefabObjectID\": \"1111f1094f430733333f8280e78dfcc3\"," + "\"ParentID\": \"123274a04cc60d56a2f024bfeef5772d\"" + "}," + "{" + "\"ID\": \"4221f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"bbb744714f746e31855f41815612d14b\"," + "\"PrefabObjectID\": \"2221f1094f430733333f8280e78dfcc3\"," + "\"ParentID\": \"1211f1094f430733333f8280e78dfcc3\"" + "}," + "{" + "\"ID\": \"3131f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"bbb744714f746e31855f41815612d14b\"," + "\"PrefabObjectID\": \"3331f1094f430733333f8280e78dfcc3\"," + "\"ParentID\": \"123274a04cc60d56a2f024bfeef5772d\"" + "}," + "{" + "\"ID\": \"5441f1094f430733333f8280e78dfcc3\"," + "\"PrefabID\": \"bbb744714f746e31855f41815612d14b\"," + "\"PrefabObjectID\": \"4441f1094f430733333f8280e78dfcc3\"," + "\"ParentID\": \"3131f1094f430733333f8280e78dfcc3\"" + "}" + "]"); + REQUIRE(!prefabAInit); + + // Spawn test instances of both prefabs + ScriptingObjectReference instanceA = PrefabManager::SpawnPrefab(prefabA); + ScriptingObjectReference instanceB = PrefabManager::SpawnPrefab(prefabB); + ScriptingObjectReference instanceC = PrefabManager::SpawnPrefab(prefabC); + + // Check state of objects + REQUIRE(instanceC); + REQUIRE(instanceC->Is()); + REQUIRE(instanceC->Children.Count() == 1); + CHECK(instanceC.As()->DirectionalInscatteringLight == instanceC->Children[0]); + REQUIRE(instanceB); + REQUIRE(instanceB->Children.Count() == 2); + ScriptingObjectReference instanceB1 = instanceB->Children[0]; + ScriptingObjectReference instanceB2 = instanceB->Children[1]; + REQUIRE(instanceB1->Is()); + REQUIRE(instanceB1->Children.Count() == 1); + CHECK(instanceB1.As()->DirectionalInscatteringLight == instanceB1->Children[0]); + REQUIRE(instanceB2->Is()); + REQUIRE(instanceB2->Children.Count() == 1); + CHECK(instanceB2.As()->DirectionalInscatteringLight == instanceB2->Children[0]); + REQUIRE(instanceA); + REQUIRE(instanceA->Children.Count() == 2); + ScriptingObjectReference instanceA1 = instanceA->Children[0]; + ScriptingObjectReference instanceA2 = instanceA->Children[1]; + REQUIRE(instanceA1->Is()); + REQUIRE(instanceA1->Children.Count() == 1); + CHECK(instanceA1.As()->DirectionalInscatteringLight == instanceA1->Children[0]); + REQUIRE(instanceA2->Is()); + REQUIRE(instanceA1->Children.Count() == 1); + CHECK(instanceA2.As()->DirectionalInscatteringLight == instanceA2->Children[0]); + + // Add instance of Prefab C to Prefab B + instanceC->SetName(StringView(TEXT("New"))); + instanceC->SetParent(instanceB); + bool applyResult = PrefabManager::ApplyAll(instanceB); + REQUIRE(!applyResult); + + // Check if Prefab A reflects that change + REQUIRE(instanceA); + REQUIRE(instanceA->Children.Count() == 3); + instanceA1 = instanceA->Children[0]; + instanceA2 = instanceA->Children[1]; + ScriptingObjectReference instanceA3 = instanceA->Children[2]; + REQUIRE(instanceA1->Is()); + REQUIRE(instanceA1->Children.Count() == 1); + CHECK(instanceA1.As()->DirectionalInscatteringLight == instanceA1->Children[0]); + REQUIRE(instanceA2->Is()); + REQUIRE(instanceA2->Children.Count() == 1); + CHECK(instanceA2.As()->DirectionalInscatteringLight == instanceA2->Children[0]); + REQUIRE(instanceA3->Is()); + REQUIRE(instanceA3->Children.Count() == 1); + CHECK(instanceA3.As()->DirectionalInscatteringLight == instanceA3->Children[0]); + + // Cleanup + instanceA->DeleteObject(); + instanceB->DeleteObject(); + instanceC->DeleteObject(); } } From ad1163bccc06faa3842dffd2bf47efa9786a2167 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Sep 2025 21:41:54 +0200 Subject: [PATCH 22/23] Fix `HashSet::Add` returning incorrect value --- Source/Engine/Core/Collections/HashSet.h | 4 ++-- Source/Engine/Core/Collections/HashSetBase.h | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index ab2601525..6751be0dc 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -408,7 +408,7 @@ public: template bool Add(const ItemType& item) { - Bucket* bucket = Base::OnAdd(item, false); + Bucket* bucket = Base::OnAdd(item, false, true); if (bucket) bucket->Occupy(item); return bucket != nullptr; @@ -421,7 +421,7 @@ public: /// True if element has been added to the collection, otherwise false if the element is already present. bool Add(T&& item) { - Bucket* bucket = Base::OnAdd(item, false); + Bucket* bucket = Base::OnAdd(item, false, true); if (bucket) bucket->Occupy(MoveTemp(item)); return bucket != nullptr; diff --git a/Source/Engine/Core/Collections/HashSetBase.h b/Source/Engine/Core/Collections/HashSetBase.h index 200e26b7b..488613d2b 100644 --- a/Source/Engine/Core/Collections/HashSetBase.h +++ b/Source/Engine/Core/Collections/HashSetBase.h @@ -359,7 +359,7 @@ protected: } template - BucketType* OnAdd(const KeyComparableType& key, bool checkUnique = true) + BucketType* OnAdd(const KeyComparableType& key, bool checkUnique = true, bool nullIfNonUnique = false) { // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) if (_deletedCount * HASH_SET_DEFAULT_SLACK_SCALE > _size) @@ -380,6 +380,8 @@ protected: Platform::CheckFailed("That key has been already added to the collection.", __FILE__, __LINE__); return nullptr; } + if (nullIfNonUnique) + return nullptr; return &_allocation.Get()[pos.ObjectIndex]; } From eff5f84185c02fefc075d46e88cc3333fafd1ac8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Sep 2025 22:14:07 +0200 Subject: [PATCH 23/23] Improve shadow maps sharing in nested scene rendering --- Source/Engine/Renderer/ShadowsPass.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 19985f0be..0bf2f70d1 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -1368,10 +1368,11 @@ RETRY_ATLAS_SETUP: tile.LinkedRectTile = nullptr; auto& linkedTile = linkedAtlasLight->Tiles[tileIndex]; - // Check if both lights use the same projections - if (tile.WorldToShadow == linkedTile.WorldToShadow && linkedTile.RectTile) + // Link tile and use its projection + if (linkedTile.RectTile) { tile.LinkedRectTile = linkedTile.RectTile; + tile.WorldToShadow = linkedTile.WorldToShadow; } } }