diff --git a/.coffee.internal.sed b/.coffee.internal.sed new file mode 100644 index 0000000..fa71177 --- /dev/null +++ b/.coffee.internal.sed @@ -0,0 +1 @@ +s/Coffee.Internal/Coffee.UIParticleInternal/g diff --git a/Runtime/Internal.meta b/Runtime/Internal.meta new file mode 100644 index 0000000..e621e79 --- /dev/null +++ b/Runtime/Internal.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 53aa3f36032944b3fb1455e774c52396 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Extensions.meta b/Runtime/Internal/Extensions.meta new file mode 100644 index 0000000..2e1b794 --- /dev/null +++ b/Runtime/Internal/Extensions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8cf8018dee45a4c42a19eec890eaa5b1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Extensions/CanvasExtensions.cs b/Runtime/Internal/Extensions/CanvasExtensions.cs new file mode 100644 index 0000000..5cbcb6e --- /dev/null +++ b/Runtime/Internal/Extensions/CanvasExtensions.cs @@ -0,0 +1,134 @@ +#if UNITY_2021_3_0 || UNITY_2021_3_1 || UNITY_2021_3_2 || UNITY_2021_3_3 || UNITY_2021_3_4 || UNITY_2021_3_5 || UNITY_2021_3_6 || UNITY_2021_3_7 || UNITY_2021_3_8 || UNITY_2021_3_9 +#elif UNITY_2021_3_10 || UNITY_2021_3_11 || UNITY_2021_3_12 || UNITY_2021_3_13 || UNITY_2021_3_14 || UNITY_2021_3_15 || UNITY_2021_3_16 || UNITY_2021_3_17 || UNITY_2021_3_18 || UNITY_2021_3_19 +#elif UNITY_2021_3_20 || UNITY_2021_3_21 || UNITY_2021_3_22 || UNITY_2021_3_23 || UNITY_2021_3_24 || UNITY_2021_3_25 || UNITY_2021_3_26 || UNITY_2021_3_27 || UNITY_2021_3_28 || UNITY_2021_3_29 +#elif UNITY_2021_3_30 || UNITY_2021_3_31 || UNITY_2021_3_32 || UNITY_2021_3_33 +#elif UNITY_2022_2_0 || UNITY_2022_2_1 || UNITY_2022_2_2 || UNITY_2022_2_3 || UNITY_2022_2_4 || UNITY_2022_2_5 || UNITY_2022_2_6 || UNITY_2022_2_7 || UNITY_2022_2_8 || UNITY_2022_2_9 +#elif UNITY_2022_2_10 || UNITY_2022_2_11 || UNITY_2022_2_12 || UNITY_2022_2_13 || UNITY_2022_2_14 +#elif UNITY_2021_3 || UNITY_2022_2 || UNITY_2022_3 || UNITY_2023_2_OR_NEWER +#define CANVAS_SUPPORT_ALWAYS_GAMMA +#endif + +using UnityEngine; +using UnityEngine.Profiling; +#if UNITY_MODULE_VR +using UnityEngine.XR; +#endif + +namespace Coffee.UIParticleInternal +{ + internal static class CanvasExtensions + { + public static bool ShouldGammaToLinearInShader(this Canvas canvas) + { + return QualitySettings.activeColorSpace == ColorSpace.Linear && +#if CANVAS_SUPPORT_ALWAYS_GAMMA + canvas.vertexColorAlwaysGammaSpace; +#else + false; +#endif + } + + public static bool ShouldGammaToLinearInMesh(this Canvas canvas) + { + return QualitySettings.activeColorSpace == ColorSpace.Linear && +#if CANVAS_SUPPORT_ALWAYS_GAMMA + !canvas.vertexColorAlwaysGammaSpace; +#else + true; +#endif + } + + public static bool IsStereoCanvas(this Canvas canvas) + { +#if UNITY_MODULE_VR + if (FrameCache.TryGet(canvas, nameof(IsStereoCanvas), out var stereo)) return stereo; + + stereo = + canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay && canvas.worldCamera != null + && XRSettings.enabled && !string.IsNullOrEmpty(XRSettings.loadedDeviceName); + FrameCache.Set(canvas, nameof(IsStereoCanvas), stereo); + return stereo; +#else + return false; +#endif + } + + /// + /// Gets the view-projection matrix for a Canvas. + /// + public static void GetViewProjectionMatrix(this Canvas canvas, out Matrix4x4 vpMatrix) + { + canvas.GetViewProjectionMatrix(Camera.MonoOrStereoscopicEye.Mono, out vpMatrix); + } + + /// + /// Gets the view-projection matrix for a Canvas. + /// + public static void GetViewProjectionMatrix(this Canvas canvas, Camera.MonoOrStereoscopicEye eye, + out Matrix4x4 vpMatrix) + { + if (FrameCache.TryGet(canvas, nameof(GetViewProjectionMatrix), out vpMatrix)) return; + + canvas.GetViewProjectionMatrix(eye, out var viewMatrix, out var projectionMatrix); + vpMatrix = viewMatrix * projectionMatrix; + FrameCache.Set(canvas, nameof(GetViewProjectionMatrix), vpMatrix); + } + + /// + /// Gets the view and projection matrices for a Canvas. + /// + public static void GetViewProjectionMatrix(this Canvas canvas, out Matrix4x4 vMatrix, out Matrix4x4 pMatrix) + { + canvas.GetViewProjectionMatrix(Camera.MonoOrStereoscopicEye.Mono, out vMatrix, out pMatrix); + } + + /// + /// Gets the view and projection matrices for a Canvas. + /// + public static void GetViewProjectionMatrix(this Canvas canvas, Camera.MonoOrStereoscopicEye eye, + out Matrix4x4 vMatrix, out Matrix4x4 pMatrix) + { + if (FrameCache.TryGet(canvas, "GetViewMatrix", (int)eye, out vMatrix) && + FrameCache.TryGet(canvas, "GetProjectionMatrix", (int)eye, out pMatrix)) + { + return; + } + + // Get view and projection matrices. + Profiler.BeginSample("(COF)[CanvasExt] GetViewProjectionMatrix"); + var rootCanvas = canvas.rootCanvas; + var cam = rootCanvas.worldCamera; + if (rootCanvas && rootCanvas.renderMode != RenderMode.ScreenSpaceOverlay && cam) + { + if (eye == Camera.MonoOrStereoscopicEye.Mono) + { + vMatrix = cam.worldToCameraMatrix; + pMatrix = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false); + } + else + { + pMatrix = cam.GetStereoProjectionMatrix((Camera.StereoscopicEye)eye); + vMatrix = cam.GetStereoViewMatrix((Camera.StereoscopicEye)eye); + pMatrix = GL.GetGPUProjectionMatrix(pMatrix, false); + } + } + else + { + var pos = rootCanvas.transform.position; + vMatrix = Matrix4x4.TRS( + new Vector3(-pos.x, -pos.y, -1000), + Quaternion.identity, + new Vector3(1, 1, -1f)); + pMatrix = Matrix4x4.TRS( + new Vector3(0, 0, -1), + Quaternion.identity, + new Vector3(1 / pos.x, 1 / pos.y, -2 / 10000f)); + } + + FrameCache.Set(canvas, "GetViewMatrix", (int)eye, vMatrix); + FrameCache.Set(canvas, "GetProjectionMatrix", (int)eye, pMatrix); + + Profiler.EndSample(); + } + } +} diff --git a/Runtime/ModifiedMaterial.cs.meta b/Runtime/Internal/Extensions/CanvasExtensions.cs.meta similarity index 83% rename from Runtime/ModifiedMaterial.cs.meta rename to Runtime/Internal/Extensions/CanvasExtensions.cs.meta index 83251d7..cab4dbe 100644 --- a/Runtime/ModifiedMaterial.cs.meta +++ b/Runtime/Internal/Extensions/CanvasExtensions.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b0beae5bb1cb142b9ab90dc0d371f026 +guid: 9dd767b8c0f95478386e7d5079cd44df MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Internal/Extensions/Color32Extensions.cs b/Runtime/Internal/Extensions/Color32Extensions.cs new file mode 100644 index 0000000..6d57cca --- /dev/null +++ b/Runtime/Internal/Extensions/Color32Extensions.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Profiling; + +namespace Coffee.UIParticleInternal +{ + internal static class Color32Extensions + { + private static readonly List s_Colors = new List(); + private static byte[] s_LinearToGammaLut; + private static byte[] s_GammaToLinearLut; + + public static byte LinearToGamma(this byte self) + { + if (s_LinearToGammaLut == null) + { + s_LinearToGammaLut = new byte[256]; + for (var i = 0; i < 256; i++) + { + s_LinearToGammaLut[i] = (byte)(Mathf.LinearToGammaSpace(i / 255f) * 255f); + } + } + + return s_LinearToGammaLut[self]; + } + + public static byte GammaToLinear(this byte self) + { + if (s_GammaToLinearLut == null) + { + s_GammaToLinearLut = new byte[256]; + for (var i = 0; i < 256; i++) + { + s_GammaToLinearLut[i] = (byte)(Mathf.GammaToLinearSpace(i / 255f) * 255f); + } + } + + return s_GammaToLinearLut[self]; + } + + public static void LinearToGamma(this Mesh self) + { + Profiler.BeginSample("(COF)[ColorExt] LinearToGamma (Mesh)"); + self.GetColors(s_Colors); + var count = s_Colors.Count; + for (var i = 0; i < count; i++) + { + var c = s_Colors[i]; + c.r = c.r.LinearToGamma(); + c.g = c.g.LinearToGamma(); + c.b = c.b.LinearToGamma(); + s_Colors[i] = c; + } + + self.SetColors(s_Colors); + Profiler.EndSample(); + } + + public static void GammaToLinear(this Mesh self) + { + Profiler.BeginSample("(COF)[ColorExt] GammaToLinear (Mesh)"); + self.GetColors(s_Colors); + var count = s_Colors.Count; + for (var i = 0; i < count; i++) + { + var c = s_Colors[i]; + c.r = c.r.GammaToLinear(); + c.g = c.g.GammaToLinear(); + c.b = c.b.GammaToLinear(); + s_Colors[i] = c; + } + + self.SetColors(s_Colors); + Profiler.EndSample(); + } + } +} diff --git a/Runtime/Utils.cs.meta b/Runtime/Internal/Extensions/Color32Extensions.cs.meta similarity index 83% rename from Runtime/Utils.cs.meta rename to Runtime/Internal/Extensions/Color32Extensions.cs.meta index 5a68a0f..6b1df28 100644 --- a/Runtime/Utils.cs.meta +++ b/Runtime/Internal/Extensions/Color32Extensions.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: d188d31b140094ebc84a9caafbc7ac71 +guid: 0ef431b9df32c410ea5fa46be81def6b MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Internal/Extensions/ComponentExtensions.cs b/Runtime/Internal/Extensions/ComponentExtensions.cs new file mode 100644 index 0000000..b45f016 --- /dev/null +++ b/Runtime/Internal/Extensions/ComponentExtensions.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.Profiling; +using Object = UnityEngine.Object; + +namespace Coffee.UIParticleInternal +{ + /// + /// Extension methods for Component class. + /// + internal static class ComponentExtensions + { + /// + /// Get components in children of a specific type in the hierarchy of a GameObject. + /// + public static T[] GetComponentsInChildren(this Component self, int depth) + where T : Component + { + var results = ListPool.Rent(); + self.GetComponentsInChildren_Internal(results, depth); + var array = results.ToArray(); + ListPool.Return(ref results); + return array; + } + + /// + /// Get components in children of a specific type in the hierarchy of a GameObject. + /// + public static void GetComponentsInChildren(this Component self, List results, int depth) + where T : Component + { + results.Clear(); + self.GetComponentsInChildren_Internal(results, depth); + } + + private static void GetComponentsInChildren_Internal(this Component self, List results, int depth) + where T : Component + { + if (!self || results == null || depth < 0) return; + + var tr = self.transform; + if (tr.TryGetComponent(out var t)) + { + results.Add(t); + } + + if (depth - 1 < 0) return; + var childCount = tr.childCount; + for (var i = 0; i < childCount; i++) + { + tr.GetChild(i).GetComponentsInChildren_Internal(results, depth - 1); + } + } + + /// + /// Get or add a component of a specific type to a GameObject. + /// + public static T GetOrAddComponent(this Component self) where T : Component + { + if (!self) return null; + return self.TryGetComponent(out var component) + ? component + : self.gameObject.AddComponent(); + } + + /// + /// Get the root component of a specific type in the hierarchy of a GameObject. + /// + public static T GetRootComponent(this Component self) where T : Component + { + T component = null; + var transform = self.transform; + while (transform) + { + if (transform.TryGetComponent(out var c)) + { + component = c; + } + + transform = transform.parent; + } + + return component; + } + + /// + /// Get a component of a specific type in the parent hierarchy of a GameObject. + /// + public static T GetComponentInParent(this Component self, bool includeSelf, Transform stopAfter, + Predicate valid) + where T : Component + { + var tr = includeSelf ? self.transform : self.transform.parent; + while (tr) + { + if (tr.TryGetComponent(out var c) && valid(c)) return c; + if (tr == stopAfter) return null; + tr = tr.parent; + } + + return null; + } + + /// + /// Add a component of a specific type to the children of a GameObject. + /// + public static void AddComponentOnChildren(this Component self, HideFlags hideFlags, bool includeSelf) + where T : Component + { + if (self == null) return; + + Profiler.BeginSample("(COF)[ComponentExt] AddComponentOnChildren > Self"); + if (includeSelf && !self.TryGetComponent(out _)) + { + var c = self.gameObject.AddComponent(); + c.hideFlags = hideFlags; + } + + Profiler.EndSample(); + + Profiler.BeginSample("(COF)[ComponentExt] AddComponentOnChildren > Child"); + var childCount = self.transform.childCount; + for (var i = 0; i < childCount; i++) + { + var child = self.transform.GetChild(i); + if (child.TryGetComponent(out _)) continue; + + var c = child.gameObject.AddComponent(); + c.hideFlags = hideFlags; + } + + Profiler.EndSample(); + } + +#if !UNITY_2021_2_OR_NEWER && !UNITY_2020_3_45 && !UNITY_2020_3_46 && !UNITY_2020_3_47 && !UNITY_2020_3_48 + public static T GetComponentInParent(this Component self, bool includeInactive) where T : Component + { + if (!self) return null; + if (!includeInactive) return self.GetComponentInParent(); + + var current = self.transform; + while (current) + { + if (current.TryGetComponent(out var c)) return c; + current = current.parent; + } + + return null; + } +#endif + +#if UNITY_EDITOR + /// + /// Verify whether it can be converted to the specified component. + /// + internal static bool CanConvertTo(this Object context) where T : MonoBehaviour + { + return context && context.GetType() != typeof(T); + } + + /// + /// Convert to the specified component. + /// + internal static void ConvertTo(this Object context) where T : MonoBehaviour + { + var target = context as MonoBehaviour; + if (target == null) return; + + var so = new SerializedObject(target); + so.Update(); + + var oldEnable = target.enabled; + target.enabled = false; + + // Find MonoScript of the specified component. + foreach (var script in Resources.FindObjectsOfTypeAll()) + { + if (script.GetClass() != typeof(T)) + { + continue; + } + + // Set 'm_Script' to convert. + so.FindProperty("m_Script").objectReferenceValue = script; + so.ApplyModifiedProperties(); + break; + } + + if (so.targetObject is MonoBehaviour mb) + { + mb.enabled = oldEnable; + } + } +#endif + } +} diff --git a/Runtime/Internal/Extensions/ComponentExtensions.cs.meta b/Runtime/Internal/Extensions/ComponentExtensions.cs.meta new file mode 100644 index 0000000..3a3d874 --- /dev/null +++ b/Runtime/Internal/Extensions/ComponentExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8455ee485a5ee4cacbdf558f66af65fb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Extensions/GraphicExtensions.cs b/Runtime/Internal/Extensions/GraphicExtensions.cs new file mode 100644 index 0000000..1b0b23c --- /dev/null +++ b/Runtime/Internal/Extensions/GraphicExtensions.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using UnityEngine.Profiling; +using UnityEngine.UI; + +namespace Coffee.UIParticleInternal +{ + /// + /// 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 void GetMaterialsForRendering(this Graphic self, List result) + { + result.Clear(); + if (!self) return; + + var cr = self.canvasRenderer; + var count = cr.materialCount; + var popCount = cr.popMaterialCount; + + if (result.Capacity < count + popCount) + { + result.Capacity = count + popCount; + } + + for (var i = 0; i < count; i++) + { + result.Add(cr.GetMaterial(i)); + } + + for (var i = 0; i < popCount; i++) + { + result.Add(cr.GetPopMaterial(i)); + } + } + + /// + /// 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("(COF)[GraphicExt] IsInScreen"); + 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); + var screenSize = GetScreenSize(); + 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 /= screenSize.x; + s_WorldCorners[i].y /= screenSize.y; + } + + 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; + } + + /// + /// Get the actual main texture of a Graphic component. + /// + public static Texture GetActualMainTexture(this Graphic self) + { + var image = self as Image; + if (image == null) return self.mainTexture; + + var sprite = image.overrideSprite; + return sprite ? sprite.GetActualTexture() : self.mainTexture; + } + + private static Vector2Int GetScreenSize() + { +#if UNITY_EDITOR + if (!Application.isPlaying && !Camera.current) + { + var res = UnityStats.screenRes.Split('x'); + return new Vector2Int(int.Parse(res[0]), int.Parse(res[1])); + } +#endif + return new Vector2Int(Screen.width, Screen.height); + } + + 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/Runtime/Internal/Extensions/GraphicExtensions.cs.meta b/Runtime/Internal/Extensions/GraphicExtensions.cs.meta new file mode 100644 index 0000000..50b44d3 --- /dev/null +++ b/Runtime/Internal/Extensions/GraphicExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3803b037cd2ed45459dd660072f223dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Extensions/Misc.cs b/Runtime/Internal/Extensions/Misc.cs new file mode 100644 index 0000000..66d0b4e --- /dev/null +++ b/Runtime/Internal/Extensions/Misc.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using UnityEditor; +using UnityEngine; + +namespace Coffee.UIParticleInternal +{ + internal static class Misc + { + public static void Destroy(Object obj) + { + if (!obj) return; +#if UNITY_EDITOR + if (!Application.isPlaying) + { + Object.DestroyImmediate(obj); + } + else +#endif + { + Object.Destroy(obj); + } + } + + public static void DestroyImmediate(Object obj) + { + if (!obj) return; +#if UNITY_EDITOR + if (Application.isEditor) + { + Object.DestroyImmediate(obj); + } + else +#endif + { + Object.Destroy(obj); + } + } + + [Conditional("UNITY_EDITOR")] + public static void SetDirty(Object obj) + { +#if UNITY_EDITOR + if (!obj) return; + EditorUtility.SetDirty(obj); +#endif + } + } +} diff --git a/Runtime/Internal/Extensions/Misc.cs.meta b/Runtime/Internal/Extensions/Misc.cs.meta new file mode 100644 index 0000000..5668392 --- /dev/null +++ b/Runtime/Internal/Extensions/Misc.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39ed6a6b0a72e482488bd298b2ae762e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Extensions/SpriteExtensions.cs b/Runtime/Internal/Extensions/SpriteExtensions.cs new file mode 100644 index 0000000..e0d4e65 --- /dev/null +++ b/Runtime/Internal/Extensions/SpriteExtensions.cs @@ -0,0 +1,58 @@ +using System; +using UnityEngine; +using UnityEngine.U2D; +#if UNITY_EDITOR +using System.Reflection; +#endif + +namespace Coffee.UIParticleInternal +{ + /// + /// Extension methods for Sprite class. + /// + internal static class SpriteExtensions + { +#if UNITY_EDITOR + private static readonly Type s_SpriteEditorExtensionType = + Type.GetType("UnityEditor.Experimental.U2D.SpriteEditorExtension, UnityEditor") + ?? Type.GetType("UnityEditor.U2D.SpriteEditorExtension, UnityEditor"); + + private static readonly MethodInfo s_GetActiveAtlasTextureMethod = s_SpriteEditorExtensionType + .GetMethod("GetActiveAtlasTexture", BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly MethodInfo s_GetActiveAtlasMethod = s_SpriteEditorExtensionType + .GetMethod("GetActiveAtlas", BindingFlags.Static | BindingFlags.NonPublic); + + /// + /// Get the actual texture of a sprite in play mode or edit mode. + /// + public static Texture2D GetActualTexture(this Sprite self) + { + if (!self) return null; + + if (Application.isPlaying) return self.texture; + + var ret = s_GetActiveAtlasTextureMethod.Invoke(null, new object[] { self }) as Texture2D; + return ret ? ret : self.texture; + } + + /// + /// Get the active sprite atlas of a sprite in play mode or edit mode. + /// + public static SpriteAtlas GetActiveAtlas(this Sprite self) + { + if (!self) return null; + + return s_GetActiveAtlasMethod.Invoke(null, new object[] { self }) as SpriteAtlas; + } +#else + /// + /// Get the actual texture of a sprite in play mode. + /// + internal static Texture2D GetActualTexture(this Sprite self) + { + return self ? self.texture : null; + } +#endif + } +} diff --git a/Runtime/Internal/Extensions/SpriteExtensions.cs.meta b/Runtime/Internal/Extensions/SpriteExtensions.cs.meta new file mode 100644 index 0000000..07ac04d --- /dev/null +++ b/Runtime/Internal/Extensions/SpriteExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7a2e11131111447cb7fc0394a14da65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Extensions/Vector3Extensions.cs b/Runtime/Internal/Extensions/Vector3Extensions.cs new file mode 100644 index 0000000..329966c --- /dev/null +++ b/Runtime/Internal/Extensions/Vector3Extensions.cs @@ -0,0 +1,46 @@ +using UnityEngine; + +namespace Coffee.UIParticleInternal +{ + 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 Vector3 GetScaled(this Vector3 self, Vector3 other1, Vector3 other2, Vector3 other3) + { + self.Scale(other1); + self.Scale(other2); + self.Scale(other3); + return self; + } + + public static bool IsVisible(this Vector3 self) + { + return 0 < Mathf.Abs(self.x * self.y * self.z); + } + + public static bool IsVisible2D(this Vector3 self) + { + return 0 < Mathf.Abs(self.x * self.y); + } + } +} diff --git a/Runtime/Internal/Extensions/Vector3Extensions.cs.meta b/Runtime/Internal/Extensions/Vector3Extensions.cs.meta new file mode 100644 index 0000000..fc562df --- /dev/null +++ b/Runtime/Internal/Extensions/Vector3Extensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a7b5fb989e4b48c8bc7ecce834060f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/ProjectSettings.meta b/Runtime/Internal/ProjectSettings.meta new file mode 100644 index 0000000..fd230f5 --- /dev/null +++ b/Runtime/Internal/ProjectSettings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 398e06c9985ad4291a95f0749c2927fb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs b/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs new file mode 100644 index 0000000..b5689de --- /dev/null +++ b/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs @@ -0,0 +1,219 @@ +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; +#if UNITY_EDITOR +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +#endif + +namespace Coffee.UIParticleInternal +{ + public abstract class PreloadedProjectSettings : ScriptableObject +#if UNITY_EDITOR + { + private class PreprocessBuildWithReport : IPreprocessBuildWithReport + { + int IOrderedCallback.callbackOrder => 0; + + void IPreprocessBuildWithReport.OnPreprocessBuild(BuildReport report) + { + Initialize(); + } + } + + [InitializeOnLoadMethod] + [InitializeOnEnterPlayMode] + private static void Initialize() + { + const BindingFlags flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; + foreach (var t in TypeCache.GetTypesDerivedFrom(typeof(PreloadedProjectSettings<>))) + { + var defaultSettings = GetDefaultSettings(t); + if (!defaultSettings) + { + // When create a new instance, automatically set it as default settings. + defaultSettings = t.GetProperty("instance", flags) + ?.GetValue(null, null) as PreloadedProjectSettings; + } + else if (GetPreloadedSettings(t).Length != 1) + { + SetDefaultSettings(defaultSettings); + } + } + + EditorApplication.QueuePlayerLoopUpdate(); + } + + protected static string GetDefaultName(Type type, bool nicify) + { + var typeName = type.Name.Replace("ProjectSettings", ""); + return nicify + ? ObjectNames.NicifyVariableName(typeName) + : typeName; + } + + private static Object[] GetPreloadedSettings(Type type) + { + return PlayerSettings.GetPreloadedAssets() + .Where(x => x && x.GetType() == type) + .ToArray(); + } + + protected static PreloadedProjectSettings GetDefaultSettings(Type type) + { + return GetPreloadedSettings(type).FirstOrDefault() as PreloadedProjectSettings + ?? AssetDatabase.FindAssets($"t:{nameof(PreloadedProjectSettings)}") + .Select(AssetDatabase.GUIDToAssetPath) + .Select(AssetDatabase.LoadAssetAtPath) + .FirstOrDefault(x => x && x.GetType() == type); + } + + protected static void SetDefaultSettings(PreloadedProjectSettings asset) + { + if (!asset) return; + var type = asset.GetType(); + if (string.IsNullOrEmpty(AssetDatabase.GetAssetPath(asset))) + { + if (!AssetDatabase.IsValidFolder("Assets/ProjectSettings")) + { + AssetDatabase.CreateFolder("Assets", "ProjectSettings"); + } + + var assetPath = $"Assets/ProjectSettings/{GetDefaultName(type, false)}.asset"; + assetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); + AssetDatabase.CreateAsset(asset, assetPath); + } + + var preloadedAssets = PlayerSettings.GetPreloadedAssets(); + var projectSettings = GetPreloadedSettings(type); + PlayerSettings.SetPreloadedAssets(preloadedAssets + .Where(x => x) + .Except(projectSettings.Except(new[] { asset })) + .Append(asset) + .Distinct() + .ToArray()); + + AssetDatabase.Refresh(); + } + } +#else + { + } +#endif + + public abstract class PreloadedProjectSettings : PreloadedProjectSettings + where T : PreloadedProjectSettings + { + private static T s_Instance; + +#if UNITY_EDITOR + private string _jsonText; + + public static T instance + { + get + { + if (s_Instance) return s_Instance; + + s_Instance = GetDefaultSettings(typeof(T)) as T; + if (s_Instance) return s_Instance; + + s_Instance = CreateInstance(); + if (!s_Instance) + { + s_Instance = null; + return s_Instance; + } + + SetDefaultSettings(s_Instance); + return s_Instance; + } + } + + private void OnPlayModeStateChanged(PlayModeStateChange state) + { + switch (state) + { + case PlayModeStateChange.ExitingEditMode: + _jsonText = EditorJsonUtility.ToJson(this); + break; + case PlayModeStateChange.ExitingPlayMode: + if (_jsonText != null) + { + EditorJsonUtility.FromJsonOverwrite(_jsonText, this); + _jsonText = null; + } + + break; + } + } +#else + public static T instance => s_Instance ? s_Instance : s_Instance = CreateInstance(); +#endif + + /// + /// This function is called when the object becomes enabled and active. + /// + protected virtual void OnEnable() + { +#if UNITY_EDITOR + var isDefaultSettings = !s_Instance || s_Instance == this || GetDefaultSettings(typeof(T)) == this; + if (!isDefaultSettings) + { + DestroyImmediate(this, true); + return; + } + + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; +#endif + + if (s_Instance) return; + s_Instance = this as T; + } + + /// + /// This function is called when the behaviour becomes disabled. + /// + protected virtual void OnDisable() + { +#if UNITY_EDITOR + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; +#endif + if (s_Instance != this) return; + + s_Instance = null; + } + +#if UNITY_EDITOR + protected sealed class PreloadedProjectSettingsProvider : SettingsProvider + { + private Editor _editor; + private PreloadedProjectSettings _target; + + public PreloadedProjectSettingsProvider(string path) : base(path, SettingsScope.Project) + { + } + + public override void OnGUI(string searchContext) + { + if (!_target) + { + if (_editor) + { + DestroyImmediate(_editor); + _editor = null; + } + + _target = instance; + _editor = Editor.CreateEditor(_target); + } + + _editor.OnInspectorGUI(); + } + } +#endif + } +} diff --git a/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs.meta b/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs.meta new file mode 100644 index 0000000..dac7122 --- /dev/null +++ b/Runtime/Internal/ProjectSettings/PreloadedProjectSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 790ea008741dc411497c8794745319eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities.meta b/Runtime/Internal/Utilities.meta new file mode 100644 index 0000000..6fa45ea --- /dev/null +++ b/Runtime/Internal/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b2877595f27c4a70a426991d515434f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/FastAction.cs b/Runtime/Internal/Utilities/FastAction.cs new file mode 100755 index 0000000..0428d6d --- /dev/null +++ b/Runtime/Internal/Utilities/FastAction.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Profiling; + +namespace Coffee.UIParticleInternal +{ + /// + /// 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) + { + if (rhs == null) return; + Profiler.BeginSample("(COF)[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) + { + if (rhs == null) return; + Profiler.BeginSample("(COF)[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; + } + } + + public void Clear() + { + _delegates.Clear(); + } + } + + /// + /// A fast action without parameters. + /// + internal class FastAction : FastActionBase + { + /// + /// Invoke all the registered delegates. + /// + public void Invoke() + { + Invoke(action => action.Invoke()); + } + } +} diff --git a/Runtime/Internal/Utilities/FastAction.cs.meta b/Runtime/Internal/Utilities/FastAction.cs.meta new file mode 100644 index 0000000..d29b94f --- /dev/null +++ b/Runtime/Internal/Utilities/FastAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7c8c268a827b4787a8e050f1fe95ad5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/FrameCache.cs b/Runtime/Internal/Utilities/FrameCache.cs new file mode 100644 index 0000000..ce0a837 --- /dev/null +++ b/Runtime/Internal/Utilities/FrameCache.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Coffee.UIParticleInternal +{ + internal static class FrameCache + { + private static readonly Dictionary s_Caches = new Dictionary(); + + static FrameCache() + { + s_Caches.Clear(); + UIExtraCallbacks.onLateAfterCanvasRebuild += ClearAllCache; + } + +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Clear() + { + s_Caches.Clear(); + } +#endif + + /// + /// 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/Runtime/Internal/Utilities/FrameCache.cs.meta b/Runtime/Internal/Utilities/FrameCache.cs.meta new file mode 100644 index 0000000..e3aa714 --- /dev/null +++ b/Runtime/Internal/Utilities/FrameCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f129e3b07ffb4d3bbb4cc5f6bd94087 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/Logging.cs b/Runtime/Internal/Utilities/Logging.cs new file mode 100644 index 0000000..1eec46d --- /dev/null +++ b/Runtime/Internal/Utilities/Logging.cs @@ -0,0 +1,254 @@ +using System; +using System.Text; +using UnityEngine; +using Object = UnityEngine.Object; +#if ENABLE_COFFEE_LOGGER +using System.Reflection; +using System.Collections.Generic; +#else +using Conditional = System.Diagnostics.ConditionalAttribute; +#endif + +namespace Coffee.UIParticleInternal +{ + internal static class Logging + { +#if !ENABLE_COFFEE_LOGGER + private const string k_DisableSymbol = "DISABLE_COFFEE_LOGGER"; + + [Conditional(k_DisableSymbol)] +#endif + private static void Log_Internal(LogType type, object tag, object message, Object context) + { +#if ENABLE_COFFEE_LOGGER + 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 + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + 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); + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + public static void Log(object tag, object message, Object context = null) + { + Log_Internal(LogType.Log, tag, message, context ? context : tag as Object); + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + 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 ENABLE_COFFEE_LOGGER + Log_Internal(LogType.Error, tag, message, context ? context : tag as Object); +#else + Debug.LogError($"{tag}: {message}", context); +#endif + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + public static void LogMulticast(Type type, string fieldName, object instance = null, string message = null) + { +#if ENABLE_COFFEE_LOGGER + 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 + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + private static void AppendTag(StringBuilder sb, object tag) + { +#if ENABLE_COFFEE_LOGGER + try + { + sb.Append("f"); + sb.Append(Time.frameCount); + sb.Append(":["); + + switch (tag) + { + case string name: + sb.Append(name); + break; + 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 + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + private static void AppendType(StringBuilder sb, Type type) + { +#if ENABLE_COFFEE_LOGGER + if (s_TypeNameCache.TryGetValue(type, out var name)) + { + sb.Append(name); + return; + } + + // New type found + var start = sb.Length; + if (0 < start && sb[start - 1] == '<' && (type.Name == "Material" || type.Name == "Color")) + { + sb.Append('@'); + } + + 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 + } + +#if !ENABLE_COFFEE_LOGGER + [Conditional(k_DisableSymbol)] +#endif + private static void AppendReadableCode(StringBuilder sb, object tag) + { +#if ENABLE_COFFEE_LOGGER + 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.7f + modifier * -0.2f; + var value = 0.8f + modifier * 0.3f; + s_Codes[hash] = ColorUtility.ToHtmlStringRGB(Color.HSVToRGB(hue, saturation, value)); + } + + sb.Append(s_Codes[hash]); +#endif + } + +#if ENABLE_COFFEE_LOGGER + private static readonly StringBuilder s_Sb = new StringBuilder(); + private static readonly string[] s_Codes = new string[64]; + private static readonly Dictionary s_TypeNameCache = new Dictionary(); +#endif + } +} diff --git a/Runtime/Internal/Utilities/Logging.cs.meta b/Runtime/Internal/Utilities/Logging.cs.meta new file mode 100644 index 0000000..6db7856 --- /dev/null +++ b/Runtime/Internal/Utilities/Logging.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8255313895da84e7cbdc876be3795334 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/MaterialRepository.cs b/Runtime/Internal/Utilities/MaterialRepository.cs new file mode 100644 index 0000000..7f15945 --- /dev/null +++ b/Runtime/Internal/Utilities/MaterialRepository.cs @@ -0,0 +1,92 @@ +using System; +using UnityEngine; +using UnityEngine.Profiling; + +namespace Coffee.UIParticleInternal +{ + /// + /// Provides functionality to manage materials. + /// + internal static class MaterialRepository + { + private static readonly ObjectRepository s_Repository = new ObjectRepository(); + + public static int count => s_Repository.count; + +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Clear() + { + s_Repository.Clear(); + } +#endif + + /// + /// Retrieves a cached material based on the hash. + /// + public static bool Valid(Hash128 hash, Material material) + { + Profiler.BeginSample("(COF)[MaterialRegistry] Valid"); + var ret = s_Repository.Valid(hash, material); + Profiler.EndSample(); + return ret; + } + + /// + /// Adds or retrieves a cached material based on the hash. + /// + public static void Get(Hash128 hash, ref Material material, Func onCreate) + { + Profiler.BeginSample("(COF)[MaterialRepository] Get"); + s_Repository.Get(hash, ref material, onCreate); + Profiler.EndSample(); + } + + /// + /// Adds or retrieves a cached material based on the hash. + /// + public static void Get(Hash128 hash, ref Material material, string shaderName) + { + Profiler.BeginSample("(COF)[MaterialRepository] Get"); + s_Repository.Get(hash, ref material, x => new Material(Shader.Find(x)) + { + hideFlags = HideFlags.DontSave | HideFlags.NotEditable + }, shaderName); + Profiler.EndSample(); + } + + /// + /// Adds or retrieves a cached material based on the hash. + /// + public static void Get(Hash128 hash, ref Material material, string shaderName, string[] keywords) + { + Profiler.BeginSample("(COF)[MaterialRepository] Get"); + s_Repository.Get(hash, ref material, x => new Material(Shader.Find(x.shaderName)) + { + hideFlags = HideFlags.DontSave | HideFlags.NotEditable, + shaderKeywords = x.keywords + }, (shaderName, keywords)); + Profiler.EndSample(); + } + + /// + /// Adds or retrieves a cached material based on the hash. + /// + public static void Get(Hash128 hash, ref Material material, Func onCreate, T source) + { + Profiler.BeginSample("(COF)[MaterialRepository] Get"); + s_Repository.Get(hash, ref material, onCreate, source); + Profiler.EndSample(); + } + + /// + /// Removes a soft mask material from the cache. + /// + public static void Release(ref Material material) + { + Profiler.BeginSample("(COF)[MaterialRepository] Release"); + s_Repository.Release(ref material); + Profiler.EndSample(); + } + } +} diff --git a/Runtime/Internal/Utilities/MaterialRepository.cs.meta b/Runtime/Internal/Utilities/MaterialRepository.cs.meta new file mode 100644 index 0000000..44b3e5f --- /dev/null +++ b/Runtime/Internal/Utilities/MaterialRepository.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 702912f2ee2ec49bb8003a64151ae4f7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/ObjectPool.cs b/Runtime/Internal/Utilities/ObjectPool.cs new file mode 100644 index 0000000..fa1c848 --- /dev/null +++ b/Runtime/Internal/Utilities/ObjectPool.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace Coffee.UIParticleInternal +{ + /// + /// 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/Runtime/Internal/Utilities/ObjectPool.cs.meta b/Runtime/Internal/Utilities/ObjectPool.cs.meta new file mode 100644 index 0000000..6372e98 --- /dev/null +++ b/Runtime/Internal/Utilities/ObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 632cb1ba34e6a4e80b55a32bb63ca369 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/ObjectRepository.cs b/Runtime/Internal/Utilities/ObjectRepository.cs new file mode 100644 index 0000000..46213cf --- /dev/null +++ b/Runtime/Internal/Utilities/ObjectRepository.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Profiling; +using Object = UnityEngine.Object; + +namespace Coffee.UIParticleInternal +{ + internal class ObjectRepository where T : Object + { + private readonly Dictionary _cache = new Dictionary(8); + private readonly Dictionary _objectKey = new Dictionary(8); + private readonly string _name; + private readonly Action _onRelease; + private readonly Stack _pool = new Stack(8); + + public ObjectRepository(Action onRelease = null) + { + _name = $"{typeof(T).Name}Repository"; + if (onRelease == null) + { + _onRelease = x => + { +#if UNITY_EDITOR + if (!Application.isPlaying) + { + Object.DestroyImmediate(x, false); + } + else +#endif + { + Object.Destroy(x); + } + }; + } + else + { + _onRelease = onRelease; + } + + for (var i = 0; i < 8; i++) + { + _pool.Push(new Entry()); + } + } + + public int count => _cache.Count; + + public void Clear() + { + foreach (var kv in _cache) + { + var entry = kv.Value; + if (entry == null) continue; + + entry.Release(_onRelease); + _pool.Push(entry); + } + + _cache.Clear(); + _objectKey.Clear(); + } + + public bool Valid(Hash128 hash, T obj) + { + return _cache.TryGetValue(hash, out var entry) && entry.storedObject == obj; + } + + /// + /// Adds or retrieves a cached object based on the hash. + /// + public void Get(Hash128 hash, ref T obj, Func onCreate) + { + if (GetFromCache(hash, ref obj)) return; + Add(hash, ref obj, onCreate()); + } + + /// + /// Adds or retrieves a cached object based on the hash. + /// + public void Get(Hash128 hash, ref T obj, Func onCreate, TS source) + { + if (GetFromCache(hash, ref obj)) return; + Add(hash, ref obj, onCreate(source)); + } + + private bool GetFromCache(Hash128 hash, ref T obj) + { + // Find existing entry. + Profiler.BeginSample("(COF)[ObjectRepository] GetFromCache"); + if (_cache.TryGetValue(hash, out var entry)) + { + if (!entry.storedObject) + { + Release(ref entry.storedObject); + Profiler.EndSample(); + return false; + } + + if (entry.storedObject != obj) + { + // if the object is different, release the old one. + Release(ref obj); + ++entry.reference; + obj = entry.storedObject; + Logging.Log(_name, $"Get(total#{count}): {entry}"); + } + + Profiler.EndSample(); + return true; + } + + Profiler.EndSample(); + return false; + } + + private void Add(Hash128 hash, ref T obj, T newObject) + { + if (!newObject) + { + Release(ref obj); + obj = newObject; + return; + } + + // Create and add a new entry. + Profiler.BeginSample("(COF)[ObjectRepository] Add"); + var newEntry = 0 < _pool.Count ? _pool.Pop() : new Entry(); + newEntry.storedObject = newObject; + newEntry.hash = hash; + newEntry.reference = 1; + _cache[hash] = newEntry; + _objectKey[newObject.GetInstanceID()] = hash; + Logging.Log(_name, $"Add(total#{count}): {newEntry}"); + Release(ref obj); + obj = newObject; + Profiler.EndSample(); + } + + /// + /// Release a object. + /// + public void Release(ref T obj) + { + if (ReferenceEquals(obj, null)) return; + + // Find and release the entry. + Profiler.BeginSample("(COF)[ObjectRepository] Release"); + var id = obj.GetInstanceID(); + if (_objectKey.TryGetValue(id, out var hash) + && _cache.TryGetValue(hash, out var entry)) + { + entry.reference--; + if (entry.reference <= 0 || !entry.storedObject) + { + Remove(entry); + } + else + { + Logging.Log(_name, $"Release(total#{_cache.Count}): {entry}"); + } + } + else + { + Logging.Log(_name, $"Release(total#{_cache.Count}): Already released: {obj}"); + } + + obj = null; + Profiler.EndSample(); + } + + private void Remove(Entry entry) + { + if (ReferenceEquals(entry, null)) return; + + Profiler.BeginSample("(COF)[ObjectRepository] Remove"); + _cache.Remove(entry.hash); + _objectKey.Remove(entry.storedObject.GetInstanceID()); + _pool.Push(entry); + entry.reference = 0; + Logging.Log(_name, $"Remove(total#{_cache.Count}): {entry}"); + entry.Release(_onRelease); + Profiler.EndSample(); + } + + private class Entry + { + public Hash128 hash; + public int reference; + public T storedObject; + + public void Release(Action onRelease) + { + reference = 0; + if (storedObject) + { + onRelease?.Invoke(storedObject); + } + + storedObject = null; + } + + public override string ToString() + { + return $"h{(uint)hash.GetHashCode()} (refs#{reference}), {storedObject}"; + } + } + } +} diff --git a/Runtime/Internal/Utilities/ObjectRepository.cs.meta b/Runtime/Internal/Utilities/ObjectRepository.cs.meta new file mode 100644 index 0000000..4d85543 --- /dev/null +++ b/Runtime/Internal/Utilities/ObjectRepository.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a713d67bdb31e45e296e5f18460717e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Internal/Utilities/UIExtraCallbacks.cs b/Runtime/Internal/Utilities/UIExtraCallbacks.cs new file mode 100755 index 0000000..be9e7ee --- /dev/null +++ b/Runtime/Internal/Utilities/UIExtraCallbacks.cs @@ -0,0 +1,93 @@ +using System; +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +namespace Coffee.UIParticleInternal +{ + /// + /// 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] +#endif + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void InitializeOnLoad() + { + Canvas.willRenderCanvases -= OnAfterCanvasRebuild; + s_IsInitializedAfterCanvasRebuild = false; + } + + /// + /// 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/Runtime/Internal/Utilities/UIExtraCallbacks.cs.meta b/Runtime/Internal/Utilities/UIExtraCallbacks.cs.meta new file mode 100644 index 0000000..ddb673d --- /dev/null +++ b/Runtime/Internal/Utilities/UIExtraCallbacks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ea318e6e3e6c46aa97c72e28230bdc9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utilities.meta b/Runtime/Utilities.meta new file mode 100644 index 0000000..27f72e0 --- /dev/null +++ b/Runtime/Utilities.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 66c42f0f30de84ca4bd8305a1188af85 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Utilities/ParticleSystemExtensions.cs b/Runtime/Utilities/ParticleSystemExtensions.cs new file mode 100644 index 0000000..48329f0 --- /dev/null +++ b/Runtime/Utilities/ParticleSystemExtensions.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Coffee.UIParticleInternal +{ + internal static class ParticleSystemExtensions + { + private static ParticleSystem.Particle[] s_TmpParticles = new ParticleSystem.Particle[2048]; + + public static ParticleSystem.Particle[] GetParticleArray(int size) + { + if (s_TmpParticles.Length < size) + { + while (s_TmpParticles.Length < size) + { + size = Mathf.NextPowerOfTwo(size); + } + + s_TmpParticles = new ParticleSystem.Particle[size]; + } + + return s_TmpParticles; + } + + public static void ValidateShape(this ParticleSystem self) + { + var shape = self.shape; + if (shape.enabled && shape.alignToDirection) + { + if (Mathf.Approximately(shape.scale.x * shape.scale.y * shape.scale.z, 0)) + { + if (Mathf.Approximately(shape.scale.x, 0)) + { + shape.scale.Set(0.0001f, shape.scale.y, shape.scale.z); + } + else if (Mathf.Approximately(shape.scale.y, 0)) + { + shape.scale.Set(shape.scale.x, 0.0001f, shape.scale.z); + } + else if (Mathf.Approximately(shape.scale.z, 0)) + { + shape.scale.Set(shape.scale.x, shape.scale.y, 0.0001f); + } + } + } + } + + public static bool CanBakeMesh(this ParticleSystemRenderer self) + { + // #69: Editor crashes when mesh is set to null when `ParticleSystem.RenderMode = Mesh` + if (self.renderMode == ParticleSystemRenderMode.Mesh && self.mesh == null) return false; + + // #61: When `ParticleSystem.RenderMode = None`, an error occurs + if (self.renderMode == ParticleSystemRenderMode.None) return false; + + return true; + } + + public static ParticleSystemSimulationSpace GetActualSimulationSpace(this ParticleSystem self) + { + var main = self.main; + var space = main.simulationSpace; + if (space == ParticleSystemSimulationSpace.Custom && !main.customSimulationSpace) + { + space = ParticleSystemSimulationSpace.Local; + } + + return space; + } + + public static bool IsLocalSpace(this ParticleSystem self) + { + return GetActualSimulationSpace(self) == ParticleSystemSimulationSpace.Local; + } + + public static bool IsWorldSpace(this ParticleSystem self) + { + return GetActualSimulationSpace(self) == ParticleSystemSimulationSpace.World; + } + + public static void SortForRendering(this List self, Transform transform, bool sortByMaterial) + { + self.Sort((a, b) => + { + var aRenderer = a.GetComponent(); + var bRenderer = b.GetComponent(); + + // Render queue: ascending + var aMat = aRenderer.sharedMaterial ? aRenderer.sharedMaterial : aRenderer.trailMaterial; + var bMat = bRenderer.sharedMaterial ? bRenderer.sharedMaterial : bRenderer.trailMaterial; + if (!aMat && !bMat) return 0; + if (!aMat) return -1; + if (!bMat) return 1; + + if (sortByMaterial) + { + return aMat.GetInstanceID() - bMat.GetInstanceID(); + } + + if (aMat.renderQueue != bMat.renderQueue) + { + return aMat.renderQueue - bMat.renderQueue; + } + + // Sorting layer: ascending + if (aRenderer.sortingLayerID != bRenderer.sortingLayerID) + { + return SortingLayer.GetLayerValueFromID(aRenderer.sortingLayerID) - + SortingLayer.GetLayerValueFromID(bRenderer.sortingLayerID); + } + + // Sorting order: ascending + if (aRenderer.sortingOrder != bRenderer.sortingOrder) + { + return aRenderer.sortingOrder - bRenderer.sortingOrder; + } + + // Z position & sortingFudge: descending + var aTransform = a.transform; + var bTransform = b.transform; + var aPos = transform.InverseTransformPoint(aTransform.position).z + aRenderer.sortingFudge; + var bPos = transform.InverseTransformPoint(bTransform.position).z + bRenderer.sortingFudge; + if (!Mathf.Approximately(aPos, bPos)) + { + return (int)Mathf.Sign(bPos - aPos); + } + + return (int)Mathf.Sign(GetIndex(self, a) - GetIndex(self, b)); + }); + } + + private static int GetIndex(IList list, Object ps) + { + for (var i = 0; i < list.Count; i++) + { + if (list[i].GetInstanceID() == ps.GetInstanceID()) + { + return i; + } + } + + return 0; + } + + public static Texture2D GetTextureForSprite(this ParticleSystem self) + { + if (!self) return null; + + // Get sprite's texture. + var tsaModule = self.textureSheetAnimation; + if (!tsaModule.enabled || tsaModule.mode != ParticleSystemAnimationMode.Sprites) return null; + + for (var i = 0; i < tsaModule.spriteCount; i++) + { + var sprite = tsaModule.GetSprite(i); + if (!sprite) continue; + + return sprite.GetActualTexture(); + } + + return null; + } + + public static void Exec(this List self, Action action) + { + foreach (var p in self) + { + if (!p) continue; + action.Invoke(p); + } + } + } +} diff --git a/Runtime/Utilities/ParticleSystemExtensions.cs.meta b/Runtime/Utilities/ParticleSystemExtensions.cs.meta new file mode 100644 index 0000000..99af3d5 --- /dev/null +++ b/Runtime/Utilities/ParticleSystemExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e51604bfb810e44519e2710fd1b8af90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: