From 46f842a5558b58cd908ee080fddd101444d13b39 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 8 Apr 2025 12:47:27 +0200 Subject: [PATCH] Add option to quickly create box, sphere, convex or triangle mesh collider for the selected static model --- .../Content/Proxy/CollisionDataProxy.cs | 43 +++--- Source/Editor/Modules/SceneEditingModule.cs | 8 +- .../SceneGraph/Actors/StaticModelNode.cs | 123 ++++++++++++------ 3 files changed, 116 insertions(+), 58 deletions(-) diff --git a/Source/Editor/Content/Proxy/CollisionDataProxy.cs b/Source/Editor/Content/Proxy/CollisionDataProxy.cs index b01cf29f5..7d2f421cf 100644 --- a/Source/Editor/Content/Proxy/CollisionDataProxy.cs +++ b/Source/Editor/Content/Proxy/CollisionDataProxy.cs @@ -75,13 +75,34 @@ namespace FlaxEditor.Content throw new Exception("Failed to create new asset."); } + private bool TryUseCollisionData(Model model, BinaryAssetItem assetItem, Action created, bool alwaysDeferCallback, CollisionDataType type) + { + var collisionData = FlaxEngine.Content.Load(assetItem.ID); + if (collisionData) + { + var options = collisionData.Options; + if ((options.Model == model.ID || options.Model == Guid.Empty) && options.Type == type) + { + Editor.Instance.Windows.ContentWin.Select(assetItem); + if (created != null && alwaysDeferCallback) + FlaxEngine.Scripting.InvokeOnUpdate(() => created(collisionData)); + else if (created != null) + created(collisionData); + return true; + } + } + return false; + } + /// /// Create collision data from model. /// /// The associated model. /// The action to call once the collision data gets created (or reused from existing). /// True if start initial item renaming by user, or tru to skip it. - public void CreateCollisionDataFromModel(Model model, Action created = null, bool withRenaming = true) + /// True if always call callback on the next engine update, otherwise callback might be called within this function if collision data already exists. + /// Type of the collider to create. + public void CreateCollisionDataFromModel(Model model, Action created = null, bool withRenaming = true, bool alwaysDeferCallback = true, CollisionDataType type = CollisionDataType.ConvexMesh) { // Check if there already is collision data for that model to reuse var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID); @@ -92,31 +113,19 @@ namespace FlaxEditor.Content // Check if there is collision that was made with this model if (child is BinaryAssetItem b && b.IsOfType()) { - var collisionData = FlaxEngine.Content.Load(b.ID); - if (collisionData && collisionData.Options.Model == model.ID) - { - Editor.Instance.Windows.ContentWin.Select(b); - if (created != null) - FlaxEngine.Scripting.InvokeOnUpdate(() => created(collisionData)); + if (TryUseCollisionData(model, b, created, alwaysDeferCallback, type)) return; - } } - // Check if there is a auto-imported collision + // Check if there is an auto-imported collision if (child is ContentFolder childFolder && childFolder.ShortName == modelItem.ShortName) { foreach (var childFolderChild in childFolder.Children) { if (childFolderChild is BinaryAssetItem c && c.IsOfType()) { - var collisionData = FlaxEngine.Content.Load(c.ID); - if (collisionData && (collisionData.Options.Model == model.ID || collisionData.Options.Model == Guid.Empty)) - { - Editor.Instance.Windows.ContentWin.Select(c); - if (created != null) - FlaxEngine.Scripting.InvokeOnUpdate(() => created(collisionData)); + if (TryUseCollisionData(model, c, created, alwaysDeferCallback, type)) return; - } } } } @@ -135,7 +144,7 @@ namespace FlaxEditor.Content } Task.Run(() => { - Editor.CookMeshCollision(assetItem.Path, CollisionDataType.ConvexMesh, model); + Editor.CookMeshCollision(assetItem.Path, type, model); if (created != null) FlaxEngine.Scripting.InvokeOnUpdate(() => created(collisionData)); }); diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs index 3e47fa5bd..e950b56a3 100644 --- a/Source/Editor/Modules/SceneEditingModule.cs +++ b/Source/Editor/Modules/SceneEditingModule.cs @@ -320,6 +320,10 @@ namespace FlaxEditor.Modules SpawnBegin?.Invoke(); + // During play in editor mode spawned actors should be dynamic (user can move them) + if (isPlayMode) + actor.StaticFlags = StaticFlags.None; + // Add it Level.SpawnActor(actor, parent); @@ -328,10 +332,6 @@ namespace FlaxEditor.Modules if (actorNode == null) throw new InvalidOperationException("Failed to create scene node for the spawned actor."); - // During play in editor mode spawned actors should be dynamic (user can move them) - if (isPlayMode) - actor.StaticFlags = StaticFlags.None; - // Call post spawn action (can possibly setup custom default values) actorNode.PostSpawn(); diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index e45b78c3c..abda67d90 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -101,7 +101,12 @@ namespace FlaxEditor.SceneGraph.Actors { base.OnContextMenu(contextMenu, window); - contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null; + var menu = contextMenu.AddChildMenu("Add collider"); + menu.Enabled = ((StaticModel)Actor).Model != null; + menu.ContextMenu.AddButton("Box", () => OnAddCollider(window, CreateBox)); + menu.ContextMenu.AddButton("Sphere", () => OnAddCollider(window, CreateSphere)); + menu.ContextMenu.AddButton("Convex", () => OnAddCollider(window, CreateConvex)); + menu.ContextMenu.AddButton("Triangle Mesh", () => OnAddCollider(window, CreateTriangle)); } /// @@ -143,7 +148,60 @@ namespace FlaxEditor.SceneGraph.Actors return base.GetActorSelectionPoints(); } - private void OnAddMeshCollider(EditorWindow window) + private delegate void Spawner(Collider collider); + private delegate void CreateCollider(StaticModel actor, Spawner spawner, bool singleNode); + + private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode) + { + var collider = new BoxCollider + { + Transform = actor.Transform, + }; + spawner(collider); + // BoxColliderNode fits the box collider automatically on spawn + } + + private void CreateSphere(StaticModel actor, Spawner spawner, bool singleNode) + { + var bounds = actor.Sphere; + var collider = new SphereCollider + { + Transform = actor.Transform, + + // Refit into the sphere bounds that are usually calculated from mesh box bounds + Position = bounds.Center, + Radius = bounds.Radius / Mathf.Max(actor.Scale.MaxValue, 0.0001f) * 0.707f, + }; + spawner(collider); + } + + private void CreateConvex(StaticModel actor, Spawner spawner, bool singleNode) + { + CreateMeshCollider(actor, spawner, singleNode, CollisionDataType.ConvexMesh); + } + + private void CreateTriangle(StaticModel actor, Spawner spawner, bool singleNode) + { + CreateMeshCollider(actor, spawner, singleNode, CollisionDataType.TriangleMesh); + } + + private void CreateMeshCollider(StaticModel actor, Spawner spawner, bool singleNode, CollisionDataType type) + { + // Create collision data (or reuse) and add collision actor + var created = (CollisionData collisionData) => + { + var collider = new MeshCollider + { + Transform = actor.Transform, + CollisionData = collisionData, + }; + spawner(collider); + }; + var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy(); + collisionDataProxy.CreateCollisionDataFromModel(actor.Model, created, singleNode, false, type); + } + + private void OnAddCollider(EditorWindow window, CreateCollider createCollider) { // Allow collider to be added to evey static model selection var selection = Array.Empty(); @@ -157,72 +215,63 @@ namespace FlaxEditor.SceneGraph.Actors { if (node is not StaticModelNode staticModelNode) continue; - + var actor = (StaticModel)staticModelNode.Actor; var model = ((StaticModel)staticModelNode.Actor).Model; if (!model) continue; + Spawner spawner = collider => + { + collider.StaticFlags = staticModelNode.Actor.StaticFlags; + staticModelNode.Root.Spawn(collider, staticModelNode.Actor); + var colliderNode = window is PrefabWindow prefabWindow ? prefabWindow.Graph.Root.Find(collider) : Editor.Instance.Scene.GetActorNode(collider); + createdNodes.Add(colliderNode); + }; // Special case for in-built Editor models that can use analytical collision var modelPath = model.Path; if (modelPath.EndsWith("/Primitives/Cube.flax", StringComparison.Ordinal)) { - var actor = new BoxCollider + var collider = new BoxCollider { - StaticFlags = staticModelNode.Actor.StaticFlags, + Transform = actor.Transform, }; - staticModelNode.Root.Spawn(actor, staticModelNode.Actor); - createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor)); + spawner(collider); continue; } if (modelPath.EndsWith("/Primitives/Sphere.flax", StringComparison.Ordinal)) { - var actor = new SphereCollider + var collider = new SphereCollider { - StaticFlags = staticModelNode.Actor.StaticFlags, + Transform = actor.Transform, }; - staticModelNode.Root.Spawn(actor, staticModelNode.Actor); - createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor)); + spawner(collider); + collider.LocalTransform = Transform.Identity; continue; } if (modelPath.EndsWith("/Primitives/Plane.flax", StringComparison.Ordinal)) { - var actor = new BoxCollider + spawner(new BoxCollider { - StaticFlags = staticModelNode.Actor.StaticFlags, + Transform = actor.Transform, Size = new Float3(100.0f, 100.0f, 1.0f), - }; - staticModelNode.Root.Spawn(actor, staticModelNode.Actor); - createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor)); + }); continue; } if (modelPath.EndsWith("/Primitives/Capsule.flax", StringComparison.Ordinal)) { - var actor = new CapsuleCollider + var collider = new CapsuleCollider { - StaticFlags = staticModelNode.Actor.StaticFlags, + Transform = actor.Transform, Radius = 25.0f, Height = 50.0f, }; - Editor.Instance.SceneEditing.Spawn(actor, staticModelNode.Actor); - actor.LocalPosition = new Vector3(0, 50.0f, 0); - actor.LocalOrientation = Quaternion.Euler(0, 0, 90.0f); - createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor)); + spawner(collider); + collider.LocalPosition = new Vector3(0, 50.0f, 0); + collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f); continue; } - // Create collision data (or reuse) and add collision actor - Action created = collisionData => - { - var actor = new MeshCollider - { - StaticFlags = staticModelNode.Actor.StaticFlags, - CollisionData = collisionData, - }; - staticModelNode.Root.Spawn(actor, staticModelNode.Actor); - createdNodes.Add(window is PrefabWindow pWindow ? pWindow.Graph.Root.Find(actor) : Editor.Instance.Scene.GetActorNode(actor)); - }; - var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy(); - collisionDataProxy.CreateCollisionDataFromModel(model, created, selection.Length == 1); + createCollider(actor, spawner, selection.Length == 1); } // Select all created nodes @@ -230,9 +279,9 @@ namespace FlaxEditor.SceneGraph.Actors { Editor.Instance.SceneEditing.Select(createdNodes); } - else if (window is PrefabWindow pWindow) + else if (window is PrefabWindow prefabWindow) { - pWindow.Select(createdNodes); + prefabWindow.Select(createdNodes); } } }