Files
zeling_v2/Assets/_Game/Scripts/UI/HUD/StatusEffectHUD.cs
2026-05-25 11:54:37 +08:00

186 lines
7.8 KiB
C#
Raw Permalink 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.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;
}
}
}