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, 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 = Addressables.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}");
Addressables.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 = 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);
}
}
}