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: