diff --git a/Packages/src/Scripts/Editor/UIParticleEditor.cs b/Packages/src/Scripts/Editor/UIParticleEditor.cs index 3ba75f0..366240e 100644 --- a/Packages/src/Scripts/Editor/UIParticleEditor.cs +++ b/Packages/src/Scripts/Editor/UIParticleEditor.cs @@ -1,3 +1,10 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEditorInternal; +using UnityEngine; +using UnityEngine.UI; #if UNITY_2021_2_OR_NEWER using UnityEditor.Overlays; #else @@ -5,26 +12,16 @@ #endif #if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; + #elif UNITY_2018_3_OR_NEWER using UnityEditor.Experimental.SceneManagement; #endif -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using Coffee.UIParticleExtensions; -using UnityEditor; -using UnityEditor.UI; -using UnityEditorInternal; -using UnityEngine; -using UnityEngine.UI; -using Object = UnityEngine.Object; namespace Coffee.UIExtensions { [CustomEditor(typeof(UIParticle))] [CanEditMultipleObjects] - internal class UIParticleEditor : GraphicEditor + internal class UIParticleEditor : Editor { #if UNITY_2021_2_OR_NEWER #if UNITY_2022_1_OR_NEWER @@ -144,10 +141,8 @@ SerializedObject CreateSerializeObject() /// /// This function is called when the object becomes enabled and active. /// - protected override void OnEnable() + private void OnEnable() { - base.OnEnable(); - _maskable = serializedObject.FindProperty("m_Maskable"); _scale3D = serializedObject.FindProperty("m_Scale3D"); _animatableProperties = serializedObject.FindProperty("m_AnimatableProperties"); @@ -525,9 +520,7 @@ private void DestroyUIParticle(UIParticle p, bool ignoreCurrent = false) { if (!p || (ignoreCurrent && target == p)) return; - var cr = p.canvasRenderer; DestroyImmediate(p); - DestroyImmediate(cr); #if UNITY_2018_3_OR_NEWER var stage = PrefabStageUtility.GetCurrentPrefabStage(); diff --git a/Packages/src/Scripts/ModifiedMaterial.cs b/Packages/src/Scripts/ModifiedMaterial.cs deleted file mode 100644 index ecc4e30..0000000 --- a/Packages/src/Scripts/ModifiedMaterial.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; - -namespace Coffee.UIParticleExtensions -{ - internal class ModifiedMaterial - { - private static readonly List s_Entries = new List(); - - public static Material Add(Material baseMat, Texture texture, int id) - { - MatEntry e; - for (var i = 0; i < s_Entries.Count; i++) - { - e = s_Entries[i]; - if (e.baseMat != baseMat || e.texture != texture || e.id != id) continue; - ++e.count; - return e.customMat; - } - - e = new MatEntry - { - count = 1, - baseMat = baseMat, - texture = texture, - id = id, - customMat = new Material(baseMat) - { - name = $"{baseMat.name}_{id}", - hideFlags = HideFlags.HideAndDontSave, - mainTexture = texture ? texture : null - } - }; - s_Entries.Add(e); - //Debug.LogFormat(">>>> ModifiedMaterial.Add -> count = count:{0}, mat:{1}, tex:{2}, id:{3}", s_Entries.Count, baseMat, texture, id); - return e.customMat; - } - - public static void Remove(Material customMat) - { - if (!customMat) return; - - for (var i = 0; i < s_Entries.Count; ++i) - { - var e = s_Entries[i]; - if (e.customMat != customMat) continue; - if (--e.count == 0) - { - //Debug.LogFormat(">>>> ModifiedMaterial.Remove -> count:{0}, mat:{1}, tex:{2}, id:{3}", s_Entries.Count - 1, e.customMat, e.texture, e.id); - Misc.DestroyImmediate(e.customMat); - e.customMat = null; - e.baseMat = null; - e.texture = null; - s_Entries.RemoveAt(i); - } - - break; - } - } - - private class MatEntry - { - public Material baseMat; - public int count; - public Material customMat; - public int id; - public Texture texture; - } - } -} diff --git a/Packages/src/Scripts/UIParticle.cs b/Packages/src/Scripts/UIParticle.cs index cc1a8ef..f28a07c 100644 --- a/Packages/src/Scripts/UIParticle.cs +++ b/Packages/src/Scripts/UIParticle.cs @@ -4,9 +4,9 @@ using Coffee.UIParticleExtensions; using UnityEditor; using UnityEngine; +using UnityEngine.EventSystems; using UnityEngine.Rendering; using UnityEngine.Serialization; -using UnityEngine.UI; using Random = UnityEngine.Random; [assembly: InternalsVisibleTo("Coffee.UIParticle.Editor")] @@ -19,7 +19,7 @@ namespace Coffee.UIExtensions [ExecuteAlways] [RequireComponent(typeof(RectTransform))] [RequireComponent(typeof(CanvasRenderer))] - public class UIParticle : MaskableGraphic, ISerializationCallbackReceiver + public class UIParticle : UIBehaviour, ISerializationCallbackReceiver { public enum AutoScalingMode { @@ -43,6 +43,9 @@ public enum PositionMode Absolute } + [SerializeField] + private bool m_Maskable = true; + [HideInInspector] [SerializeField] internal bool m_IsTrail; @@ -104,17 +107,43 @@ public enum PositionMode private bool m_ResetScaleOnEnable; private readonly List _renderers = new List(); + private Canvas _canvas; private int _groupId; private Camera _orthoCamera; private DrivenRectTransformTracker _tracker; + public RectTransform rectTransform => transform as RectTransform; + + public Canvas canvas + { + get + { + if (_canvas == null) + { + var tr = transform; + while (tr && !_canvas) + { + if (tr.TryGetComponent(out _canvas)) return _canvas; + tr = tr.parent; + } + } + + return _canvas; + } + } + /// - /// Should this graphic be considered a target for ray-casting? + /// Does this graphic allow masking. /// - public override bool raycastTarget + public bool maskable { - get => false; - set { } + get => m_Maskable; + set + { + if (value == m_Maskable) return; + m_Maskable = value; + UpdateRendererMaterial(); + } } /// @@ -269,8 +298,6 @@ public IEnumerable materials } } - public override Material materialForRendering => null; - /// /// Paused. /// @@ -285,7 +312,6 @@ protected override void OnEnable() ResetGroupId(); UpdateTracker(); UIParticleUpdater.Register(this); - RegisterDirtyMaterialCallback(UpdateRendererMaterial); if (0 < particles.Count) { @@ -296,7 +322,7 @@ protected override void OnEnable() RefreshParticles(); } - base.OnEnable(); + UpdateRendererMaterial(); // Reset scale for upgrade. if (m_ResetScaleOnEnable) @@ -314,9 +340,12 @@ protected override void OnDisable() UpdateTracker(); UIParticleUpdater.Unregister(this); _renderers.ForEach(r => r.Reset()); - UnregisterDirtyMaterialCallback(UpdateRendererMaterial); + _canvas = null; + } - base.OnDisable(); + protected override void OnCanvasHierarchyChanged() + { + _canvas = null; } /// @@ -326,11 +355,17 @@ protected override void OnDidApplyAnimationProperties() { } + protected override void OnTransformParentChanged() + { + _canvas = null; + } + #if UNITY_EDITOR protected override void OnValidate() { base.OnValidate(); UpdateTracker(); + UpdateRendererMaterial(); } #endif @@ -469,7 +504,7 @@ private void RefreshParticles(GameObject root) RefreshParticles(particles); } - public void RefreshParticles(List particles) + public void RefreshParticles(List particleSystems) { // #246: Nullptr exceptions when using nested UIParticle components in hierarchy _renderers.Clear(); @@ -489,9 +524,9 @@ public void RefreshParticles(List particles) } var j = 0; - for (var i = 0; i < particles.Count; i++) + for (var i = 0; i < particleSystems.Count; i++) { - var ps = particles[i]; + var ps = particleSystems[i]; if (!ps) continue; GetRenderer(j++).Set(this, ps, false); if (ps.trails.enabled) @@ -521,11 +556,10 @@ internal void UpdateRenderers() for (var i = 0; i < _renderers.Count; i++) { var r = _renderers[i]; - if (!r) - { - RefreshParticles(particles); - break; - } + if (r) continue; + + RefreshParticles(particles); + break; } var bakeCamera = GetBakeCamera(); @@ -533,6 +567,7 @@ internal void UpdateRenderers() { var r = _renderers[i]; if (!r) continue; + r.UpdateMesh(bakeCamera); } } @@ -554,17 +589,6 @@ internal void ResetGroupId() : Random.Range(m_GroupId, m_GroupMaxId + 1); } - protected override void UpdateMaterial() - { - } - - /// - /// Call to update the geometry of the Graphic onto the CanvasRenderer. - /// - protected override void UpdateGeometry() - { - } - private void UpdateRendererMaterial() { for (var i = 0; i < _renderers.Count; i++) @@ -628,8 +652,7 @@ private Camera GetBakeCamera() } // - var size = ((RectTransform)root.transform).rect.size; - _orthoCamera.orthographicSize = Mathf.Max(size.x, size.y) * root.scaleFactor; + _orthoCamera.orthographicSize = 10; _orthoCamera.transform.SetPositionAndRotation(new Vector3(0, 0, -1000), Quaternion.identity); _orthoCamera.orthographic = true; _orthoCamera.farClipPlane = 2000f; @@ -639,7 +662,7 @@ private Camera GetBakeCamera() private void UpdateTracker() { - if (!enabled || !autoScaling || autoScalingMode != AutoScalingMode.Transform) + if (!enabled || autoScalingMode != AutoScalingMode.Transform) { _tracker.Clear(); } diff --git a/Packages/src/Scripts/UIParticleRenderer.cs b/Packages/src/Scripts/UIParticleRenderer.cs index 63edfea..33458fe 100644 --- a/Packages/src/Scripts/UIParticleRenderer.cs +++ b/Packages/src/Scripts/UIParticleRenderer.cs @@ -20,7 +20,7 @@ internal class UIParticleRenderer : MaskableGraphic private static readonly List s_Renderers = new List(); private static readonly List s_Colors = new List(); private static readonly Vector3[] s_Corners = new Vector3[4]; - private Material _currentMaterialForRendering; + private Material _materialForRendering; private bool _delay; private int _index; private bool _isTrail; @@ -110,9 +110,8 @@ public void Reset(int index = -1) } else { - ModifiedMaterial.Remove(_modifiedMaterial); - _modifiedMaterial = null; - _currentMaterialForRendering = null; + MaterialRegistry.Release(ref _modifiedMaterial); + _materialForRendering = null; } } @@ -128,17 +127,14 @@ protected override void OnEnable() hideFlags = HideFlags.HideAndDontSave }; } - - _currentMaterialForRendering = null; } protected override void OnDisable() { base.OnDisable(); - ModifiedMaterial.Remove(_modifiedMaterial); - _modifiedMaterial = null; - _currentMaterialForRendering = null; + MaterialRegistry.Release(ref _modifiedMaterial); + _materialForRendering = null; } public static UIParticleRenderer AddRenderer(UIParticle parent, int index) @@ -170,12 +166,9 @@ public static UIParticleRenderer AddRenderer(UIParticle parent, int index) /// public override Material GetModifiedMaterial(Material baseMaterial) { - _currentMaterialForRendering = null; - if (!IsActive() || !_parent) { - ModifiedMaterial.Remove(_modifiedMaterial); - _modifiedMaterial = null; + MaterialRegistry.Release(ref _modifiedMaterial); return baseMaterial; } @@ -185,18 +178,26 @@ public override Material GetModifiedMaterial(Material baseMaterial) var texture = mainTexture; if (texture == null && _parent.m_AnimatableProperties.Length == 0) { - ModifiedMaterial.Remove(_modifiedMaterial); - _modifiedMaterial = null; + MaterialRegistry.Release(ref _modifiedMaterial); return modifiedMaterial; } // - var id = _parent.m_AnimatableProperties.Length == 0 ? 0 : GetInstanceID(); - modifiedMaterial = ModifiedMaterial.Add(modifiedMaterial, texture, id); - ModifiedMaterial.Remove(_modifiedMaterial); - _modifiedMaterial = modifiedMaterial; + var hash = new Hash128( + modifiedMaterial ? (uint)modifiedMaterial.GetInstanceID() : 0, + texture ? (uint)texture.GetInstanceID() : 0, + 0 < _parent.m_AnimatableProperties.Length ? (uint)GetInstanceID() : 0, + 0 + ); + if (!MaterialRegistry.Valid(hash, _modifiedMaterial)) + { + MaterialRegistry.Get(hash, ref _modifiedMaterial, () => new Material(modifiedMaterial) + { + hideFlags = HideFlags.HideAndDontSave, + }); + } - return modifiedMaterial; + return _modifiedMaterial; } public void Set(UIParticle parent, ParticleSystem ps, bool isTrail) @@ -259,8 +260,7 @@ public void UpdateMesh(Camera bakeCamera) || (!_particleSystem.IsAlive() && !_particleSystem.isPlaying) // No particle. || (_isTrail && !_particleSystem.trails.enabled) // Trail, but it is not enabled. #if UNITY_2018_3_OR_NEWER - || canvasRenderer.GetInheritedAlpha() < - 0.01f // #102: Do not bake particle system to mesh when the alpha is zero. + || canvasRenderer.GetInheritedAlpha() < 0.01f // #102: Do not bake particle system to mesh when the alpha is zero. #endif ) { @@ -402,57 +402,77 @@ public void UpdateMesh(Camera bakeCamera) workerMesh.SetColors(s_Colors); Profiler.EndSample(); } + + var components = ListPool.Rent(); + GetComponents(typeof(IMeshModifier), components); + +#pragma warning disable CS0618 // Type or member is obsolete + for (var i = 0; i < components.Count; i++) + { + ((IMeshModifier)components[i]).ModifyMesh(workerMesh); + } +#pragma warning restore CS0618 // Type or member is obsolete + + ListPool.Return(ref components); } Profiler.EndSample(); + // Update animatable material properties. + Profiler.BeginSample("[UIParticleRenderer] Update Animatable Material Properties"); + UpdateMaterialProperties(); + Profiler.EndSample(); // Get grouped renderers. + Profiler.BeginSample("[UIParticleRenderer] Set Mesh"); s_Renderers.Clear(); if (_parent.useMeshSharing) { UIParticleUpdater.GetGroupedRenderers(_parent.groupId, _index, s_Renderers); } - // Set mesh to the CanvasRenderer. - Profiler.BeginSample("[UIParticleRenderer] Set Mesh"); for (var i = 0; i < s_Renderers.Count; i++) { if (s_Renderers[i] == this) continue; + s_Renderers[i].canvasRenderer.SetMesh(workerMesh); s_Renderers[i]._lastBounds = _lastBounds; + s_Renderers[i].canvasRenderer.materialCount = 1; + s_Renderers[i].canvasRenderer.SetMaterial(materialForRendering, 0); } - if (!_parent.canRender) + if (_parent.canRender) + { + canvasRenderer.SetMesh(workerMesh); + } + else { workerMesh.Clear(); + canvasRenderer.SetMesh(workerMesh); } - canvasRenderer.SetMesh(workerMesh); Profiler.EndSample(); - // Update animatable material properties. - Profiler.BeginSample("[UIParticleRenderer] Update Animatable Material Properties"); - UpdateMaterialProperties(); - if (!_parent.useMeshSharing) + s_Renderers.Clear(); + } + + public override Material materialForRendering + { + get { - if (!_currentMaterialForRendering) + if (!_materialForRendering) { - _currentMaterialForRendering = materialForRendering; + _materialForRendering = base.materialForRendering; } - for (var i = 0; i < s_Renderers.Count; i++) - { - if (s_Renderers[i] == this) continue; - - s_Renderers[i].canvasRenderer.materialCount = 1; - s_Renderers[i].canvasRenderer.SetMaterial(_currentMaterialForRendering, 0); - } + return _materialForRendering; } + } - Profiler.EndSample(); - - s_Renderers.Clear(); + public override void SetMaterialDirty() + { + _materialForRendering = null; + base.SetMaterialDirty(); } internal void UpdateParticleCount() @@ -673,12 +693,12 @@ private void UpdateMaterialProperties() if (s_Mpb.isEmpty) return; // #41: Copy the value from MaterialPropertyBlock to CanvasRenderer - if (!_modifiedMaterial) return; + if (!materialForRendering) return; for (var i = 0; i < _parent.m_AnimatableProperties.Length; i++) { var ap = _parent.m_AnimatableProperties[i]; - ap.UpdateMaterialProperties(_modifiedMaterial, s_Mpb); + ap.UpdateMaterialProperties(materialForRendering, s_Mpb); } s_Mpb.Clear(); diff --git a/Packages/src/Scripts/UIParticleUpdater.cs b/Packages/src/Scripts/UIParticleUpdater.cs index a069cf0..c0d1b6f 100644 --- a/Packages/src/Scripts/UIParticleUpdater.cs +++ b/Packages/src/Scripts/UIParticleUpdater.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Coffee.UIParticleExtensions; using UnityEditor; using UnityEngine; @@ -39,12 +40,12 @@ public static void Unregister(UIParticleAttractor attractor) #if UNITY_EDITOR [InitializeOnLoadMethod] +#else + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] #endif - [RuntimeInitializeOnLoadMethod] private static void InitializeOnLoad() { - Canvas.willRenderCanvases -= Refresh; - Canvas.willRenderCanvases += Refresh; + UIExtraCallbacks.onAfterCanvasRebuild += Refresh; } private static void Refresh() diff --git a/Packages/src/Scripts/Utilities.meta b/Packages/src/Scripts/Utilities.meta new file mode 100644 index 0000000..021a7e8 --- /dev/null +++ b/Packages/src/Scripts/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee9c46d98608d4236a072404521519d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Scripts/Utils.cs b/Packages/src/Scripts/Utilities/Extensions.cs similarity index 64% rename from Packages/src/Scripts/Utils.cs rename to Packages/src/Scripts/Utilities/Extensions.cs index 6700333..3e739cb 100644 --- a/Packages/src/Scripts/Utils.cs +++ b/Packages/src/Scripts/Utilities/Extensions.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Reflection; using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.UI; using Object = UnityEngine.Object; namespace Coffee.UIParticleExtensions { - public static class Color32Extensions + internal static class Color32Extensions { private static byte[] s_LinearToGammaLut; @@ -25,7 +27,7 @@ public static byte LinearToGamma(this byte self) } } - public static class Vector3Extensions + internal static class Vector3Extensions { public static Vector3 Inverse(this Vector3 self) { @@ -83,14 +85,14 @@ public static Texture2D GetActualTexture(this Sprite self) : self.texture; } #else - internal static Texture2D GetActualTexture(this Sprite self) + public static Texture2D GetActualTexture(this Sprite self) { return self ? self.texture : null; } #endif } - public static class ParticleSystemExtensions + internal static class ParticleSystemExtensions { private static ParticleSystem.Particle[] s_TmpParticles = new ParticleSystem.Particle[2048]; @@ -227,8 +229,11 @@ public static Texture2D GetTextureForSprite(this ParticleSystem self) public static void Exec(this List self, Action action) { - self.RemoveAll(p => !p); - self.ForEach(action); + foreach (var p in self) + { + if (!p) continue; + action.Invoke(p); + } } } @@ -282,4 +287,142 @@ public static T GetComponentInParent(this Component self, bool includeInactiv } #endif } + + internal static class ListExtensions + { + public static void RemoveAtFast(this List self, int index) + { + if (self == null) return; + + var lastIndex = self.Count - 1; + self[index] = self[lastIndex]; + self.RemoveAt(lastIndex); + } + } + + internal static class MeshExtensions + { + internal static readonly ObjectPool s_MeshPool = new ObjectPool( + () => + { + var mesh = new Mesh + { + hideFlags = HideFlags.DontSave | HideFlags.NotEditable + }; + mesh.MarkDynamic(); + return mesh; + }, + mesh => mesh, + mesh => + { + if (mesh) + { + mesh.Clear(); + } + }); + + public static Mesh Rent() + { + return s_MeshPool.Rent(); + } + + public static void Return(ref Mesh mesh) + { + s_MeshPool.Return(ref mesh); + } + } + + // internal static class Vector3Extensions + // { + // public static Vector3 Inverse(this Vector3 self) + // { + // self.x = Mathf.Approximately(self.x, 0) ? 1 : 1 / self.x; + // self.y = Mathf.Approximately(self.y, 0) ? 1 : 1 / self.y; + // self.z = Mathf.Approximately(self.z, 0) ? 1 : 1 / self.z; + // return self; + // } + // + // public static Vector3 GetScaled(this Vector3 self, Vector3 other1) + // { + // self.Scale(other1); + // return self; + // } + // + // public static Vector3 GetScaled(this Vector3 self, Vector3 other1, Vector3 other2) + // { + // self.Scale(other1); + // self.Scale(other2); + // return self; + // } + // + // public static bool IsVisible(this Vector3 self) + // { + // return 0 < Mathf.Abs(self.x * self.y); + // } + // } + + /// + /// Extension methods for Graphic class. + /// + internal static class GraphicExtensions + { + private static readonly Vector3[] s_WorldCorners = new Vector3[4]; + private static readonly Bounds s_ScreenBounds = new Bounds(new Vector3(0.5f, 0.5f, 0.5f), new Vector3(1, 1, 1)); + + /// + /// Check if a Graphic component is currently in the screen view. + /// + public static bool IsInScreen(this Graphic self) + { + if (!self || !self.canvas) return false; + + if (FrameCache.TryGet(self, nameof(IsInScreen), out bool result)) + { + return result; + } + + Profiler.BeginSample("(CCR)[GraphicExtensions] InScreen"); + var cam = self.canvas.renderMode != RenderMode.ScreenSpaceOverlay + ? self.canvas.worldCamera + : null; + var min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); + var max = new Vector3(float.MinValue, float.MinValue, float.MinValue); + self.rectTransform.GetWorldCorners(s_WorldCorners); + + for (var i = 0; i < 4; i++) + { + if (cam) + { + s_WorldCorners[i] = cam.WorldToViewportPoint(s_WorldCorners[i]); + } + else + { + s_WorldCorners[i] = RectTransformUtility.WorldToScreenPoint(null, s_WorldCorners[i]); + s_WorldCorners[i].x /= Screen.width; + s_WorldCorners[i].y /= Screen.height; + } + + s_WorldCorners[i].z = 0; + min = Vector3.Min(s_WorldCorners[i], min); + max = Vector3.Max(s_WorldCorners[i], max); + } + + var bounds = new Bounds(min, Vector3.zero); + bounds.Encapsulate(max); + result = bounds.Intersects(s_ScreenBounds); + FrameCache.Set(self, nameof(IsInScreen), result); + Profiler.EndSample(); + + return result; + } + + public static float GetParentGroupAlpha(this Graphic self) + { + var alpha = self.canvasRenderer.GetAlpha(); + if (Mathf.Approximately(alpha, 0)) return 1; + + var inheritedAlpha = self.canvasRenderer.GetInheritedAlpha(); + return Mathf.Clamp01(inheritedAlpha / alpha); + } + } } diff --git a/Packages/src/Scripts/ModifiedMaterial.cs.meta b/Packages/src/Scripts/Utilities/Extensions.cs.meta similarity index 83% rename from Packages/src/Scripts/ModifiedMaterial.cs.meta rename to Packages/src/Scripts/Utilities/Extensions.cs.meta index 83251d7..2c12456 100644 --- a/Packages/src/Scripts/ModifiedMaterial.cs.meta +++ b/Packages/src/Scripts/Utilities/Extensions.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b0beae5bb1cb142b9ab90dc0d371f026 +guid: 782909d0d91f94681b1201c40c3f38c1 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Scripts/Utilities/FastAction.cs b/Packages/src/Scripts/Utilities/FastAction.cs new file mode 100755 index 0000000..908388e --- /dev/null +++ b/Packages/src/Scripts/Utilities/FastAction.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Profiling; + +namespace Coffee.UIParticleExtensions +{ + /// + /// Base class for a fast action. + /// + internal class FastActionBase + { + private static readonly ObjectPool> s_NodePool = + new ObjectPool>(() => new LinkedListNode(default), _ => true, x => x.Value = default); + + private readonly LinkedList _delegates = new LinkedList(); + + /// + /// Adds a delegate to the action. + /// + public void Add(T rhs) + { + Profiler.BeginSample("(CCR)[FastAction] Add Action"); + var node = s_NodePool.Rent(); + node.Value = rhs; + _delegates.AddLast(node); + Profiler.EndSample(); + } + + /// + /// Removes a delegate from the action. + /// + public void Remove(T rhs) + { + Profiler.BeginSample("(CCR)[FastAction] Remove Action"); + var node = _delegates.Find(rhs); + if (node != null) + { + _delegates.Remove(node); + s_NodePool.Return(ref node); + } + + Profiler.EndSample(); + } + + /// + /// Invokes the action with a callback function. + /// + protected void Invoke(Action callback) + { + var node = _delegates.First; + while (node != null) + { + try + { + callback(node.Value); + } + catch (Exception e) + { + Debug.LogException(e); + } + + node = node.Next; + } + } + } + + /// + /// A fast action without parameters. + /// + internal class FastAction : FastActionBase + { + /// + /// Invoke all the registered delegates. + /// + public void Invoke() + { + Invoke(action => action.Invoke()); + } + } +} diff --git a/Packages/src/Scripts/Utils.cs.meta b/Packages/src/Scripts/Utilities/FastAction.cs.meta similarity index 83% rename from Packages/src/Scripts/Utils.cs.meta rename to Packages/src/Scripts/Utilities/FastAction.cs.meta index 5a68a0f..41a8b5f 100644 --- a/Packages/src/Scripts/Utils.cs.meta +++ b/Packages/src/Scripts/Utilities/FastAction.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: d188d31b140094ebc84a9caafbc7ac71 +guid: 5e733065dbda24076812072e73499fce MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Packages/src/Scripts/Utilities/FrameCache.cs b/Packages/src/Scripts/Utilities/FrameCache.cs new file mode 100644 index 0000000..8a472b5 --- /dev/null +++ b/Packages/src/Scripts/Utilities/FrameCache.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; + +namespace Coffee.UIParticleExtensions +{ + internal static class FrameCache + { + private static readonly Dictionary s_Caches = new Dictionary(); + + static FrameCache() + { + s_Caches.Clear(); + UIExtraCallbacks.onLateAfterCanvasRebuild += ClearAllCache; + } + + /// + /// Tries to retrieve a value from the frame cache with a specified key. + /// + public static bool TryGet(object key1, string key2, out T result) + { + return GetFrameCache().TryGet((key1.GetHashCode(), key2.GetHashCode()), out result); + } + + /// + /// Tries to retrieve a value from the frame cache with a specified key. + /// + public static bool TryGet(object key1, string key2, int key3, out T result) + { + return GetFrameCache().TryGet((key1.GetHashCode(), key2.GetHashCode() + key3), out result); + } + + /// + /// Sets a value in the frame cache with a specified key. + /// + public static void Set(object key1, string key2, T result) + { + GetFrameCache().Set((key1.GetHashCode(), key2.GetHashCode()), result); + } + + + /// + /// Sets a value in the frame cache with a specified key. + /// + public static void Set(object key1, string key2, int key3, T result) + { + GetFrameCache().Set((key1.GetHashCode(), key2.GetHashCode() + key3), result); + } + + private static void ClearAllCache() + { + foreach (var cache in s_Caches.Values) + { + cache.Clear(); + } + } + + private static FrameCacheContainer GetFrameCache() + { + var t = typeof(T); + if (s_Caches.TryGetValue(t, out var frameCache)) return frameCache as FrameCacheContainer; + + frameCache = new FrameCacheContainer(); + s_Caches.Add(t, frameCache); + + return (FrameCacheContainer)frameCache; + } + + private interface IFrameCache + { + void Clear(); + } + + private class FrameCacheContainer : IFrameCache + { + private readonly Dictionary<(int, int), T> _caches = new Dictionary<(int, int), T>(); + + public void Clear() + { + _caches.Clear(); + } + + public bool TryGet((int, int) key, out T result) + { + return _caches.TryGetValue(key, out result); + } + + public void Set((int, int) key, T result) + { + _caches[key] = result; + } + } + } +} diff --git a/Packages/src/Scripts/Utilities/FrameCache.cs.meta b/Packages/src/Scripts/Utilities/FrameCache.cs.meta new file mode 100644 index 0000000..7b9bf1b --- /dev/null +++ b/Packages/src/Scripts/Utilities/FrameCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 62274b67742f8490dac601059905c489 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Scripts/Utilities/Logging.cs b/Packages/src/Scripts/Utilities/Logging.cs new file mode 100644 index 0000000..9a758e0 --- /dev/null +++ b/Packages/src/Scripts/Utilities/Logging.cs @@ -0,0 +1,230 @@ +using System; +using System.Text; +using UnityEngine; +using Conditional = System.Diagnostics.ConditionalAttribute; +using Object = UnityEngine.Object; +#if UIP_LOG +using System.Reflection; +using System.Collections.Generic; +#endif + +namespace Coffee.UIParticleExtensions +{ + internal static class Logging + { + private const string k_EnableSymbol = "UIP_LOG"; + + [Conditional(k_EnableSymbol)] + private static void Log_Internal(LogType type, object tag, object message, Object context) + { +#if UIP_LOG + AppendTag(s_Sb, tag); + s_Sb.Append(message); + switch (type) + { + case LogType.Error: + case LogType.Assert: + case LogType.Exception: + Debug.LogError(s_Sb, context); + break; + case LogType.Warning: + Debug.LogWarning(s_Sb, context); + break; + case LogType.Log: + Debug.Log(s_Sb, context); + break; + } + + s_Sb.Length = 0; +#endif + } + + [Conditional(k_EnableSymbol)] + public static void LogIf(bool enable, object tag, object message, Object context = null) + { + if (!enable) return; + Log_Internal(LogType.Log, tag, message, context ? context : tag as Object); + } + + [Conditional(k_EnableSymbol)] + public static void Log(object tag, object message, Object context = null) + { + Log_Internal(LogType.Log, tag, message, context ? context : tag as Object); + } + + [Conditional(k_EnableSymbol)] + public static void LogWarning(object tag, object message, Object context = null) + { + Log_Internal(LogType.Warning, tag, message, context ? context : tag as Object); + } + + public static void LogError(object tag, object message, Object context = null) + { +#if UIP_LOG + Log_Internal(LogType.Error, tag, message, context ? context : tag as Object); +#else + Debug.LogError($"{tag}: {message}", context); +#endif + } + + [Conditional(k_EnableSymbol)] + public static void LogMulticast(Type type, string fieldName, object instance = null, string message = null) + { +#if UIP_LOG + AppendTag(s_Sb, instance ?? type); + + var handler = type + .GetField(fieldName, + BindingFlags.Static | BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic) + ?.GetValue(instance); + + var list = ((MulticastDelegate)handler)?.GetInvocationList() ?? Array.Empty(); + s_Sb.Append(""); + s_Sb.Append(type.Name); + s_Sb.Append("."); + s_Sb.Append(fieldName); + s_Sb.Append(" has "); + s_Sb.Append(list.Length); + s_Sb.Append(" callbacks"); + if (message != null) + { + s_Sb.Append(" ("); + s_Sb.Append(message); + s_Sb.Append(")"); + } + + s_Sb.Append(":"); + + for (var i = 0; i < list.Length; i++) + { + s_Sb.Append("\n - "); + s_Sb.Append(list[i].Method.DeclaringType?.Name); + s_Sb.Append("."); + s_Sb.Append(list[i].Method.Name); + } + + Debug.Log(s_Sb); + s_Sb.Length = 0; +#endif + } + + [Conditional(k_EnableSymbol)] + private static void AppendTag(StringBuilder sb, object tag) + { +#if UIP_LOG + try + { + sb.Append("f"); + sb.Append(Time.frameCount); + sb.Append(":["); + + switch (tag) + { + case Type type: + AppendType(sb, type); + break; + case Object uObject: + AppendType(sb, tag.GetType()); + sb.Append(" #"); + sb.Append(uObject.name); + break; + default: + AppendType(sb, tag.GetType()); + break; + } + + sb.Append("] "); + } + catch + { + sb.Append("f"); + sb.Append(Time.frameCount); + sb.Append(":["); + sb.Append(tag); + sb.Append("] "); + } +#endif + } + + [Conditional(k_EnableSymbol)] + private static void AppendType(StringBuilder sb, Type type) + { +#if UIP_LOG + if (s_TypeNameCache.TryGetValue(type, out var name)) + { + sb.Append(name); + return; + } + + // New type found + var start = sb.Length; + sb.Append(type.Name); + if (type.IsGenericType) + { + sb.Length -= 2; + sb.Append("<"); + foreach (var gType in type.GetGenericArguments()) + { + AppendType(sb, gType); + sb.Append(", "); + } + + sb.Length -= 2; + sb.Append(">"); + } + + s_TypeNameCache.Add(type, sb.ToString(start, sb.Length - start)); +#endif + } + + + [Conditional(k_EnableSymbol)] + private static void AppendReadableCode(StringBuilder sb, object tag) + { +#if UIP_LOG + int hash; + try + { + switch (tag) + { + case string text: + hash = text.GetHashCode(); + break; + case Type type: + type = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + hash = type.FullName?.GetHashCode() ?? 0; + break; + default: + hash = tag.GetType().FullName?.GetHashCode() ?? 0; + break; + } + } + catch + { + sb.Append("FFFFFF"); + return; + } + + hash = hash & (s_Codes.Length - 1); + if (s_Codes[hash] == null) + { + var hue = hash / (float)s_Codes.Length; + var modifier = 1f - Mathf.Clamp01(Mathf.Abs(hue - 0.65f) / 0.2f); + var saturation = 0.8f + modifier * -0.2f; + var value = 0.7f + modifier * 0.3f; + s_Codes[hash] = ColorUtility.ToHtmlStringRGB(Color.HSVToRGB(hue, saturation, value)); + } + + sb.Append(s_Codes[hash]); +#endif + } + +#if UIP_LOG + private static readonly StringBuilder s_Sb = new StringBuilder(); + private static readonly string[] s_Codes = new string[32]; + private static readonly Dictionary s_TypeNameCache = new Dictionary(); +#endif + } +} diff --git a/Packages/src/Scripts/Utilities/Logging.cs.meta b/Packages/src/Scripts/Utilities/Logging.cs.meta new file mode 100644 index 0000000..113901c --- /dev/null +++ b/Packages/src/Scripts/Utilities/Logging.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f23550dfa4cc847c3beba4e4cdbc53dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Scripts/Utilities/MaterialRegistry.cs b/Packages/src/Scripts/Utilities/MaterialRegistry.cs new file mode 100644 index 0000000..e2fae36 --- /dev/null +++ b/Packages/src/Scripts/Utilities/MaterialRegistry.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Profiling; +using Object = UnityEngine.Object; + +namespace Coffee.UIParticleExtensions +{ + /// + /// Provides functionality to manage materials. + /// + internal static class MaterialRegistry + { + private static readonly ObjectPool s_MatEntryPool = + new ObjectPool(() => new MatEntry(), _ => true, ent => ent.Release()); + + private static readonly List s_List = new List(); + public static int activeMaterialCount => s_List.Count; + +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Clear() + { + foreach (var ent in s_List) + { + ent.Release(); + } + + s_List.Clear(); + } +#endif + + public static bool Valid(Hash128 hash, Material material) + { + // Find existing entry. + Profiler.BeginSample("(CCR)[MaterialRegistry] Valid > Find existing entry"); + for (var i = 0; i < s_List.Count; ++i) + { + var ent = s_List[i]; + if (ent.hash != hash) continue; + Profiler.EndSample(); + + // Existing entry found. + return ent.customMat == material; + } + Profiler.EndSample(); + + return false; + } + + /// + /// Adds or retrieves a cached material based on the hash. + /// + public static void Get(Hash128 hash, ref Material material, Func onCreate) + { + // Find existing entry. + Profiler.BeginSample("(CCR)[MaterialRegistry] Get > Find existing entry"); + for (var i = 0; i < s_List.Count; ++i) + { + var ent = s_List[i]; + if (ent.hash != hash) continue; + + // Existing entry found. + if (ent.customMat != material) + { + // if the material is different, release the old one. + Release(ref material); + ++ent.count; + material = ent.customMat; + Logging.Log(typeof(MaterialRegistry), + $"Get(#{s_List.Count}): {ent.hash.GetHashCode()} (#{ent.count}), {ent.customMat.shader}"); + } + + Profiler.EndSample(); + return; + } + + Profiler.EndSample(); + + // Create new entry. + Profiler.BeginSample("(CCR)[MaterialRegistry] Get > Create new entry"); + var entry = s_MatEntryPool.Rent(); + entry.customMat = onCreate(); + entry.hash = hash; + entry.count = 1; + s_List.Add(entry); + Logging.Log(typeof(MaterialRegistry), + $"Get(#{s_List.Count}): {entry.hash.GetHashCode()}, {entry.customMat.shader}"); + + Release(ref material); + material = entry.customMat; + Profiler.EndSample(); + } + + /// + /// Removes a soft mask material from the cache. + /// + public static void Release(ref Material customMat) + { + if (customMat == null) return; + + Profiler.BeginSample("(CCR)[MaterialRegistry] Release"); + for (var i = 0; i < s_List.Count; i++) + { + var ent = s_List[i]; + + if (ent.customMat != customMat) + { + continue; + } + + if (--ent.count <= 0) + { + Profiler.BeginSample("(CCR)[MaterialRegistry] Release > RemoveAt"); + Logging.Log(typeof(MaterialRegistry), + $"Release(#{s_List.Count - 1}): {ent.hash.GetHashCode()}, {ent.customMat.shader}"); + s_List.RemoveAtFast(i); + s_MatEntryPool.Return(ref ent); + Profiler.EndSample(); + } + else + { + Logging.Log(typeof(MaterialRegistry), + $"Release(#{s_List.Count}): {ent.hash.GetHashCode()} (#{ent.count}), {ent.customMat.shader}"); + } + + customMat = null; + break; + } + + Profiler.EndSample(); + } + + private class MatEntry + { + public int count; + public Material customMat; + public Hash128 hash; + + public void Release() + { + count = 0; + if (customMat) + { +#if UNITY_EDITOR + if (!Application.isPlaying) + { + Object.DestroyImmediate(customMat, false); + } + else +#endif + { + Object.Destroy(customMat); + } + } + + customMat = null; + } + } + } +} diff --git a/Packages/src/Scripts/Utilities/MaterialRegistry.cs.meta b/Packages/src/Scripts/Utilities/MaterialRegistry.cs.meta new file mode 100644 index 0000000..f12e765 --- /dev/null +++ b/Packages/src/Scripts/Utilities/MaterialRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d4443d3cac624a67a7656c323918823 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Scripts/Utilities/ObjectPool.cs b/Packages/src/Scripts/Utilities/ObjectPool.cs new file mode 100644 index 0000000..b3f50b0 --- /dev/null +++ b/Packages/src/Scripts/Utilities/ObjectPool.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace Coffee.UIParticleExtensions +{ + /// + /// Object pool. + /// + internal class ObjectPool + { + private readonly Func _onCreate; // Delegate for creating instances + private readonly Action _onReturn; // Delegate for returning instances to the pool + private readonly Predicate _onValid; // Delegate for checking if instances are valid + private readonly Stack _pool = new Stack(32); // Object pool + private int _count; // Total count of created instances + + public ObjectPool(Func onCreate, Predicate onValid, Action onReturn) + { + _onCreate = onCreate; + _onValid = onValid; + _onReturn = onReturn; + } + + /// + /// Rent an instance from the pool. + /// When you no longer need it, return it with . + /// + public T Rent() + { + while (0 < _pool.Count) + { + var instance = _pool.Pop(); + if (_onValid(instance)) + { + return instance; + } + } + + // If there are no instances in the pool, create a new one. + Logging.Log(this, $"A new instance is created (pooled: {_pool.Count}, created: {++_count})."); + return _onCreate(); + } + + /// + /// Return an instance to the pool and assign null. + /// Be sure to return the instance obtained with with this method. + /// + public void Return(ref T instance) + { + if (instance == null || _pool.Contains(instance)) return; // Ignore if already pooled or null. + + _onReturn(instance); // Return the instance to the pool. + _pool.Push(instance); + Logging.Log(this, $"An instance is released (pooled: {_pool.Count}, created: {_count})."); + instance = default; // Set the reference to null. + } + } + + /// + /// Object pool for . + /// + internal static class ListPool + { + private static readonly ObjectPool> s_ListPool = + new ObjectPool>(() => new List(), _ => true, x => x.Clear()); + + /// + /// Rent an instance from the pool. + /// When you no longer need it, return it with . + /// + public static List Rent() + { + return s_ListPool.Rent(); + } + + /// + /// Return an instance to the pool and assign null. + /// Be sure to return the instance obtained with with this method. + /// + public static void Return(ref List toRelease) + { + s_ListPool.Return(ref toRelease); + } + } +} diff --git a/Packages/src/Scripts/Utilities/ObjectPool.cs.meta b/Packages/src/Scripts/Utilities/ObjectPool.cs.meta new file mode 100644 index 0000000..d2f5e47 --- /dev/null +++ b/Packages/src/Scripts/Utilities/ObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 810cf71c0724c47ff93b4e04b741fbfb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Scripts/Utilities/UIExtraCallbacks.cs b/Packages/src/Scripts/Utilities/UIExtraCallbacks.cs new file mode 100755 index 0000000..4b8f73e --- /dev/null +++ b/Packages/src/Scripts/Utilities/UIExtraCallbacks.cs @@ -0,0 +1,92 @@ +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +namespace Coffee.UIParticleExtensions +{ + /// + /// Provides additional callbacks related to canvas and UI system. + /// + internal static class UIExtraCallbacks + { + private static bool s_IsInitializedAfterCanvasRebuild; + private static readonly FastAction s_AfterCanvasRebuildAction = new FastAction(); + private static readonly FastAction s_LateAfterCanvasRebuildAction = new FastAction(); + private static readonly FastAction s_BeforeCanvasRebuildAction = new FastAction(); + + static UIExtraCallbacks() + { + Canvas.willRenderCanvases += OnBeforeCanvasRebuild; + Logging.LogMulticast(typeof(Canvas), "willRenderCanvases", message: "ctor"); + } + + /// + /// Event that occurs after canvas rebuilds. + /// + public static event Action onLateAfterCanvasRebuild + { + add => s_LateAfterCanvasRebuildAction.Add(value); + remove => s_LateAfterCanvasRebuildAction.Remove(value); + } + + /// + /// Event that occurs before canvas rebuilds. + /// + public static event Action onBeforeCanvasRebuild + { + add => s_BeforeCanvasRebuildAction.Add(value); + remove => s_BeforeCanvasRebuildAction.Remove(value); + } + + /// + /// Event that occurs after canvas rebuilds. + /// + public static event Action onAfterCanvasRebuild + { + add => s_AfterCanvasRebuildAction.Add(value); + remove => s_AfterCanvasRebuildAction.Remove(value); + } + + /// + /// Initializes the UIExtraCallbacks to ensure proper event handling. + /// + private static void InitializeAfterCanvasRebuild() + { + if (s_IsInitializedAfterCanvasRebuild) return; + s_IsInitializedAfterCanvasRebuild = true; + + CanvasUpdateRegistry.IsRebuildingLayout(); + Canvas.willRenderCanvases += OnAfterCanvasRebuild; + Logging.LogMulticast(typeof(Canvas), "willRenderCanvases", + message: "InitializeAfterCanvasRebuild"); + } + +#if UNITY_EDITOR + [InitializeOnLoadMethod] +#else + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] +#endif + private static void InitializeOnLoad() + { + } + + /// + /// Callback method called before canvas rebuilds. + /// + private static void OnBeforeCanvasRebuild() + { + s_BeforeCanvasRebuildAction.Invoke(); + InitializeAfterCanvasRebuild(); + } + + /// + /// Callback method called after canvas rebuilds. + /// + private static void OnAfterCanvasRebuild() + { + s_AfterCanvasRebuildAction.Invoke(); + s_LateAfterCanvasRebuildAction.Invoke(); + } + } +} diff --git a/Packages/src/Scripts/Utilities/UIExtraCallbacks.cs.meta b/Packages/src/Scripts/Utilities/UIExtraCallbacks.cs.meta new file mode 100644 index 0000000..29c56ad --- /dev/null +++ b/Packages/src/Scripts/Utilities/UIExtraCallbacks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 560d815ed933a4196827c244a145aec1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: