186 lines
7.8 KiB
C#
186 lines
7.8 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Combat.StatusEffects;
|
||
using StatusEffectType = BaseGames.Combat.StatusEffects.StatusEffectType;
|
||
|
||
namespace BaseGames.UI.HUD
|
||
{
|
||
/// <summary>
|
||
/// 状态效果 HUD —— 在 HUD 上为玩家展示当前激活的状态效果图标、叠层数和倒计时。
|
||
///
|
||
/// 布局约定:挂在 HUD Canvas 内,持有一个 _slotTemplate(默认 inactive)和一个 _container。
|
||
/// 每种 StatusEffectType 最多对应一个槽位实例,通过字典管理。
|
||
///
|
||
/// 订阅的事件频道:
|
||
/// _onStatusEffectApplied → OnStatusEffectApplied
|
||
/// _onStatusEffectExpired → OnStatusEffectExpired
|
||
/// </summary>
|
||
public class StatusEffectHUD : MonoBehaviour
|
||
{
|
||
// ── Inspector 字段 ────────────────────────────────────────────────────
|
||
|
||
[Header("Slot")]
|
||
[Tooltip("槽位模板预制(设为 inactive);每种效果类型会从此 Instantiate 一个实例。")]
|
||
[SerializeField] private GameObject _slotTemplate;
|
||
|
||
[Tooltip("槽位实例的父节点(水平/垂直 LayoutGroup 即可)。")]
|
||
[SerializeField] private Transform _container;
|
||
|
||
[Header("效果图标配置")]
|
||
[SerializeField] private SlotConfig[] _slotConfigs;
|
||
|
||
[Header("Event Channels")]
|
||
[SerializeField] private StatusEffectEventChannelSO _onStatusEffectApplied;
|
||
[SerializeField] private StatusEffectEventChannelSO _onStatusEffectExpired;
|
||
|
||
// ── 内部数据 ──────────────────────────────────────────────────────────
|
||
|
||
/// <summary>每种 StatusEffectType 对应的 Inspector 配置(图标 Sprite)。</summary>
|
||
[System.Serializable]
|
||
public struct SlotConfig
|
||
{
|
||
public StatusEffectType effectType;
|
||
[Tooltip("该状态效果在 HUD 上显示的图标。")]
|
||
public Sprite icon;
|
||
}
|
||
|
||
private class SlotInstance
|
||
{
|
||
public GameObject root;
|
||
public Image iconImage;
|
||
public TMP_Text durationText; // 倒计时文本(可为 null)
|
||
public TMP_Text stackText; // 叠层数文本(可为 null)
|
||
public Coroutine countdown;
|
||
}
|
||
|
||
private readonly Dictionary<StatusEffectType, SlotInstance> _activeSlots = new();
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
// 倒计时节流:缓存 WaitForSecondsRealtime 以避免每帧堆分配
|
||
private readonly WaitForSecondsRealtime _countdownTick = new(0.1f);
|
||
|
||
// 图标快速查找表: Awake 时一次性从 _slotConfigs 构建,避免 OnStatusEffectApplied 高频线性扫描
|
||
private Dictionary<StatusEffectType, Sprite> _iconLookup;
|
||
|
||
// ── 生命周期 ───────────────────────────────────────────────────────
|
||
|
||
private void Awake()
|
||
{
|
||
_iconLookup = new Dictionary<StatusEffectType, Sprite>(_slotConfigs?.Length ?? 0);
|
||
if (_slotConfigs != null)
|
||
foreach (var cfg in _slotConfigs)
|
||
_iconLookup[cfg.effectType] = cfg.icon;
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onStatusEffectApplied?.Subscribe(OnStatusEffectApplied).AddTo(_subs);
|
||
_onStatusEffectExpired?.Subscribe(OnStatusEffectExpired).AddTo(_subs);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
_subs.Clear();
|
||
StopAllCoroutines();
|
||
foreach (var slot in _activeSlots.Values)
|
||
if (slot.root != null) slot.root.SetActive(false);
|
||
_activeSlots.Clear();
|
||
}
|
||
|
||
// ── 事件处理 ──────────────────────────────────────────────────────────
|
||
|
||
private void OnStatusEffectApplied(StatusEffectEvent e)
|
||
{
|
||
if (!_activeSlots.TryGetValue(e.EffectType, out var slot))
|
||
{
|
||
slot = CreateSlot(e.EffectType);
|
||
if (slot == null) return;
|
||
_activeSlots[e.EffectType] = slot;
|
||
}
|
||
|
||
slot.root.SetActive(true);
|
||
RefreshSlotDisplay(slot, e.StackCount, e.RemainingDuration);
|
||
|
||
// 重启倒计时协程(OnStack 刷新持续时间时也会走到这里)
|
||
if (slot.countdown != null)
|
||
StopCoroutine(slot.countdown);
|
||
slot.countdown = StartCoroutine(CountdownRoutine(slot, e.RemainingDuration));
|
||
}
|
||
|
||
private void OnStatusEffectExpired(StatusEffectEvent e)
|
||
{
|
||
if (!_activeSlots.TryGetValue(e.EffectType, out var slot)) return;
|
||
|
||
if (slot.countdown != null)
|
||
{
|
||
StopCoroutine(slot.countdown);
|
||
slot.countdown = null;
|
||
}
|
||
slot.root.SetActive(false);
|
||
}
|
||
|
||
// ── 协程 ─────────────────────────────────────────────────────────────
|
||
|
||
private IEnumerator CountdownRoutine(SlotInstance slot, float totalDuration)
|
||
{
|
||
float endTime = Time.realtimeSinceStartup + totalDuration;
|
||
|
||
while (Time.realtimeSinceStartup < endTime)
|
||
{
|
||
float remaining = endTime - Time.realtimeSinceStartup;
|
||
if (slot.durationText != null)
|
||
slot.durationText.text = remaining.ToString("F1");
|
||
yield return _countdownTick; // 0.1s 节忍,替代每帧 yield return null
|
||
}
|
||
|
||
slot.countdown = null;
|
||
slot.root.SetActive(false);
|
||
}
|
||
|
||
// ── 内部辅助 ──────────────────────────────────────────────────────────
|
||
|
||
private void RefreshSlotDisplay(SlotInstance slot, int stackCount, float duration)
|
||
{
|
||
if (slot.durationText != null)
|
||
slot.durationText.text = duration.ToString("F1");
|
||
|
||
bool multiStack = stackCount > 1;
|
||
if (slot.stackText != null)
|
||
{
|
||
slot.stackText.gameObject.SetActive(multiStack);
|
||
if (multiStack) slot.stackText.text = stackCount.ToString();
|
||
}
|
||
}
|
||
|
||
private SlotInstance CreateSlot(StatusEffectType type)
|
||
{
|
||
if (_slotTemplate == null || _container == null) return null;
|
||
|
||
_iconLookup.TryGetValue(type, out Sprite icon); // O(1) 查找
|
||
|
||
var go = Instantiate(_slotTemplate, _container);
|
||
go.SetActive(false);
|
||
|
||
var slot = new SlotInstance { root = go };
|
||
|
||
// 约定:第一个 Image 子节点作为图标;第一个 TMP_Text 作为倒计时;第二个作为叠层数
|
||
var images = go.GetComponentsInChildren<Image>(includeInactive: true);
|
||
if (images.Length > 0)
|
||
{
|
||
slot.iconImage = images[0];
|
||
if (icon != null) slot.iconImage.sprite = icon;
|
||
}
|
||
|
||
var texts = go.GetComponentsInChildren<TMP_Text>(includeInactive: true);
|
||
if (texts.Length > 0) slot.durationText = texts[0];
|
||
if (texts.Length > 1) slot.stackText = texts[1];
|
||
|
||
return slot;
|
||
}
|
||
}
|
||
}
|