using System.Collections; using UnityEngine; using UnityEngine.UI; using BaseGames.Core; using BaseGames.Spells; namespace BaseGames.UI.HUD { /// /// 法术槽 HUD 小组件。 /// /// 通过 ServiceLocator 获取 ISpellService,显示: /// · 已装备法术图标(无法术时显示空槽占位) /// · 冷却进度环(_cooldownFill.fillAmount = CooldownFraction,0 = 冷却完毕,1 = 刚施放) /// /// 图标刺新:订阅 ISpellService.OnSpellChanged 事件,装备切换实时响应(零延迟)。 /// 冷却进度:协程专职追踪,冷却中每帧更新,待机时每 0.1 秒轮询。 /// /// Inspector 必填: /// _iconImage — 法术图标 Image /// _cooldownFill — 覆盖在图标上的冷却遮罩 Image(fillMethod = Radial360 推荐) /// _emptySlot — 无法术时显示的占位对象 /// public class SpellSlotWidget : MonoBehaviour { // ── Inspector 字段 ──────────────────────────────────────────────────── [SerializeField] private Image _iconImage; [Tooltip("冷却进度覆盖图(填充方式建议设为 Radial360 + 顺时针)。")] [SerializeField] private Image _cooldownFill; [Tooltip("无法术装备时显示的空槽占位对象。")] [SerializeField] private GameObject _emptySlot; // ── 私有状态 ────────────────────────────────────────────────────────── private ISpellService _spellService; private SpellSO _cachedSpell; // 延迟绑定期间班本记录,避免重复 SetSprite // ── 生命周期 ────────────────────────────────────────────────────────── private void OnEnable() { _cachedSpell = null; StartCoroutine(TrackSpell()); } private void OnDisable() { StopAllCoroutines(); if (_spellService != null) { _spellService.OnSpellChanged -= RefreshIcon; _spellService = null; } _cachedSpell = null; } // ── 协程 ───────────────────────────────────────────────────────────── /// /// 阶段一:延迟绑定——等到 ISpellService 就绪,订阅图标事件并同步初始状态。 /// 阶段二:冷却追踪——事件驱动图标(零延迟),协程专职更新进度环。 /// private IEnumerator TrackSpell() { var pollWait = new WaitForSeconds(0.1f); // 阶段一:延迟绑定(SpellManager 可能晚于此组件初始化) while (_spellService == null) { _spellService = ServiceLocator.GetOrDefault(); if (_spellService == null) { ShowEmpty(); yield return pollWait; continue; } // 服务就绪:订阅装备变更事件(零延迟图标刷新)+ 同步当前状态 _spellService.OnSpellChanged += RefreshIcon; RefreshIcon(_spellService.EquippedSpell); } // 阶段二:冷却进度追踪(图标已由事件驱动,协程仅负责冷却环) while (true) { float f = _spellService.CooldownFraction; if (_cooldownFill != null) _cooldownFill.fillAmount = f; yield return f > 0f ? null : (object)pollWait; } } // ── 视觉刷新 ────────────────────────────────────────────────────────── private void RefreshIcon(SpellSO spell) { _cachedSpell = spell; bool hasSpell = spell != null; if (_emptySlot != null) _emptySlot.SetActive(!hasSpell); if (_iconImage != null) { _iconImage.gameObject.SetActive(hasSpell); if (hasSpell) _iconImage.sprite = spell.icon; } if (_cooldownFill != null) { _cooldownFill.fillAmount = 0f; _cooldownFill.gameObject.SetActive(hasSpell); } } private void ShowEmpty() { if (_emptySlot != null) _emptySlot.SetActive(true); if (_iconImage != null) _iconImage.gameObject.SetActive(false); if (_cooldownFill != null) { _cooldownFill.fillAmount = 0f; _cooldownFill.gameObject.SetActive(false); } } } }