Files
zeling_v2/Assets/Scripts/VFX/VFXPool.cs
2026-05-12 15:34:08 +08:00

153 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace BaseGames.VFX
{
/// <summary>
/// ParticleSystem 专用对象池。挂在 Persistent 场景 [VFXPool] GameObject 上。
/// 粒子播放完成(或超过 MaxLifetime后自动回池调用方无需手动归还。
/// 不依赖 UniTask使用 Coroutine 驱动回收。
/// </summary>
public class VFXPool : MonoBehaviour, IVFXPoolService
{
/// <summary>全局兜底超时(秒)。防止循环粒子永不回池。</summary>
[SerializeField, Min(1f)] private float _globalMaxLifetime = 10f;
private readonly Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools
= new Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>>();
private void Awake()
{
BaseGames.Core.ServiceLocator.Register<IVFXPoolService>(this);
}
private void OnDestroy()
{
BaseGames.Core.ServiceLocator.Unregister<IVFXPoolService>(this);
}
/// <summary>
/// 在世界坐标播放一次特效Fire-and-forgetCoroutine 自动回池)。
/// </summary>
/// <param name="maxLifetime">&gt;0 时覆盖全局超时≤0 时使用 _globalMaxLifetime。</param>
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));
}
/// <summary>预热:预先创建若干实例避免首次播放卡顿。</summary>
public void Warmup(AssetReferenceGameObject vfxRef, int count)
{
StartCoroutine(WarmupCoroutine(vfxRef, count));
}
// ── 内部实现 ─────────────────────────────────────────────────────────────
/// <summary>池命中路径:跳过异步加载,直接定位+播放。</summary>
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);
}
/// <summary>池未命中路径:异步加载实例后播放。</summary>
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<ParticleSystem>();
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);
}
/// <summary>等待粒子结束(或超时)后回池。</summary>
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<ParticleSystem>();
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<ParticleSystem>();
_pools[key].Enqueue(ps);
}
}
}