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