using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AddressableAssets; namespace BaseGames.VFX { /// /// ParticleSystem 专用对象池。挂在 Persistent 场景 [VFXPool] GameObject 上。 /// 粒子播放完成(或超过 MaxLifetime)后自动回池,调用方无需手动归还。 /// 不依赖 UniTask,使用 Coroutine 驱动回收。 /// public class VFXPool : MonoBehaviour { public static VFXPool Instance { get; private set; } /// 全局兜底超时(秒)。防止循环粒子永不回池。 [SerializeField, Min(1f)] private float _globalMaxLifetime = 10f; private readonly Dictionary> _pools = new Dictionary>(); private void Awake() => Instance = this; /// /// 在世界坐标播放一次特效(Fire-and-forget,Coroutine 自动回池)。 /// /// >0 时覆盖全局超时;≤0 时使用 _globalMaxLifetime。 public void Play(AssetReferenceGameObject vfxRef, Vector3 position, Quaternion rotation = default, float maxLifetime = 0f) { StartCoroutine(PlayCoroutine(vfxRef, position, rotation, maxLifetime)); } /// 预热:预先创建若干实例避免首次播放卡顿。 public void Warmup(AssetReferenceGameObject vfxRef, int count) { StartCoroutine(WarmupCoroutine(vfxRef, count)); } // ── 内部实现 ───────────────────────────────────────────────────────────── private IEnumerator PlayCoroutine(AssetReferenceGameObject vfxRef, Vector3 position, Quaternion rotation, float maxLifetime) { ParticleSystem ps = null; // 从池中取或异步创建 if (TryDequeue(vfxRef, out ps)) { // 直接使用 } else { // 异步加载并实例化 var op = Addressables.InstantiateAsync(vfxRef, transform); yield return op; if (op.Result == null) { Debug.LogError($"[VFXPool] Failed to instantiate VFX: {vfxRef.RuntimeKey}"); yield break; } ps = op.Result.GetComponent(); if (ps == null) { Debug.LogError($"[VFXPool] No ParticleSystem on VFX prefab: {vfxRef.RuntimeKey}"); Addressables.ReleaseInstance(op.Result); yield break; } ps.gameObject.SetActive(false); } // 播放 ps.transform.SetPositionAndRotation(position, rotation); ps.gameObject.SetActive(true); ps.Play(); float limit = maxLifetime > 0f ? maxLifetime : _globalMaxLifetime; float elapsed = 0f; // 等待粒子结束或超时 while (elapsed < limit && ps.IsAlive(true)) { elapsed += Time.deltaTime; yield return null; } if (ps.IsAlive(true)) { ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); Debug.LogWarning( $"[VFXPool] '{vfxRef.RuntimeKey}' 超过 {limit:F1}s 强制回收。" + " 请检查粒子是否设为 Loop 或 Duration 过长。"); } ps.gameObject.SetActive(false); Enqueue(vfxRef, ps); } private IEnumerator WarmupCoroutine(AssetReferenceGameObject vfxRef, int count) { for (int i = 0; i < count; i++) { var op = Addressables.InstantiateAsync(vfxRef, transform); yield return op; if (op.Result == null) continue; var ps = op.Result.GetComponent(); if (ps == null) { Addressables.ReleaseInstance(op.Result); continue; } ps.gameObject.SetActive(false); Enqueue(vfxRef, ps); } } private bool TryDequeue(AssetReferenceGameObject key, out ParticleSystem ps) { ps = null; return _pools.TryGetValue(key, out var q) && q.Count > 0 && (ps = q.Dequeue()) != null; } private void Enqueue(AssetReferenceGameObject key, ParticleSystem ps) { if (!_pools.ContainsKey(key)) _pools[key] = new Queue(); _pools[key].Enqueue(ps); } } }