摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,24 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.VFX",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.VFX",
"references": [
"BaseGames.Core.Events",
"BaseGames.Core",
"BaseGames.Combat",
"BaseGames.Feedback",
"BaseGames.Player",
"Unity.Addressables",
"Unity.ResourceManager",
"Unity.RenderPipelines.Core.Runtime",
"Unity.RenderPipelines.Universal.Runtime"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c71c756e9181e714aa56edbbf35d4df2
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Combat;
namespace BaseGames.VFX
{
/// <summary>
/// 全局命中特效派发器:订阅 HitConfirmedEventChannel
/// 根据 DamageInfo.FxType 从 VFXCatalog 查找并播放特效。
/// 放置在 Persistent 场景的 [Systems] GameObject 上。
/// </summary>
public class HitFXSpawner : MonoBehaviour
{
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
[SerializeField] private VFXCatalogSO _catalog;
private readonly CompositeDisposable _subs = new();
private void Awake()
{
Debug.Assert(_catalog != null, "[HitFXSpawner] _catalog 未赋值,请在 Inspector 中指定 VFXCatalogSO。", this);
_catalog.Initialize();
}
private void OnEnable()
{
_onHitConfirmed?.Subscribe(HandleHit).AddTo(_subs);
}
private void OnDisable()
{
_subs.Clear();
}
private void HandleHit(HitInfo info)
{
var pool = ServiceLocator.GetOrDefault<IVFXPoolService>();
if (pool == null) return;
if (_catalog.TryGetHitFX(info.DamageInfo.FxType, out var vfxRef))
pool.Play(vfxRef, info.HitPoint);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6380807294a34ad4cb0a2b7099712163
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System.Collections;
using UnityEngine;
using BaseGames.Feedback;
namespace BaseGames.VFX
{
/// <summary>
/// 受伤闪白效果控制器。
/// 通过 MaterialPropertyBlock 修改 SpriteRenderer 材质的 _FlashAmount 参数0~1
/// 对应 Shader 需暴露 _FlashColor 与 _FlashAmount 两个属性。
/// 调用 Flash() 触发一次闪白动画Coroutine 实现,不依赖 UniTask
/// </summary>
public class HurtFlashController : MonoBehaviour
{
[SerializeField] private SpriteRenderer _renderer;
[SerializeField] private FeedbackConfigSO _config;
private static readonly int FlashColorID = Shader.PropertyToID("_FlashColor");
private static readonly int FlashAmountID = Shader.PropertyToID("_FlashAmount");
private MaterialPropertyBlock _block;
private Coroutine _flashCoroutine;
private WaitForSeconds _waitForFlash;
private void Awake()
{
Debug.Assert(_config != null, "[HurtFlashController] _config 未赋值,请在 Inspector 中指定 FeedbackConfigSO。", this);
if (_renderer == null)
_renderer = GetComponent<SpriteRenderer>();
_block = new MaterialPropertyBlock();
if (_config != null)
_waitForFlash = new WaitForSeconds(_config.HurtFlashDuration);
}
/// <summary>触发一次闪白动画。若上一次闪白未结束则重置计时器。</summary>
public void Flash()
{
if (_flashCoroutine != null)
StopCoroutine(_flashCoroutine);
_flashCoroutine = StartCoroutine(FlashCoroutine());
}
private IEnumerator FlashCoroutine()
{
SetFlash(1f);
yield return _waitForFlash ?? new WaitForSeconds(_config.HurtFlashDuration);
SetFlash(0f);
_flashCoroutine = null;
}
private void SetFlash(float amount)
{
_renderer.GetPropertyBlock(_block);
_block.SetColor(FlashColorID, _config.HurtFlashColor);
_block.SetFloat(FlashAmountID, amount);
_renderer.SetPropertyBlock(_block);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0913d359cdb3e3849b286bc6e7cc311f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
// Assets/Scripts/VFX/IVFXPoolService.cs
// VFX 对象池服务接口,通过 ServiceLocator 注册与查询。
// VFXPool 实现此接口;调用方通过接口解耦。
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace BaseGames.VFX
{
public interface IVFXPoolService
{
void Play(AssetReferenceGameObject vfxRef,
Vector3 position,
Quaternion rotation = default,
float maxLifetime = 0f);
void Warmup(AssetReferenceGameObject vfxRef, int count);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c0a73c2b538a6104aaa0e398c22e4b1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Player;
namespace BaseGames.VFX
{
/// <summary>
/// 形态切换时替换玩家精灵调色板。
/// 通过 Texture2D 查找表LUTShader 实现,不换 Sprite 资产。
/// 挂在玩家 SpriteRenderer 所在 GameObject 上。
/// 由 FormController 在切换形态时调用 ApplyPalette(FormType)。
/// </summary>
public class PaletteSwapSystem : MonoBehaviour
{
[SerializeField] private SpriteRenderer _renderer;
[SerializeField] private PaletteCatalogSO _catalog;
private static readonly int PaletteTexID = Shader.PropertyToID("_PaletteTex");
private MaterialPropertyBlock _block;
private void Awake()
{
if (_renderer == null)
_renderer = GetComponent<SpriteRenderer>();
Debug.Assert(_catalog != null, "[PaletteSwapSystem] _catalog 未赋值,请在 Inspector 中指定 PaletteCatalogSO。", this);
_block = new MaterialPropertyBlock();
}
/// <summary>
/// 切换到指定形态的调色板。由 FormController.SwitchForm() 调用。
/// 要求 SpriteRenderer 所用 Shader 支持 _PaletteTexTexture属性。
/// </summary>
public void ApplyPalette(FormType form)
{
if (_renderer == null) return;
if (!_catalog.TryGetPalette(form, out var tex)) return;
_renderer.GetPropertyBlock(_block);
_block.SetTexture(PaletteTexID, tex);
_renderer.SetPropertyBlock(_block);
}
}
// ────────────────────────────────────────────────────────────────────────────
// PaletteCatalogSO
// ────────────────────────────────────────────────────────────────────────────
/// <summary>
/// 形态 → 调色板 LUT1D Texture2D256×1 px映射表。
/// 资产路径Assets/ScriptableObjects/VFX/VFX_PaletteCatalog.asset
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/VFX/PaletteCatalog")]
public class PaletteCatalogSO : ScriptableObject
{
[SerializeField] private PaletteEntry[] _entries;
/// <summary>懒初始化字典缓存,将 O(n) 线性查找降为 O(1)。</summary>
private Dictionary<FormType, Texture2D> _cache;
/// <summary>根据形态类型查找对应的调色板 LUT 纹理。</summary>
public bool TryGetPalette(FormType form, out Texture2D tex)
{
if (_cache == null)
{
_cache = new Dictionary<FormType, Texture2D>(_entries?.Length ?? 0);
if (_entries != null)
foreach (var e in _entries)
_cache[e.form] = e.paletteLUT;
}
return _cache.TryGetValue(form, out tex);
}
private void OnValidate() => _cache = null; // 编辑器更改 _entries 后重建缓存
}
/// <summary>单条形态 → LUT 映射数据。</summary>
[Serializable]
public struct PaletteEntry
{
public FormType form;
public Texture2D paletteLUT; // 1D 查找表纹理256×1 px
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d83eccfa3bf5cb14293f818b424ca5c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,135 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using BaseGames.Core.Events;
namespace BaseGames.VFX
{
/// <summary>
/// 后处理 Volume 分区管理器,挂在 Persistent 场景 [PostProcess] GameObject 上。
/// 通过 Coroutine 平滑 blend Weight监听游戏状态/Boss/死亡/胜利事件。
/// 注意:水下后处理由 UnderwaterPostProcessingControllerWorld.Liquid独立负责此组件不处理。
/// </summary>
public class PostProcessManager : MonoBehaviour
{
[Header("Volume 引用Persistent 场景内)")]
[SerializeField] private Volume _bossArenaVolume;
[SerializeField] private Volume _deathVolume;
[SerializeField] private Volume _victoryVolume;
[Header("事件频道")]
[SerializeField] private VoidEventChannelSO _onBossFightStarted;
[SerializeField] private VoidEventChannelSO _onBossFightEnded;
[SerializeField] private VoidEventChannelSO _onPlayerDied;
[SerializeField] private VoidEventChannelSO _onPlayerRespawned;
[SerializeField] private VoidEventChannelSO _onBossDefeated;
[SerializeField] private float _blendDuration = 0.4f;
private Volume[] _managedVolumes;
private Coroutine _blendCoroutine;
/// <summary>调用 BlendCoroutine / ResetAllCoroutine 时复用的起始权重缓存,避免每次分配新数组。</summary>
private float[] _startWeights;
private readonly CompositeDisposable _subs = new();
private void Awake()
{
_managedVolumes = new[] { _bossArenaVolume, _deathVolume, _victoryVolume };
_startWeights = new float[_managedVolumes.Length];
}
private void OnEnable()
{
_onBossFightStarted?.Subscribe(HandleBossFightStarted).AddTo(_subs);
_onBossFightEnded?.Subscribe(HandleBossFightEnded).AddTo(_subs);
_onPlayerDied?.Subscribe(HandlePlayerDied).AddTo(_subs);
_onPlayerRespawned?.Subscribe(HandlePlayerRespawned).AddTo(_subs);
_onBossDefeated?.Subscribe(HandleBossDefeated).AddTo(_subs);
}
private void OnDisable()
{
_subs.Clear();
}
// ── 事件处理 ────────────────────────────────────────────────────────────
private void HandleBossFightStarted() => BlendTo(_bossArenaVolume);
private void HandleBossFightEnded() => ResetAll();
private void HandlePlayerDied() => BlendTo(_deathVolume);
private void HandlePlayerRespawned() => ResetAll();
private void HandleBossDefeated() => BlendTo(_victoryVolume);
// ── 混合逻辑 ─────────────────────────────────────────────────────────────
/// <summary>将目标 Volume 权重渐变为 1其他管理中的 Volume 渐变为 0。</summary>
private void BlendTo(Volume target)
{
if (_blendCoroutine != null)
StopCoroutine(_blendCoroutine);
_blendCoroutine = StartCoroutine(BlendCoroutine(target, 1f));
}
/// <summary>所有受管 Volume 权重渐变回 0恢复默认后处理。</summary>
private void ResetAll()
{
if (_blendCoroutine != null)
StopCoroutine(_blendCoroutine);
_blendCoroutine = StartCoroutine(ResetAllCoroutine());
}
private IEnumerator BlendCoroutine(Volume target, float targetWeight)
{
float elapsed = 0f;
for (int i = 0; i < _managedVolumes.Length; i++)
_startWeights[i] = _managedVolumes[i] != null ? _managedVolumes[i].weight : 0f;
while (elapsed < _blendDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / _blendDuration);
for (int i = 0; i < _managedVolumes.Length; i++)
{
if (_managedVolumes[i] == null) continue;
float dest = _managedVolumes[i] == target ? targetWeight : 0f;
_managedVolumes[i].weight = Mathf.Lerp(_startWeights[i], dest, t);
}
yield return null;
}
// 确保精确收敛
for (int i = 0; i < _managedVolumes.Length; i++)
{
if (_managedVolumes[i] == null) continue;
_managedVolumes[i].weight = _managedVolumes[i] == target ? targetWeight : 0f;
}
_blendCoroutine = null;
}
private IEnumerator ResetAllCoroutine()
{
float elapsed = 0f;
for (int i = 0; i < _managedVolumes.Length; i++)
_startWeights[i] = _managedVolumes[i] != null ? _managedVolumes[i].weight : 0f;
while (elapsed < _blendDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / _blendDuration);
for (int i = 0; i < _managedVolumes.Length; i++)
{
if (_managedVolumes[i] == null) continue;
_managedVolumes[i].weight = Mathf.Lerp(_startWeights[i], 0f, t);
}
yield return null;
}
for (int i = 0; i < _managedVolumes.Length; i++)
{
if (_managedVolumes[i] != null)
_managedVolumes[i].weight = 0f;
}
_blendCoroutine = null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19b8573e60a70ee419b133d557af9c2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using BaseGames.Core.Events;
namespace BaseGames.VFX
{
/// <summary>
/// 区域进出时平滑切换 Global Light 2D 颜色和强度。
/// 挂在 Persistent 场景 [Lighting] GameObject 上,监听 OnRegionEntered 事件频道。
/// </summary>
public class RegionLightController : MonoBehaviour
{
[SerializeField] private Light2D _globalLight;
[SerializeField] private RegionLightCatalogSO _catalog;
[SerializeField] private StringEventChannelSO _onRegionEntered;
[SerializeField] private float _transitionDuration = 1.5f;
private Coroutine _colorCoroutine;
private Coroutine _intensityCoroutine;
private readonly CompositeDisposable _subs = new();
private void Awake()
{
Debug.Assert(_catalog != null, "[RegionLightController] _catalog 未赋值,请在 Inspector 中指定 RegionLightCatalogSO。", this);
Debug.Assert(_globalLight != null, "[RegionLightController] _globalLight 未赋值,请在 Inspector 中绑定 Light2D。", this);
}
private void OnEnable()
{
_onRegionEntered?.Subscribe(OnRegionEntered).AddTo(_subs);
}
private void OnDisable()
{
_subs.Clear();
}
private void OnRegionEntered(string regionId)
{
if (!_catalog.TryGet(regionId, out var config)) return;
if (_colorCoroutine != null) StopCoroutine(_colorCoroutine);
if (_intensityCoroutine != null) StopCoroutine(_intensityCoroutine);
_colorCoroutine = StartCoroutine(TweenColor(_globalLight.color, config.Color, _transitionDuration));
_intensityCoroutine = StartCoroutine(TweenIntensity(_globalLight.intensity, config.Intensity, _transitionDuration));
}
// ── Coroutine helpers ────────────────────────────────────────────────────
private IEnumerator TweenColor(Color from, Color to, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
_globalLight.color = Color.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
yield return null;
}
_globalLight.color = to;
_colorCoroutine = null;
}
private IEnumerator TweenIntensity(float from, float to, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
_globalLight.intensity = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / duration));
yield return null;
}
_globalLight.intensity = to;
_intensityCoroutine = null;
}
}
// ────────────────────────────────────────────────────────────────────────────
// RegionLightCatalogSO
// ────────────────────────────────────────────────────────────────────────────
/// <summary>
/// 区域 ID → Global Light 2D 颜色 + 强度映射表。
/// 资产路径Assets/ScriptableObjects/VFX/VFX_RegionLightCatalog.asset
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/VFX/RegionLightCatalog")]
public class RegionLightCatalogSO : ScriptableObject
{
[Serializable]
public struct RegionLightConfig
{
public string regionId;
public Color Color;
[Range(0f, 1f)] public float Intensity;
}
[SerializeField] private RegionLightConfig[] _entries;
/// <summary>根据 regionId 查找对应的灯光配置。</summary>
public bool TryGet(string regionId, out RegionLightConfig cfg)
{
if (_entries != null)
{
foreach (var e in _entries)
{
if (e.regionId == regionId) { cfg = e; return true; }
}
}
cfg = default;
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7f337b24a34bd5499faf556ce7daccc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using BaseGames.Combat;
namespace BaseGames.VFX
{
/// <summary>
/// VFX 资产映射字典HitFxType → VFX Prefab Addressable 引用。
/// 在 GameManager.OnGameplayStarted 中调用 Initialize() 建立快速查表。
/// 资产路径Assets/ScriptableObjects/VFX/VFX_Catalog.asset
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/VFX/VFXCatalog")]
public class VFXCatalogSO : ScriptableObject
{
[Header("命中特效映射")]
public VFXEntry[] hitEffects;
[Header("预热配置")]
public VFXWarmupEntry[] warmups;
private Dictionary<HitFxType, AssetReferenceGameObject> _map;
/// <summary>建立快速查表字典。在 Gameplay 开始前调用一次。</summary>
public void Initialize()
{
_map = new Dictionary<HitFxType, AssetReferenceGameObject>();
if (hitEffects == null) return;
foreach (var e in hitEffects)
_map[e.type] = e.vfxRef;
}
/// <summary>根据 HitFxType 查找对应 VFX Prefab 引用。调用前必须先调用 Initialize()。</summary>
public bool TryGetHitFX(HitFxType type, out AssetReferenceGameObject vfxRef)
{
Debug.Assert(_map != null, "[VFXCatalogSO] TryGetHitFX 被调用前必须先调用 Initialize()。");
return _map.TryGetValue(type, out vfxRef);
}
}
[Serializable]
public struct VFXEntry
{
public HitFxType type;
public AssetReferenceGameObject vfxRef;
}
[Serializable]
public struct VFXWarmupEntry
{
public AssetReferenceGameObject vfxRef;
[Min(1)] public int warmupCount;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a55d31d8ca14fa5498d42bc28a4189f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,152 @@
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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1f2e57f42abd15c419f9013416d4a6d8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: