Files
zeling_v2/Assets/Scripts/VFX/VFXPool.cs
2026-05-08 11:04:00 +08:00

132 lines
5.1 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
{
public static VFXPool Instance { get; private set; }
/// <summary>全局兜底超时(秒)。防止循环粒子永不回池。</summary>
[SerializeField, Min(1f)] private float _globalMaxLifetime = 10f;
private readonly Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools
= new Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>>();
private void Awake() => Instance = 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)
{
StartCoroutine(PlayCoroutine(vfxRef, position, rotation, maxLifetime));
}
/// <summary>预热:预先创建若干实例避免首次播放卡顿。</summary>
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<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();
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<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);
}
}
}