using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using BaseGames.Core.Events; using BaseGames.Localization; namespace BaseGames.UI { /// /// 单条通知弹窗(架构 10_UIModule §11)。 /// 由 ToastManager 控制显示和自动隐藏。 /// [RequireComponent(typeof(CanvasGroup))] public class ToastNotification : MonoBehaviour { [SerializeField] private TMP_Text _titleText; [SerializeField] private TMP_Text _bodyText; [SerializeField] private Image _icon; [SerializeField] private float _displayDuration = 3f; [SerializeField] private float _fadeDuration = 0.25f; private CanvasGroup _cg; private Coroutine _hideCoroutine; private WaitForSecondsRealtime _waitDisplay; // 缓存:避免每次 Show 堆分配 /// 淡入 + 保持 + 淡出的总时长(供 ToastManager 队列计时用)。 public float TotalTime => _displayDuration + _fadeDuration * 2f; private void Awake() { _cg = GetComponent(); _cg.alpha = 0f; _waitDisplay = new WaitForSecondsRealtime(_displayDuration); } public void Show(string title, string body, Sprite icon = null) { if (_titleText != null) _titleText.text = title; if (_bodyText != null) _bodyText.text = body; if (_icon != null) { _icon.sprite = icon; _icon.enabled = icon != null; } gameObject.SetActive(true); if (_hideCoroutine != null) StopCoroutine(_hideCoroutine); _hideCoroutine = StartCoroutine(AutoHide()); } private IEnumerator AutoHide() { // 淡入 yield return StartCoroutine(FadeTo(1f)); // 保持(复用缓存的 WaitForSecondsRealtime) yield return _waitDisplay; // 淡出 yield return StartCoroutine(FadeTo(0f)); gameObject.SetActive(false); } private IEnumerator FadeTo(float target) { float start = _cg.alpha; float elapsed = 0f; while (elapsed < _fadeDuration) { _cg.alpha = Mathf.Lerp(start, target, elapsed / _fadeDuration); elapsed += Time.unscaledDeltaTime; yield return null; } _cg.alpha = target; } } // ───────────────────────────────────────────────────────────────────────── /// /// 通知队列管理器(架构 10_UIModule §11 ToastManager)。 /// 同时只显示一条 Toast;队列不为空时前一条结束后立即显示下一条。 /// 订阅 EVT_AchievementUnlocked / EVT_AbilityUnlocked 事件频道。 /// public class ToastManager : MonoBehaviour { [SerializeField] private ToastNotification _toast; [Header("Event Channels")] [SerializeField] private StringEventChannelSO _onAchievementUnlocked; // EVT_AchievementUnlocked [SerializeField] private StringEventChannelSO _onAbilityUnlocked; // EVT_AbilityUnlocked private readonly Queue<(string title, string body, Sprite icon)> _queue = new(); private readonly CompositeDisposable _subs = new(); private bool _showing; private WaitForSecondsRealtime _queueWait; // 缓存:避免 ProcessQueue 每条堆分配 private void Awake() { // _toast.TotalTime 基于 Inspector 常量,只需 Awake 时初始化一次 if (_toast != null) _queueWait = new WaitForSecondsRealtime(_toast.TotalTime + 0.1f); } private void OnEnable() { _onAchievementUnlocked?.Subscribe(OnAchievement).AddTo(_subs); _onAbilityUnlocked?.Subscribe(OnAbility).AddTo(_subs); } private void OnDisable() { _subs.Clear(); } private void OnAchievement(string id) => Enqueue(LocalizationManager.Get("TOAST_ACHIEVEMENT_TITLE", LocalizationTable.UI), id, null); private void OnAbility(string abilityId) => Enqueue(LocalizationManager.Get("TOAST_ABILITY_TITLE", LocalizationTable.UI), abilityId, null); public void Enqueue(string title, string body, Sprite icon = null) { _queue.Enqueue((title, body, icon)); if (!_showing) StartCoroutine(ProcessQueue()); } private IEnumerator ProcessQueue() { _showing = true; while (_queue.Count > 0) { var (title, body, icon) = _queue.Dequeue(); // _toast 为 null 时直接终止队列(不能显示,也无需等待) if (_toast == null) break; _toast.Show(title, body, icon); // 使用缓存的等待对象,_toast 为 null 时已在上方 break if (_queueWait == null) _queueWait = new WaitForSecondsRealtime(_toast.TotalTime + 0.1f); yield return _queueWait; } _showing = false; } } }