多轮审查和修复
This commit is contained in:
@@ -12,8 +12,11 @@
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Feedback",
|
||||
"BaseGames.Player",
|
||||
"Unity.Addressables",
|
||||
"Unity.ResourceManager"
|
||||
"Unity.ResourceManager",
|
||||
"Unity.RenderPipelines.Core.Runtime",
|
||||
"Unity.RenderPipelines.Universal.Runtime"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.VFX
|
||||
@@ -13,30 +15,31 @@ namespace BaseGames.VFX
|
||||
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
|
||||
[SerializeField] private VFXCatalogSO _catalog;
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_catalog != null)
|
||||
_catalog.Initialize();
|
||||
Debug.Assert(_catalog != null, "[HitFXSpawner] _catalog 未赋值,请在 Inspector 中指定 VFXCatalogSO。", this);
|
||||
_catalog.Initialize();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_onHitConfirmed != null)
|
||||
_onHitConfirmed.OnEventRaised += HandleHit;
|
||||
_onHitConfirmed?.Subscribe(HandleHit).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_onHitConfirmed != null)
|
||||
_onHitConfirmed.OnEventRaised -= HandleHit;
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
private void HandleHit(HitInfo info)
|
||||
{
|
||||
if (_catalog == null || VFXPool.Instance == null) return;
|
||||
var pool = ServiceLocator.GetOrDefault<IVFXPoolService>();
|
||||
if (pool == null) return;
|
||||
|
||||
if (_catalog.TryGetHitFX(info.DamageInfo.FxType, out var vfxRef))
|
||||
VFXPool.Instance.Play(vfxRef, info.HitPoint);
|
||||
pool.Play(vfxRef, info.HitPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace BaseGames.VFX
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[HurtFlashController] _config 未赋值,请在 Inspector 中指定 FeedbackConfigSO。", this);
|
||||
if (_renderer == null)
|
||||
_renderer = GetComponent<SpriteRenderer>();
|
||||
_block = new MaterialPropertyBlock();
|
||||
@@ -39,7 +40,7 @@ namespace BaseGames.VFX
|
||||
private IEnumerator FlashCoroutine()
|
||||
{
|
||||
SetFlash(1f);
|
||||
yield return new WaitForSeconds(_config != null ? _config.HurtFlashDuration : 0.12f);
|
||||
yield return new WaitForSeconds(_config.HurtFlashDuration);
|
||||
SetFlash(0f);
|
||||
_flashCoroutine = null;
|
||||
}
|
||||
@@ -47,8 +48,7 @@ namespace BaseGames.VFX
|
||||
private void SetFlash(float amount)
|
||||
{
|
||||
_renderer.GetPropertyBlock(_block);
|
||||
if (_config != null)
|
||||
_block.SetColor(FlashColorID, _config.HurtFlashColor);
|
||||
_block.SetColor(FlashColorID, _config.HurtFlashColor);
|
||||
_block.SetFloat(FlashAmountID, amount);
|
||||
_renderer.SetPropertyBlock(_block);
|
||||
}
|
||||
|
||||
19
Assets/Scripts/VFX/IVFXPoolService.cs
Normal file
19
Assets/Scripts/VFX/IVFXPoolService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/IVFXPoolService.cs.meta
Normal file
11
Assets/Scripts/VFX/IVFXPoolService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0a73c2b538a6104aaa0e398c22e4b1b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Assets/Scripts/VFX/PaletteSwapSystem.cs
Normal file
85
Assets/Scripts/VFX/PaletteSwapSystem.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.VFX
|
||||
{
|
||||
/// <summary>
|
||||
/// 形态切换时替换玩家精灵调色板。
|
||||
/// 通过 Texture2D 查找表(LUT)Shader 实现,不换 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 支持 _PaletteTex(Texture)属性。
|
||||
/// </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>
|
||||
/// 形态 → 调色板 LUT(1D Texture2D,256×1 px)映射表。
|
||||
/// 资产路径:Assets/ScriptableObjects/VFX/VFX_PaletteCatalog.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "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)
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/PaletteSwapSystem.cs.meta
Normal file
11
Assets/Scripts/VFX/PaletteSwapSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d83eccfa3bf5cb14293f818b424ca5c9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
135
Assets/Scripts/VFX/PostProcessManager.cs
Normal file
135
Assets/Scripts/VFX/PostProcessManager.cs
Normal 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/死亡/胜利事件。
|
||||
/// 注意:水下后处理由 UnderwaterPostProcessingController(World.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/PostProcessManager.cs.meta
Normal file
11
Assets/Scripts/VFX/PostProcessManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19b8573e60a70ee419b133d557af9c2e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
114
Assets/Scripts/VFX/RegionLightController.cs
Normal file
114
Assets/Scripts/VFX/RegionLightController.cs
Normal 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 = "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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/VFX/RegionLightController.cs.meta
Normal file
11
Assets/Scripts/VFX/RegionLightController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7f337b24a34bd5499faf556ce7daccc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -31,22 +31,11 @@ namespace BaseGames.VFX
|
||||
_map[e.type] = e.vfxRef;
|
||||
}
|
||||
|
||||
/// <summary>根据 HitFxType 查找对应 VFX Prefab 引用。</summary>
|
||||
/// <summary>根据 HitFxType 查找对应 VFX Prefab 引用。调用前必须先调用 Initialize()。</summary>
|
||||
public bool TryGetHitFX(HitFxType type, out AssetReferenceGameObject vfxRef)
|
||||
{
|
||||
if (_map != null)
|
||||
return _map.TryGetValue(type, out vfxRef);
|
||||
|
||||
// 未初始化时回退至线性查找
|
||||
if (hitEffects != null)
|
||||
{
|
||||
foreach (var e in hitEffects)
|
||||
{
|
||||
if (e.type == type) { vfxRef = e.vfxRef; return true; }
|
||||
}
|
||||
}
|
||||
vfxRef = default;
|
||||
return false;
|
||||
Debug.Assert(_map != null, "[VFXCatalogSO] TryGetHitFX 被调用前必须先调用 Initialize()。");
|
||||
return _map.TryGetValue(type, out vfxRef);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,8 @@ namespace BaseGames.VFX
|
||||
/// 粒子播放完成(或超过 MaxLifetime)后自动回池,调用方无需手动归还。
|
||||
/// 不依赖 UniTask,使用 Coroutine 驱动回收。
|
||||
/// </summary>
|
||||
public class VFXPool : MonoBehaviour
|
||||
public class VFXPool : MonoBehaviour, IVFXPoolService
|
||||
{
|
||||
public static VFXPool Instance { get; private set; }
|
||||
|
||||
/// <summary>全局兜底超时(秒)。防止循环粒子永不回池。</summary>
|
||||
[SerializeField, Min(1f)] private float _globalMaxLifetime = 10f;
|
||||
@@ -21,7 +20,15 @@ namespace BaseGames.VFX
|
||||
private readonly Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools
|
||||
= new Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>>();
|
||||
|
||||
private void Awake() => Instance = this;
|
||||
private void Awake()
|
||||
{
|
||||
BaseGames.Core.ServiceLocator.Register<IVFXPoolService>(this);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
BaseGames.Core.ServiceLocator.Unregister<IVFXPoolService>(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在世界坐标播放一次特效(Fire-and-forget,Coroutine 自动回池)。
|
||||
@@ -32,7 +39,11 @@ namespace BaseGames.VFX
|
||||
Quaternion rotation = default,
|
||||
float maxLifetime = 0f)
|
||||
{
|
||||
StartCoroutine(PlayCoroutine(vfxRef, position, rotation, maxLifetime));
|
||||
// 池中有现成实例:同步播放,无需等待 Addressables 异步加载
|
||||
if (TryDequeue(vfxRef, out var ps))
|
||||
StartCoroutine(PlayImmediate(vfxRef, ps, position, rotation, maxLifetime));
|
||||
else
|
||||
StartCoroutine(PlayLoadAsync(vfxRef, position, rotation, maxLifetime));
|
||||
}
|
||||
|
||||
/// <summary>预热:预先创建若干实例避免首次播放卡顿。</summary>
|
||||
@@ -42,47 +53,56 @@ namespace BaseGames.VFX
|
||||
}
|
||||
|
||||
// ── 内部实现 ─────────────────────────────────────────────────────────────
|
||||
private IEnumerator PlayCoroutine(AssetReferenceGameObject vfxRef,
|
||||
|
||||
/// <summary>池命中路径:跳过异步加载,直接定位+播放。</summary>
|
||||
private IEnumerator PlayImmediate(AssetReferenceGameObject vfxRef,
|
||||
ParticleSystem ps,
|
||||
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();
|
||||
yield return WaitAndReturn(ps, vfxRef, maxLifetime);
|
||||
}
|
||||
|
||||
float limit = maxLifetime > 0f ? maxLifetime : _globalMaxLifetime;
|
||||
/// <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;
|
||||
@@ -92,13 +112,14 @@ namespace BaseGames.VFX
|
||||
if (ps.IsAlive(true))
|
||||
{
|
||||
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
Debug.LogWarning(
|
||||
$"[VFXPool] '{vfxRef.RuntimeKey}' 超过 {limit:F1}s 强制回收。" +
|
||||
" 请检查粒子是否设为 Loop 或 Duration 过长。");
|
||||
if (vfxRef != null)
|
||||
Debug.LogWarning(
|
||||
$"[VFXPool] '{vfxRef.RuntimeKey}' 超过 {limit:F1}s 强制回收。" +
|
||||
" 请检查粒子是否设为 Loop 或 Duration 过长。");
|
||||
}
|
||||
|
||||
ps.gameObject.SetActive(false);
|
||||
Enqueue(vfxRef, ps);
|
||||
if (vfxRef != null) Enqueue(vfxRef, ps);
|
||||
}
|
||||
|
||||
private IEnumerator WarmupCoroutine(AssetReferenceGameObject vfxRef, int count)
|
||||
|
||||
Reference in New Issue
Block a user