using System; using System.Collections.Generic; using Coffee.UIParticleInternal; using UnityEngine; using UnityEngine.Events; namespace Coffee.UIExtensions { [ExecuteAlways] public class UIParticleAttractor : MonoBehaviour, ISerializationCallbackReceiver { public enum Movement { Linear, Smooth, Sphere } public enum UpdateMode { Normal, UnscaledTime } [SerializeField] [HideInInspector] private ParticleSystem m_ParticleSystem; [SerializeField] private List m_ParticleSystems = new List(); [Range(0.1f, 10f)] [SerializeField] private float m_DestinationRadius = 1; [Range(0f, 0.95f)] [SerializeField] private float m_DelayRate; [Range(0.001f, 100f)] [SerializeField] private float m_MaxSpeed = 1; [SerializeField] private Movement m_Movement; [SerializeField] private UpdateMode m_UpdateMode; [SerializeField] private UnityEvent m_OnAttracted; private List _uiParticles = new List(); public float destinationRadius { get => m_DestinationRadius; set => m_DestinationRadius = Mathf.Clamp(value, 0.1f, 10f); } public float delay { get => m_DelayRate; set => m_DelayRate = value; } public float maxSpeed { get => m_MaxSpeed; set => m_MaxSpeed = value; } public Movement movement { get => m_Movement; set => m_Movement = value; } public UpdateMode updateMode { get => m_UpdateMode; set => m_UpdateMode = value; } public UnityEvent onAttracted { get => m_OnAttracted; set => m_OnAttracted = value; } /// /// The target ParticleSystems to attract. Use and /// to modify the list. /// public IReadOnlyList particleSystems => m_ParticleSystems; public void AddParticleSystem(ParticleSystem ps) { if (m_ParticleSystems == null) { m_ParticleSystems = new List(); } var i = m_ParticleSystems.IndexOf(ps); if (0 <= i) return; // Already added: skip m_ParticleSystems.Add(ps); _uiParticles.Clear(); } public void RemoveParticleSystem(ParticleSystem ps) { if (m_ParticleSystems == null) { return; } var i = m_ParticleSystems.IndexOf(ps); if (i < 0) return; // Not found. skip m_ParticleSystems.RemoveAt(i); _uiParticles.Clear(); } private void Awake() { UpgradeIfNeeded(); } private void OnEnable() { UIParticleUpdater.Register(this); } private void OnDisable() { UIParticleUpdater.Unregister(this); } private void OnDestroy() { _uiParticles = null; m_ParticleSystems = null; } internal void Attract() { // Collect UIParticle if needed (same size as m_ParticleSystems) CollectUIParticlesIfNeeded(); for (var particleIndex = 0; particleIndex < m_ParticleSystems.Count; particleIndex++) { var particleSystem = m_ParticleSystems[particleIndex]; // Skip: The ParticleSystem is not active if (particleSystem == null || !particleSystem.gameObject.activeInHierarchy) continue; // Skip: No active particles var count = particleSystem.particleCount; if (count == 0) continue; var particles = ParticleSystemExtensions.GetParticleArray(count); particleSystem.GetParticles(particles, count); var uiParticle = _uiParticles[particleIndex]; var dstPos = GetDestinationPosition(uiParticle, particleSystem); for (var i = 0; i < count; i++) { // Attracted var p = particles[i]; if (0f < p.remainingLifetime && Vector3.Distance(p.position, dstPos) < m_DestinationRadius) { p.remainingLifetime = 0f; particles[i] = p; if (m_OnAttracted != null) { try { m_OnAttracted.Invoke(); } catch (Exception e) { Debug.LogException(e); } } continue; } // Calc attracting time var delayTime = p.startLifetime * m_DelayRate; var duration = p.startLifetime - delayTime; var time = Mathf.Max(0, p.startLifetime - p.remainingLifetime - delayTime); // Delay if (time <= 0) continue; // Attract p.position = GetAttractedPosition(p.position, dstPos, duration, time); p.velocity *= 0.5f; particles[i] = p; } particleSystem.SetParticles(particles, count); } } private Vector3 GetDestinationPosition(UIParticle uiParticle, ParticleSystem particleSystem) { var isUI = uiParticle && uiParticle.enabled; var psPos = particleSystem.transform.position; var attractorPos = transform.position; var dstPos = attractorPos; var isLocalSpace = particleSystem.IsLocalSpace(); if (isLocalSpace) { dstPos = particleSystem.transform.InverseTransformPoint(dstPos); } if (isUI) { var inverseScale = uiParticle.parentScale.Inverse(); var scale3d = uiParticle.scale3DForCalc; dstPos = dstPos.GetScaled(inverseScale, scale3d.Inverse()); // Relative mode if (uiParticle.positionMode == UIParticle.PositionMode.Relative) { var diff = uiParticle.transform.position - psPos; diff.Scale(scale3d - inverseScale); diff.Scale(scale3d.Inverse()); dstPos += diff; } #if UNITY_EDITOR if (!Application.isPlaying && !isLocalSpace) { dstPos += psPos - psPos.GetScaled(inverseScale, scale3d.Inverse()); } #endif } return dstPos; } private Vector3 GetAttractedPosition(Vector3 current, Vector3 target, float duration, float time) { var speed = m_MaxSpeed; switch (m_UpdateMode) { case UpdateMode.Normal: speed *= 60 * Time.deltaTime; break; case UpdateMode.UnscaledTime: speed *= 60 * Time.unscaledDeltaTime; break; } switch (m_Movement) { case Movement.Linear: speed /= duration; break; case Movement.Smooth: target = Vector3.Lerp(current, target, time / duration); break; case Movement.Sphere: target = Vector3.Slerp(current, target, time / duration); break; } return Vector3.MoveTowards(current, target, speed); } private void CollectUIParticlesIfNeeded() { if (m_ParticleSystems.Count == 0 || _uiParticles.Count != 0) return; // Expand capacity if (_uiParticles.Capacity < m_ParticleSystems.Capacity) { _uiParticles.Capacity = m_ParticleSystems.Capacity; } // Find UIParticle that controls the ParticleSystem for (var i = 0; i < m_ParticleSystems.Count; i++) { var ps = m_ParticleSystems[i]; if (ps == null) { _uiParticles.Add(null); continue; } var uiParticle = ps.GetComponentInParent(true); _uiParticles.Add(uiParticle.particles.Contains(ps) ? uiParticle : null); } } #if UNITY_EDITOR private void OnValidate() { _uiParticles.Clear(); } #endif void ISerializationCallbackReceiver.OnBeforeSerialize() { UpgradeIfNeeded(); } void ISerializationCallbackReceiver.OnAfterDeserialize() { } private void UpgradeIfNeeded() { // Multiple ParticleSystems support: from 'm_ParticleSystem' to 'm_ParticleSystems' if (m_ParticleSystem != null) { if (!m_ParticleSystems.Contains(m_ParticleSystem)) { m_ParticleSystems.Add(m_ParticleSystem); } m_ParticleSystem = null; Debug.Log($"Upgraded!"); } } } }