diff --git a/Editor/UIParticleEditor.cs b/Editor/UIParticleEditor.cs index 2a06548..8d7bd3c 100644 --- a/Editor/UIParticleEditor.cs +++ b/Editor/UIParticleEditor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -8,6 +9,7 @@ using UnityEngine; using UnityEngine.Profiling; using UnityEngine.UI; using Coffee.UIParticleInternal; +using Object = UnityEngine.Object; #if UNITY_2021_2_OR_NEWER using UnityEditor.Overlays; #else @@ -66,6 +68,7 @@ namespace Coffee.UIExtensions private ReorderableList _ro; private bool _showMax; private bool _is3DScaleMode; + private GameObject[] _gameObjects; private static readonly HashSet s_Shaders = new HashSet(); #if UNITY_2018 || UNITY_2019 @@ -183,6 +186,13 @@ namespace Coffee.UIExtensions y.hasMultipleDifferentValues || z.hasMultipleDifferentValues; } + + // Add temporary ParticleSystem for preview if enabled. + if (UIParticleProjectSettings.previewOnSelect) + { + _gameObjects = targets.OfType().Select(x => x.gameObject).ToArray(); + ParticleSystemPreviewSystem.Register(_gameObjects); + } } /// @@ -344,6 +354,10 @@ namespace Coffee.UIExtensions } } #endif + + // Remove the temporary ParticleSystem. + ParticleSystemPreviewSystem.DrawWarningForTemporary(_gameObjects); + Profiler.EndSample(); } @@ -482,7 +496,7 @@ namespace Coffee.UIExtensions #endif } - private static bool FixButton(bool show, string text) + private static bool FixButton(bool show, string text, GUIContent buttonText = null) { if (!show) return false; using (new EditorGUILayout.HorizontalScope(GUILayout.ExpandWidth(true))) @@ -490,7 +504,7 @@ namespace Coffee.UIExtensions EditorGUILayout.HelpBox(text, MessageType.Warning, true); using (new EditorGUILayout.VerticalScope()) { - return GUILayout.Button(s_ContentFix, GUILayout.Width(30)); + return GUILayout.Button(buttonText ?? s_ContentFix, GUILayout.ExpandWidth(false)); } } } diff --git a/Runtime/Internal/Utilities/Misc.cs b/Runtime/Internal/Utilities/Misc.cs index b6654cc..11518ee 100644 --- a/Runtime/Internal/Utilities/Misc.cs +++ b/Runtime/Internal/Utilities/Misc.cs @@ -48,15 +48,10 @@ namespace Coffee.UIParticleInternal { if (obj == null) return; #if UNITY_EDITOR - if (Application.isEditor) - { - Object.DestroyImmediate(obj); - } - else + Object.DestroyImmediate(obj, true); +#else + Object.Destroy(obj); #endif - { - Object.Destroy(obj); - } } [Conditional("UNITY_EDITOR")] diff --git a/Runtime/ParticleSystemPreviewer.cs b/Runtime/ParticleSystemPreviewer.cs new file mode 100644 index 0000000..a884fe5 --- /dev/null +++ b/Runtime/ParticleSystemPreviewer.cs @@ -0,0 +1,237 @@ +using System.Collections.Generic; +using System.Linq; +using Coffee.UIParticleInternal; +using UnityEditor; +using UnityEngine; + +namespace Coffee.UIExtensions +{ + [Icon("Packages/com.coffee.ui-particle/Editor/UIParticleIcon.png")] + [ExecuteAlways] + internal class ParticleSystemPreviewer : MonoBehaviour + { + // Do nothing. + } + +#if UNITY_EDITOR + [CustomEditor(typeof(ParticleSystemPreviewer))] + [CanEditMultipleObjects] + internal class ParticleSystemPreviewerEditor : Editor + { + private GameObject[] _gameObjects; + + private void OnEnable() + { + _gameObjects = targets.OfType().Select(x => x.gameObject).ToArray(); + ParticleSystemPreviewSystem.Register(_gameObjects); + } + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + ParticleSystemPreviewSystem.DrawWarningForTemporary(_gameObjects); + ParticleSystemPreviewSystem.DrawWarningForPermanent(_gameObjects); + } + } + + /// + /// This class manages temporary ParticleSystems for preview purposes. + /// When previewing in the editor, it is common to place an empty ParticleSystem as the root, but it consumes memory at runtime if included in the build. + /// The temporary ParticleSystems created by this class only exist when the specified GameObject is selected, and are automatically deleted when the selection is cleared. + /// + internal class ParticleSystemPreviewSystem : ScriptableSingleton + { + private const HideFlags k_TemporaryHideFlags = HideFlags.DontSave | HideFlags.NotEditable; + + [SerializeField] + private List m_PreviewObjects = new List(); + +#if UNITY_2019_3_OR_NEWER + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] +#endif + [InitializeOnLoadMethod] + public static void Initialize() + { + instance.OnSelectionChanged(); + + Selection.selectionChanged -= instance.OnSelectionChanged; + Selection.selectionChanged += instance.OnSelectionChanged; + } + + /// + /// Adds a temporary ParticleSystem to the specified GameObject for preview purposes. + /// + public static void Register(GameObject[] targets) + { + foreach (var target in targets) + { + Register(target); + } + } + + /// + /// Adds a temporary ParticleSystem to the specified GameObject for preview purposes. + /// + public static void Register(GameObject target) + { + if (!target) return; + if (EditorApplication.isPlaying) return; + if (instance.m_PreviewObjects.Contains(target)) return; + if (target.TryGetComponent(out var ps)) + { + if (ps.hideFlags == k_TemporaryHideFlags) + { + RegisterParticleSystem(ps); + } + + return; + } + + // Create temporary ParticleSystem for preview. + RegisterParticleSystem(target.AddComponent()); + } + + /// + /// Removes the temporary ParticleSystem associated with the specified GameObject. + /// + /// + public static void Unregister(GameObject target) + { + if (!target) return; + + var index = instance.m_PreviewObjects.IndexOf(target); + if (index < 0) return; + + instance.m_PreviewObjects.RemoveAt(index); + if (HasTemporaryParticleSystem(target)) + { + RemoveParticleSystem(target); + } + } + + private static void RegisterParticleSystem(ParticleSystem ps) + { + if (!ps) return; + if (EditorApplication.isPlaying) return; + + ps.hideFlags = k_TemporaryHideFlags; + + var emission = ps.emission; + emission.enabled = false; + var shape = ps.shape; + shape.enabled = false; + + if (ps.TryGetComponent(out var psr)) + { + psr.enabled = false; + psr.hideFlags = k_TemporaryHideFlags; + } + + instance.m_PreviewObjects.Add(ps.gameObject); + EditorUtility.SetDirty(ps.gameObject); + } + + /// + /// Removes the temporary ParticleSystem associated with the specified GameObject. + /// + /// + private static void RemoveParticleSystem(GameObject target) + { + if (target.TryGetComponent(out var ps)) + { + Misc.DestroyImmediate(ps); + EditorUtility.SetDirty(target); + } + + if (target.TryGetComponent(out var psr)) + { + Misc.DestroyImmediate(psr); + EditorUtility.SetDirty(target); + } + } + + /// + /// Checks if the specified GameObject has a temporary ParticleSystem. + /// + private static bool HasTemporaryParticleSystem(GameObject target) + { + return target + && instance.m_PreviewObjects.Contains(target) + && target.TryGetComponent(out var ps) + && ps.hideFlags == k_TemporaryHideFlags; + } + + /// + /// Checks if the specified GameObject has a permanent ParticleSystem. + /// + private static bool HasPermanentParticleSystem(GameObject target) + { + return target + && target.TryGetComponent(out var ps) + && ps.hideFlags != k_TemporaryHideFlags; + } + + private void OnSelectionChanged() + { + var selectedGameObjects = Selection.gameObjects; + for (var i = m_PreviewObjects.Count - 1; 0 <= i; i--) + { + var go = m_PreviewObjects[i]; + if (!go) + { + m_PreviewObjects.RemoveAt(i); + } + else if (EditorApplication.isPlaying && !selectedGameObjects.Contains(go)) + { + Unregister(go); + } + } + } + + public static void DrawWarningForTemporary(GameObject[] gameObjects) + { + if (gameObjects == null || gameObjects.Length == 0 || !gameObjects.Any(HasTemporaryParticleSystem)) return; + + if (WarningButton("The temporary ParticleSystem for preview is attached.\n" + + "It will be removed when exiting edit mode.", "Remove")) + { + foreach (var go in gameObjects) + { + if (HasTemporaryParticleSystem(go)) + { + RemoveParticleSystem(go); + } + } + } + } + + public static void DrawWarningForPermanent(GameObject[] gameObjects) + { + if (gameObjects == null || gameObjects.Length == 0 || !gameObjects.Any(HasPermanentParticleSystem)) return; + + if (WarningButton("The permanent ParticleSystem is attached.\n" + + "It will be included in build.", "Remove")) + { + foreach (var go in gameObjects) + { + if (HasPermanentParticleSystem(go)) + { + RemoveParticleSystem(go); + Unregister(go); + Register(go); + } + } + } + } + + private static bool WarningButton(string message, string buttonText) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.HelpBox(message, MessageType.Warning, true); + var clicked = GUILayout.Button(EditorGUIUtility.TrTempContent(buttonText)); + EditorGUILayout.EndHorizontal(); + return clicked; + } + } +#endif +} diff --git a/Runtime/ParticleSystemPreviewer.cs.meta b/Runtime/ParticleSystemPreviewer.cs.meta new file mode 100644 index 0000000..db984de --- /dev/null +++ b/Runtime/ParticleSystemPreviewer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b171deb49fb7b471291108ad7e1c9baa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UIParticle.cs b/Runtime/UIParticle.cs index 0146f67..27a29a8 100644 --- a/Runtime/UIParticle.cs +++ b/Runtime/UIParticle.cs @@ -545,7 +545,10 @@ namespace Coffee.UIExtensions { var ps = particles[i]; if (!ps +#if UNITY_EDITOR + || (ps.hideFlags & HideFlags.DontSave) != 0 // Dummy ParticleSystems for preview. || ps.gameObject.CompareTag("EditorOnly") // Ignore "EditorOnly" tagged ParticleSystems. +#endif || ps.GetComponentInParent(true) != this) // Ignore ParticleSystems that are not under this UIParticle. { particles.RemoveAt(i); diff --git a/Runtime/UIParticleProjectSettings.cs b/Runtime/UIParticleProjectSettings.cs index ab295d6..3bd96c2 100644 --- a/Runtime/UIParticleProjectSettings.cs +++ b/Runtime/UIParticleProjectSettings.cs @@ -25,10 +25,16 @@ namespace Coffee.UIExtensions [SerializeField] private bool m_HideGeneratedObjects = true; - public static HideFlags globalHideFlags => instance.m_HideGeneratedObjects + [Tooltip("When selecting UIParticle, a temporary ParticleSystem is generated for preview.")] + [SerializeField] + private bool m_PreviewOnSelect = true; + + internal static HideFlags globalHideFlags => instance.m_HideGeneratedObjects ? HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInHierarchy | HideFlags.HideInInspector : HideFlags.DontSave | HideFlags.NotEditable; + internal static bool previewOnSelect => instance.m_PreviewOnSelect; + #if UNITY_EDITOR [SettingsProvider] private static SettingsProvider CreateSettingsProvider()