多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -284,100 +284,101 @@ namespace BaseGames.VFX
/// <summary>
/// ParticleSystem 专用对象池,挂在 Persistent 场景 [VFXPool] GameObject 上。
/// 粒子播放完成(或超过 MaxLifetime后自动回池调用方无需手动归还。
/// 不依赖 UniTask使用 Coroutine 驱动回收。
/// </summary>
public class VFXPool : MonoBehaviour
{
public static VFXPool Instance { get; private set; }
/// <summary>
/// 全局兜底超时(秒)。单个特效可在 Prefab 的 VFXPoolEntry 组件上覆盖。
/// 防止循环粒子或异常长时间特效永不回池导致内存膨胀。
/// </summary>
[SerializeField, Min(1f)] private float _globalMaxLifetime = 10f;
readonly Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools = new();
private readonly Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>> _pools
= new Dictionary<AssetReferenceGameObject, Queue<ParticleSystem>>();
void Awake() => Instance = this;
private void Awake() => Instance = this;
/// <summary>
/// 在世界坐标播放一次特效Fire-and-forgetUniTask 自动回池)。
/// 在世界坐标播放一次特效Fire-and-forgetCoroutine 自动回池)。
/// </summary>
/// <param name="maxLifetime">
/// &gt; 0 时覆盖全局超时;≤ 0 时使用 <see cref="_globalMaxLifetime"/>。
/// </param>
public async UniTaskVoid Play(AssetReferenceGameObject vfxRef,
Vector3 position,
Quaternion rotation = default,
float maxLifetime = 0f)
public void Play(AssetReferenceGameObject vfxRef,
Vector3 position,
Quaternion rotation = default,
float maxLifetime = 0f)
{
var ps = await GetOrCreateAsync(vfxRef);
float limit = maxLifetime > 0f ? maxLifetime : _globalMaxLifetime;
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))
{
var op = Addressables.InstantiateAsync(vfxRef, transform);
yield return op;
if (op.Result == null) { Debug.LogError($"[VFXPool] Failed: {vfxRef.RuntimeKey}"); yield break; }
ps = op.Result.GetComponent<ParticleSystem>();
if (ps == null) { Addressables.ReleaseInstance(op.Result); yield break; }
ps.gameObject.SetActive(false);
}
ps.transform.SetPositionAndRotation(position, rotation);
ps.gameObject.SetActive(true);
ps.Play();
// 双重退出条件:粒子自然结束 OR 超时强制回收
using var cts = new System.Threading.CancellationTokenSource(
System.TimeSpan.FromSeconds(limit));
try
float limit = maxLifetime > 0f ? maxLifetime : _globalMaxLifetime;
float elapsed = 0f;
while (elapsed < limit && ps.IsAlive(true))
{
await UniTask.WaitUntil(() => !ps.IsAlive(true),
cancellationToken: cts.Token);
elapsed += Time.deltaTime;
yield return null;
}
catch (System.OperationCanceledException)
if (ps.IsAlive(true))
{
// 超时:强制停止
ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
Debug.LogWarning(
$"[VFXPool] '{vfxRef.RuntimeKey}' 超过 {limit:F1}s 强制回收。" +
"请检查粒子是否设为 Loop 或 Duration 过长。");
Debug.LogWarning($"[VFXPool] '{vfxRef.RuntimeKey}' 超过 {limit:F1}s 强制回收。");
}
ps.gameObject.SetActive(false);
_pools[vfxRef].Enqueue(ps);
Enqueue(vfxRef, ps);
}
/// <summary>
/// 预热:预先创建若干实例避免首次播放卡顿。
/// </summary>
public async UniTask WarmupAsync(AssetReferenceGameObject vfxRef, int count)
private IEnumerator WarmupCoroutine(AssetReferenceGameObject vfxRef, int count)
{
for (int i = 0; i < count; i++)
{
var go = await Addressables.InstantiateAsync(vfxRef, transform).Task;
var ps = go.GetComponent<ParticleSystem>();
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);
if (!_pools.ContainsKey(vfxRef)) _pools[vfxRef] = new Queue<ParticleSystem>();
_pools[vfxRef].Enqueue(ps);
Enqueue(vfxRef, ps);
}
}
async UniTask<ParticleSystem> GetOrCreateAsync(AssetReferenceGameObject vfxRef)
private bool TryDequeue(AssetReferenceGameObject key, out ParticleSystem ps)
{
if (_pools.TryGetValue(vfxRef, out var q) && q.Count > 0)
return q.Dequeue();
ps = null;
return _pools.TryGetValue(key, out var q) && q.Count > 0 && (ps = q.Dequeue()) != null;
}
// 池不存在时初始化
if (!_pools.ContainsKey(vfxRef))
_pools[vfxRef] = new Queue<ParticleSystem>();
var go = await Addressables.InstantiateAsync(vfxRef, transform).Task;
return go.GetComponent<ParticleSystem>();
private void Enqueue(AssetReferenceGameObject key, ParticleSystem ps)
{
if (!_pools.ContainsKey(key)) _pools[key] = new Queue<ParticleSystem>();
_pools[key].Enqueue(ps);
}
}
}
```
**`maxLifetime` 使用指南**
| 特效类型 | 建议 `maxLifetime` | 说明 |
|----------|-------------------|------|
| 命中火花 / 数字 | 默认3s | 时长已知,无需覆盖 |
| 爆炸 / 大范围 | `5f` | 稍长,粒子散逸需时间 |
| Boss 相位特效 | `15f` | 长动画,须显式指定 |
| 环境氛围循环粒子 | **不使用 VFXPool** | Loop 粒子应手动管理生命周期 |
---
## 7. VFXCatalogSO — VFX 映射字典
@@ -448,16 +449,32 @@ namespace BaseGames.VFX
/// </summary>
public class HitFXSpawner : MonoBehaviour
{
[SerializeField] HitConfirmedEventChannelSO _onHitConfirmed;
[SerializeField] VFXCatalogSO _catalog;
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
[SerializeField] private VFXCatalogSO _catalog;
void OnEnable() => _onHitConfirmed.OnEventRaised += HandleHit;
void OnDisable() => _onHitConfirmed.OnEventRaised -= HandleHit;
void HandleHit(HitInfo info)
private void Awake()
{
if (_catalog.TryGetHitFX(info.DamageInfo.HitFxType, out var vfxRef))
VFXPool.Instance.Play(vfxRef, info.HitPoint).Forget();
if (_catalog != null)
_catalog.Initialize();
}
private void OnEnable()
{
if (_onHitConfirmed != null)
_onHitConfirmed.OnEventRaised += HandleHit;
}
private void OnDisable()
{
if (_onHitConfirmed != null)
_onHitConfirmed.OnEventRaised -= HandleHit;
}
private void HandleHit(HitInfo info)
{
if (_catalog == null || VFXPool.Instance == null) return;
if (_catalog.TryGetHitFX(info.DamageInfo.FxType, out var vfxRef))
VFXPool.Instance.Play(vfxRef, info.HitPoint);
}
}
}
@@ -474,32 +491,48 @@ namespace BaseGames.VFX
/// 受击白闪效果,通过 Material Property Block 修改 Shader 参数。
/// 挂在玩家/敌人的 SpriteRenderer 所在 GameObject 上。
/// 不复制 Material避免 GC通过 MaterialPropertyBlock 实现。
/// 若上一次闪白未结束则重置计时器(支持连击打断重置)。
/// </summary>
public class HurtFlashController : MonoBehaviour
{
[SerializeField] SpriteRenderer _renderer;
[SerializeField] FeedbackConfigSO _config;
[SerializeField] private SpriteRenderer _renderer;
[SerializeField] private FeedbackConfigSO _config;
static readonly int FlashColorID = Shader.PropertyToID("_FlashColor");
static readonly int FlashAmountID = Shader.PropertyToID("_FlashAmount");
private static readonly int FlashColorID = Shader.PropertyToID("_FlashColor");
private static readonly int FlashAmountID = Shader.PropertyToID("_FlashAmount");
MaterialPropertyBlock _block;
private MaterialPropertyBlock _block;
private Coroutine _flashCoroutine;
void Awake() => _block = new MaterialPropertyBlock();
private void Awake()
{
if (_renderer == null)
_renderer = GetComponent<SpriteRenderer>();
_block = new MaterialPropertyBlock();
}
/// <summary>触发一次受击白闪(由 IFeedbackPlayer.PlayTakeHit 间接调用)。</summary>
public async UniTaskVoid Flash(CancellationToken ct = default)
/// <summary>触发一次受击白闪。若上一次未结束则重置计时器。</summary>
public void Flash()
{
if (_flashCoroutine != null)
StopCoroutine(_flashCoroutine);
_flashCoroutine = StartCoroutine(FlashCoroutine());
}
private IEnumerator FlashCoroutine()
{
SetFlash(1f);
yield return new WaitForSeconds(_config != null ? _config.HurtFlashDuration : 0.12f);
SetFlash(0f);
_flashCoroutine = null;
}
private void SetFlash(float amount)
{
_renderer.GetPropertyBlock(_block);
_block.SetColor(FlashColorID, _config.HurtFlashColor);
_block.SetFloat(FlashAmountID, 1f);
_renderer.SetPropertyBlock(_block);
await UniTask.Delay(
TimeSpan.FromSeconds(_config.HurtFlashDuration),
cancellationToken: ct);
_block.SetFloat(FlashAmountID, 0f);
if (_config != null)
_block.SetColor(FlashColorID, _config.HurtFlashColor);
_block.SetFloat(FlashAmountID, amount);
_renderer.SetPropertyBlock(_block);
}
}
@@ -519,21 +552,28 @@ namespace BaseGames.VFX
/// 形态切换时替换玩家精灵调色板。
/// 通过 Texture2D 查找表LUTShader 实现,不换 Sprite 资产。
/// 挂在玩家 SpriteRenderer 所在 GameObject 上。
/// 由 FormController 在切换形态时调用 ApplyPalette(FormType)。
/// </summary>
public class PaletteSwapSystem : MonoBehaviour
{
[SerializeField] SpriteRenderer _renderer;
[SerializeField] PaletteCatalogSO _catalog;
[SerializeField] private SpriteRenderer _renderer;
[SerializeField] private PaletteCatalogSO _catalog;
static readonly int PaletteTexID = Shader.PropertyToID("_PaletteTex");
private static readonly int PaletteTexID = Shader.PropertyToID("_PaletteTex");
MaterialPropertyBlock _block;
private MaterialPropertyBlock _block;
void Awake() => _block = new MaterialPropertyBlock();
private void Awake()
{
if (_renderer == null)
_renderer = GetComponent<SpriteRenderer>();
_block = new MaterialPropertyBlock();
}
/// <summary>切换到指定形态的调色板。由 FormController 调用。</summary>
/// <summary>切换到指定形态的调色板。由 FormController.SwitchForm() 调用。</summary>
public void ApplyPalette(FormType form)
{
if (_catalog == null || _renderer == null) return;
if (!_catalog.TryGetPalette(form, out var tex)) return;
_renderer.GetPropertyBlock(_block);
_block.SetTexture(PaletteTexID, tex);
@@ -544,13 +584,16 @@ namespace BaseGames.VFX
[CreateAssetMenu(menuName = "VFX/PaletteCatalog")]
public class PaletteCatalogSO : ScriptableObject
{
public PaletteEntry[] entries;
[SerializeField] private PaletteEntry[] _entries;
public bool TryGetPalette(FormType form, out Texture2D tex)
{
foreach (var e in entries)
if (_entries != null)
{
if (e.form == form) { tex = e.paletteLUT; return true; }
foreach (var e in _entries)
{
if (e.form == form) { tex = e.paletteLUT; return true; }
}
}
tex = null;
return false;
@@ -560,8 +603,8 @@ namespace BaseGames.VFX
[Serializable]
public struct PaletteEntry
{
public FormType form;
public Texture2D paletteLUT; // 1D 查找表纹理256×1 px
public FormType form;
public Texture2D paletteLUT; // 1D 查找表纹理256×1 px
}
}
```
@@ -575,97 +618,129 @@ namespace BaseGames.VFX
{
/// <summary>
/// 后处理 Volume 分区管理器,挂在 Persistent 场景 [PostProcess] GameObject 上。
/// 通过 DOTween 平滑 blend Weight监听游戏状态事件。
/// 通过 Coroutine 平滑 blend Weight监听游戏状态/Boss/死亡/胜利事件。
/// 注意:水下后处理由 UnderwaterPostProcessingControllerWorld.Liquid独立负责此组件不处理。
/// </summary>
public class PostProcessManager : MonoBehaviour
{
[Header("Volume 引用Persistent 场景内)")]
[SerializeField] Volume _underwaterVolume; // Priority=10
[SerializeField] Volume _bossArenaVolume; // Priority=10
[SerializeField] Volume _deathVolume; // Priority=20
[SerializeField] Volume _victoryVolume; // Priority=10
[SerializeField] private Volume _bossArenaVolume; // Priority=10
[SerializeField] private Volume _deathVolume; // Priority=20
[SerializeField] private Volume _victoryVolume; // Priority=10
[Header("事件频道")]
[SerializeField] VoidEventChannelSO _onLiquidEntered;
[SerializeField] VoidEventChannelSO _onLiquidExited;
[SerializeField] VoidEventChannelSO _onBossFightStarted;
[SerializeField] VoidEventChannelSO _onBossFightEnded;
[SerializeField] VoidEventChannelSO _onPlayerDied;
[SerializeField] VoidEventChannelSO _onPlayerRespawned;
[SerializeField] VoidEventChannelSO _onBossDefeated;
[SerializeField] private VoidEventChannelSO _onBossFightStarted;
[SerializeField] private VoidEventChannelSO _onBossFightEnded;
[SerializeField] private VoidEventChannelSO _onPlayerDied;
[SerializeField] private VoidEventChannelSO _onPlayerRespawned;
[SerializeField] private VoidEventChannelSO _onBossDefeated;
[SerializeField] float _blendDuration = 0.4f;
[SerializeField] private float _blendDuration = 0.4f;
private Volume[] _nonDefaultVolumes;
private Volume[] _managedVolumes;
private Coroutine _blendCoroutine;
private void Awake()
{
_nonDefaultVolumes = new[] { _underwaterVolume, _bossArenaVolume, _deathVolume, _victoryVolume };
_managedVolumes = new[] { _bossArenaVolume, _deathVolume, _victoryVolume };
}
private void OnEnable()
{
_onLiquidEntered.OnEventRaised += () => BlendTo(_underwaterVolume);
_onLiquidExited.OnEventRaised += ResetAll;
_onBossFightStarted.OnEventRaised += () => BlendTo(_bossArenaVolume);
_onBossFightEnded.OnEventRaised += ResetAll;
_onPlayerDied.OnEventRaised += () => BlendTo(_deathVolume);
_onPlayerRespawned.OnEventRaised += ResetAll;
_onBossDefeated.OnEventRaised += () => BlendTo(_victoryVolume);
if (_onBossFightStarted != null) _onBossFightStarted.OnEventRaised += HandleBossFightStarted;
if (_onBossFightEnded != null) _onBossFightEnded.OnEventRaised += HandleBossFightEnded;
if (_onPlayerDied != null) _onPlayerDied.OnEventRaised += HandlePlayerDied;
if (_onPlayerRespawned != null) _onPlayerRespawned.OnEventRaised += HandlePlayerRespawned;
if (_onBossDefeated != null) _onBossDefeated.OnEventRaised += HandleBossDefeated;
}
private void OnDisable()
{
_onLiquidEntered.OnEventRaised -= () => BlendTo(_underwaterVolume);
_onLiquidExited.OnEventRaised -= ResetAll;
_onBossFightStarted.OnEventRaised -= () => BlendTo(_bossArenaVolume);
_onBossFightEnded.OnEventRaised -= ResetAll;
_onPlayerDied.OnEventRaised -= () => BlendTo(_deathVolume);
_onPlayerRespawned.OnEventRaised -= ResetAll;
_onBossDefeated.OnEventRaised -= () => BlendTo(_victoryVolume);
if (_onBossFightStarted != null) _onBossFightStarted.OnEventRaised -= HandleBossFightStarted;
if (_onBossFightEnded != null) _onBossFightEnded.OnEventRaised -= HandleBossFightEnded;
if (_onPlayerDied != null) _onPlayerDied.OnEventRaised -= HandlePlayerDied;
if (_onPlayerRespawned != null) _onPlayerRespawned.OnEventRaised -= HandlePlayerRespawned;
if (_onBossDefeated != null) _onBossDefeated.OnEventRaised -= HandleBossDefeated;
}
private void HandleBossFightStarted() => BlendTo(_bossArenaVolume);
private void HandleBossFightEnded() => ResetAll();
private void HandlePlayerDied() => BlendTo(_deathVolume);
private void HandlePlayerRespawned() => ResetAll();
private void HandleBossDefeated() => BlendTo(_victoryVolume);
private void BlendTo(Volume target)
{
foreach (var v in _nonDefaultVolumes)
DOTween.To(() => v.weight, x => v.weight = x, 0f, _blendDuration)
.SetAutoKill(true).SetLink(gameObject);
DOTween.To(() => target.weight, x => target.weight = x, 1f, _blendDuration)
.SetAutoKill(true).SetLink(gameObject);
if (_blendCoroutine != null) StopCoroutine(_blendCoroutine);
_blendCoroutine = StartCoroutine(BlendCoroutine(target, 1f));
}
private void ResetAll()
{
foreach (var v in _nonDefaultVolumes)
DOTween.To(() => v.weight, x => v.weight = x, 0f, _blendDuration)
.SetAutoKill(true).SetLink(gameObject);
if (_blendCoroutine != null) StopCoroutine(_blendCoroutine);
_blendCoroutine = StartCoroutine(ResetAllCoroutine());
}
private IEnumerator BlendCoroutine(Volume target, float targetWeight)
{
float elapsed = 0f;
float[] startWeights = new float[_managedVolumes.Length];
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;
}
}
private IEnumerator ResetAllCoroutine()
{
float elapsed = 0f;
float[] startWeights = new float[_managedVolumes.Length];
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;
}
}
}
}
```
> **⚠️ 实现说明**
> - 使用 `Time.unscaledDeltaTime`,暂停期间 blend 不受 `Time.timeScale` 影响
> - 水下 Volume`_underwaterVolume`)由 `World.Liquid` 模块中的 `UnderwaterPostProcessingController` 独立管理,不在此组件中
> - 不使用 DOTween不使用 Lambda 订阅Lambda 无法从事件中移除)
### Volume 结构与 Profile 参数
```
Persistent 场景 [PostProcess]:
├── Volume_Default Priority=0 Weight=1.0(始终生效基础 Profile
├── Volume_Underwater Priority=10 Weight=0进水时 blend 到 1.0
├── Volume_Underwater Priority=10 Weight=0由 UnderwaterPostProcessingController 管理
├── Volume_BossArena Priority=10 Weight=0Boss 战开始时 blend 到 1.0
├── Volume_Death Priority=20 Weight=0玩家死亡时 blend 到 1.0
└── Volume_Victory Priority=10 Weight=0Boss 击败时 blend 到 1.0
```
| Volume | Bloom | Color Grading | Vignette | Chromatic Aberration |
|--------|-------|--------------|----------|---------------------|
| Default | Intensity 0.3 | 正常 | 0.2 | 关闭 |
| Underwater | 0.1 | 青绿 Filter -0.3 | 0.45 | 0.4 |
| BossArena | 0.5 | 饱和度 +20% | 0.35 | 0.15 |
| Death | 0 | 去饱和度 -80% | 0.7(黑色)| 0.8 |
| Victory | 0.8(白色)| 亮度 +0.4 | 0 | 0 |
> **DOTween 规范**:所有 `DOTween.To()` 必须链式调用 `.SetAutoKill(true).SetLink(gameObject)`;禁止使用 `DOTween.KillAll()`。
---
## 12. RegionLightController
@@ -676,28 +751,64 @@ namespace BaseGames.VFX
/// <summary>
/// 区域进出时平滑切换 Global Light 2D 颜色和强度。
/// 挂在 Persistent 场景 [Lighting] GameObject 上,监听 OnRegionEntered 事件频道。
/// 使用 Coroutine 实现平滑过渡,不依赖 DOTween。
/// </summary>
public class RegionLightController : MonoBehaviour
{
[SerializeField] Light2D _globalLight;
[SerializeField] RegionLightCatalogSO _catalog; // RegionId → 颜色 + 强度
[SerializeField] StringEventChannelSO _onRegionEntered;
[SerializeField] float _transitionDuration = 1.5f;
[SerializeField] private Light2D _globalLight;
[SerializeField] private RegionLightCatalogSO _catalog;
[SerializeField] private StringEventChannelSO _onRegionEntered;
[SerializeField] private float _transitionDuration = 1.5f;
private void OnEnable() => _onRegionEntered.OnEventRaised += OnRegionEntered;
private void OnDisable() => _onRegionEntered.OnEventRaised -= OnRegionEntered;
private Coroutine _colorCoroutine;
private Coroutine _intensityCoroutine;
private void OnEnable()
{
if (_onRegionEntered != null)
_onRegionEntered.OnEventRaised += OnRegionEntered;
}
private void OnDisable()
{
if (_onRegionEntered != null)
_onRegionEntered.OnEventRaised -= OnRegionEntered;
}
private void OnRegionEntered(string regionId)
{
if (_catalog == null || _globalLight == null) return;
if (!_catalog.TryGet(regionId, out var config)) return;
DOTween.To(() => _globalLight.color,
x => _globalLight.color = x,
config.Color, _transitionDuration)
.SetAutoKill(true).SetLink(gameObject);
DOTween.To(() => _globalLight.intensity,
x => _globalLight.intensity = x,
config.Intensity, _transitionDuration)
.SetAutoKill(true).SetLink(gameObject);
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));
}
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;
}
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;
}
}
@@ -712,13 +823,16 @@ namespace BaseGames.VFX
[Range(0f, 1f)] public float Intensity;
}
[SerializeField] RegionLightConfig[] _entries;
[SerializeField] private RegionLightConfig[] _entries;
public bool TryGet(string regionId, out RegionLightConfig cfg)
{
foreach (var e in _entries)
if (_entries != null)
{
if (e.regionId == regionId) { cfg = e; return true; }
foreach (var e in _entries)
{
if (e.regionId == regionId) { cfg = e; return true; }
}
}
cfg = default;
return false;