#if UNITY_2019_3_11 || UNITY_2019_3_12 || UNITY_2019_3_13 || UNITY_2019_3_14 || UNITY_2019_3_15 || UNITY_2019_4_OR_NEWER #define SERIALIZE_FIELD_MASKABLE #endif using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using Coffee.UIParticleExtensions; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UI; [assembly: InternalsVisibleTo("Coffee.UIParticle.Editor")] namespace Coffee.UIExtensions { /// /// Render maskable and sortable particle effect ,without Camera, RenderTexture or Canvas. /// [ExecuteAlways] [RequireComponent(typeof(RectTransform))] [RequireComponent(typeof(CanvasRenderer))] public class UIParticle : MaskableGraphic { public enum MeshSharing { None, Auto, Primary, PrimarySimulator, Replica, } [HideInInspector][SerializeField] internal bool m_IsTrail = false; [Tooltip("Particle effect scale")] [SerializeField] private Vector3 m_Scale3D = new Vector3(10, 10, 10); [Tooltip("Animatable material properties. If you want to change the material properties of the ParticleSystem in Animation, enable it.")] [SerializeField] internal AnimatableProperty[] m_AnimatableProperties = new AnimatableProperty[0]; [Tooltip("Particles")] [SerializeField] private List m_Particles = new List(); [Tooltip("Mesh sharing.None: disable mesh sharing.\nAuto: automatically select Primary/Replica.\nPrimary: provides particle simulation results to the same group.\nPrimary Simulator: Primary, but do not render the particle (simulation only).\nReplica: render simulation results provided by the primary.")] [SerializeField] private MeshSharing m_MeshSharing = MeshSharing.None; [Tooltip("Mesh sharing group ID. If non-zero is specified, particle simulation results are shared within the group.")] [SerializeField] private int m_GroupId = 0; [SerializeField] private int m_GroupMaxId = 0; [SerializeField] [Tooltip("The particles will be emitted at the ParticleSystem position.\nMove the UIParticle/ParticleSystem to move the particle.")] private bool m_AbsoluteMode = false; private List m_Renderers = new List(); #if !SERIALIZE_FIELD_MASKABLE [SerializeField] private bool m_Maskable = true; #endif private DrivenRectTransformTracker _tracker; private Camera _orthoCamera; private int _groupId; /// /// Should this graphic be considered a target for raycasting? /// public override bool raycastTarget { get { return false; } set { } } /// /// Mesh sharing.None: disable mesh sharing. /// Auto: automatically select Primary/Replica. /// Primary: provides particle simulation results to the same group. /// Primary Simulator: Primary, but do not render the particle (simulation only). /// Replica: render simulation results provided by the primary. /// public MeshSharing meshSharing { get { return m_MeshSharing; } set { m_MeshSharing = value; } } /// /// Mesh sharing group ID. If non-zero is specified, particle simulation results are shared within the group. /// public int groupId { get { return _groupId; } set { if (m_GroupId == value) return; m_GroupId = value; if (m_GroupId != m_GroupMaxId) ResetGroupId(); } } public int groupMaxId { get { return m_GroupMaxId; } set { if (m_GroupMaxId == value) return; m_GroupMaxId = value; ResetGroupId(); } } /// /// Absolute particle position mode. /// The particles will be emitted at the ParticleSystem position. /// Move the UIParticle/ParticleSystem to move the particle. /// public bool absoluteMode { get { return m_AbsoluteMode; } set { m_AbsoluteMode = value; } } internal bool useMeshSharing { get { return m_MeshSharing != MeshSharing.None; } } internal bool isPrimary { get { return m_MeshSharing == MeshSharing.Primary || m_MeshSharing == MeshSharing.PrimarySimulator; } } internal bool canSimulate { get { return m_MeshSharing == MeshSharing.None || m_MeshSharing == MeshSharing.Auto || m_MeshSharing == MeshSharing.Primary || m_MeshSharing == MeshSharing.PrimarySimulator; } } internal bool canRender { get { return m_MeshSharing == MeshSharing.None || m_MeshSharing == MeshSharing.Auto || m_MeshSharing == MeshSharing.Primary || m_MeshSharing == MeshSharing.Replica; } } /// /// Particle effect scale. /// public float scale { get { return m_Scale3D.x; } set { m_Scale3D = new Vector3(value, value, value); } } /// /// Particle effect scale. /// public Vector3 scale3D { get { return m_Scale3D; } set { m_Scale3D = value; } } public List particles { get { return m_Particles; } } /// /// Get all base materials to render. /// public IEnumerable materials { get { for (var i = 0; i < m_Renderers.Count; i++) { if (!m_Renderers[i] || !m_Renderers[i].material) continue; yield return m_Renderers[i].material; } yield break; } } public override Material materialForRendering { get { return null; } } /// /// Paused. /// public bool isPaused { get; internal set; } public void Play() { particles.Exec(p => p.Simulate(0, false, true)); isPaused = false; } public void Pause() { particles.Exec(p => p.Pause()); isPaused = true; } public void Resume() { isPaused = false; } public void Stop() { particles.Exec(p => p.Stop()); isPaused = true; } public void Clear() { particles.Exec(p => p.Clear()); isPaused = true; } public void SetParticleSystemInstance(GameObject instance) { SetParticleSystemInstance(instance, true); } public void SetParticleSystemInstance(GameObject instance, bool destroyOldParticles) { if (!instance) return; foreach (Transform child in transform) { var go = child.gameObject; go.SetActive(false); if (!destroyOldParticles) continue; #if UNITY_EDITOR if (!Application.isPlaying) DestroyImmediate(go); else #endif Destroy(go); } var tr = instance.transform; tr.SetParent(transform, false); tr.localPosition = Vector3.zero; RefreshParticles(instance); } public void SetParticleSystemPrefab(GameObject prefab) { if (!prefab) return; SetParticleSystemInstance(Instantiate(prefab.gameObject), true); } public void RefreshParticles() { RefreshParticles(gameObject); } private void RefreshParticles(GameObject root) { if (!root) return; root.GetComponentsInChildren(particles); particles.RemoveAll(x => x.GetComponentInParent() != this); foreach (var ps in particles) { var tsa = ps.textureSheetAnimation; if (tsa.mode == ParticleSystemAnimationMode.Sprites && tsa.uvChannelMask == 0) tsa.uvChannelMask = UVChannelFlags.UV0; } RefreshParticles(particles); } public void RefreshParticles(List particles) { // #246: Nullptr exceptions when using nested UIParticle components in hierarchy m_Renderers.Clear(); foreach (Transform child in transform) { var uiParticleRenderer = child.GetComponent(); if (uiParticleRenderer != null) { m_Renderers.Add(uiParticleRenderer); } } var j = 0; for (var i = 0; i < particles.Count; i++) { if (!particles[i]) continue; GetRenderer(j++).Set(this, particles[i], false); if (particles[i].trails.enabled) { GetRenderer(j++).Set(this, particles[i], true); } } for (; j < m_Renderers.Count; j++) { GetRenderer(j).Clear(j); } } internal void UpdateTransformScale() { //var newScale = Vector3.one; //if (uiScaling) //{ // newScale = transform.parent.lossyScale.Inverse(); //} var newScale = transform.parent.lossyScale.Inverse(); if (transform.localScale != newScale) { transform.localScale = newScale; } } internal void UpdateRenderers() { if (!isActiveAndEnabled) return; foreach (var rend in m_Renderers) { if (!rend) { RefreshParticles(particles); break; } } var bakeCamera = GetBakeCamera(); for (var i = 0; i < m_Renderers.Count; i++) { if (!m_Renderers[i]) continue; m_Renderers[i].UpdateMesh(bakeCamera); } } internal void UpdateParticleCount() { for (var i = 0; i < m_Renderers.Count; i++) { if (!m_Renderers[i]) continue; m_Renderers[i].UpdateParticleCount(); } } protected override void OnEnable() { #if !SERIALIZE_FIELD_MASKABLE maskable = m_Maskable; #endif ResetGroupId(); _tracker.Add(this, rectTransform, DrivenTransformProperties.Scale); UIParticleUpdater.Register(this); RegisterDirtyMaterialCallback(UpdateRendererMaterial); if (0 < particles.Count) { RefreshParticles(particles); } else { RefreshParticles(); } base.OnEnable(); } internal void ResetGroupId() { if (m_GroupId == m_GroupMaxId) { _groupId = m_GroupId; } else { _groupId = Random.Range(m_GroupId, m_GroupMaxId + 1); } } /// /// This function is called when the behaviour becomes disabled. /// protected override void OnDisable() { _tracker.Clear(); UIParticleUpdater.Unregister(this); m_Renderers.ForEach(r => r.Clear()); UnregisterDirtyMaterialCallback(UpdateRendererMaterial); base.OnDisable(); } protected override void UpdateMaterial() { } /// /// Call to update the geometry of the Graphic onto the CanvasRenderer. /// protected override void UpdateGeometry() { } /// /// Callback for when properties have been changed by animation. /// protected override void OnDidApplyAnimationProperties() { } private void UpdateRendererMaterial() { for (var i = 0; i < m_Renderers.Count; i++) { if (!m_Renderers[i]) continue; m_Renderers[i].maskable = maskable; m_Renderers[i].SetMaterialDirty(); } } internal UIParticleRenderer GetRenderer(int index) { if (m_Renderers.Count <= index) { m_Renderers.Add(UIParticleRenderer.AddRenderer(this, index)); } if (!m_Renderers[index]) { m_Renderers[index] = UIParticleRenderer.AddRenderer(this, index); } return m_Renderers[index]; } private Camera GetBakeCamera() { if (!canvas) return Camera.main; // World camera. var root = canvas.rootCanvas; if (root.renderMode != RenderMode.ScreenSpaceOverlay) return root.worldCamera ? root.worldCamera : Camera.main; // Create ortho-camera. if (!_orthoCamera) { _orthoCamera = GetComponentInChildren(); if (!_orthoCamera) { var go = new GameObject("UIParticleOverlayCamera") { hideFlags = HideFlags.DontSave, }; go.SetActive(false); go.transform.SetParent(transform, false); _orthoCamera = go.AddComponent(); _orthoCamera.enabled = false; } } // var size = ((RectTransform)root.transform).rect.size; _orthoCamera.orthographicSize = Mathf.Max(size.x, size.y) * root.scaleFactor; _orthoCamera.transform.SetPositionAndRotation(new Vector3(0, 0, -1000), Quaternion.identity); _orthoCamera.orthographic = true; _orthoCamera.farClipPlane = 2000f; return _orthoCamera; } } }