using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using BaseGames.Core; using BaseGames.Core.Events; namespace BaseGames.UI { /// 屏幕阅读器优先级(与 WCAG / ARIA live-region 等级对齐)。 public enum ScreenReaderPriority { /// Polite —— 当前播报结束后再播报,不打断玩家。 Polite = 0, /// Assertive —— 立即打断当前播报(如战斗死亡、关键提示)。 Assertive = 1 } /// /// 屏幕阅读器接口(无障碍)。默认无实现;接入第三方 TTS / 平台 API(iOS VoiceOver、 /// Android TalkBack、Steam Overlay TTS)时实现本接口并通过 ServiceLocator 注册。 /// 设计为 string-only 接口而非富文本,避免不同 TTS 引擎对标签解析不一致。 /// public interface IScreenReader { void Speak(string text, ScreenReaderPriority priority = ScreenReaderPriority.Polite); void StopAll(); } /// /// 字幕管理器(架构 10_UIModule §11 无障碍补完)。 /// /// 职责:与对话气泡(DialogueUI)解耦,专门承担"叙述性字幕 / 关键提示文本 / 战斗播报"。 /// 订阅 (推荐项目内新建 EVT_SubtitleShow)显示底部居中字幕; /// 若 中存在 实现则同步触发 TTS。 /// /// 队列策略:Polite 串行(FIFO);Assertive 立即清空队列并显示。 /// public class SubtitleManager : MonoBehaviour { [Header("UI")] [SerializeField] private GameObject _root; [SerializeField] private TMP_Text _label; [SerializeField] private CanvasGroup _canvasGroup; [Header("时长")] [Tooltip("每行字幕显示时间(秒);可被 Show(text, durationOverride) 覆盖。")] [SerializeField] private float _defaultDuration = 3f; [SerializeField] private float _fadeDuration = 0.2f; [Header("事件频道(可选)")] [SerializeField] private StringEventChannelSO _onSubtitleShow; private readonly Queue<(string text, float duration, ScreenReaderPriority pri)> _queue = new(); private Coroutine _playCo; private readonly CompositeDisposable _subs = new(); private void OnEnable() { if (_root != null) _root.SetActive(false); if (_canvasGroup != null) _canvasGroup.alpha = 0f; _onSubtitleShow?.Subscribe(t => Show(t, _defaultDuration, ScreenReaderPriority.Polite)).AddTo(_subs); } private void OnDisable() => _subs.Clear(); /// 显示一行字幕;自动按优先级排队。 public void Show(string text, float duration = -1f, ScreenReaderPriority priority = ScreenReaderPriority.Polite) { if (string.IsNullOrEmpty(text)) return; float d = duration > 0f ? duration : _defaultDuration; if (priority == ScreenReaderPriority.Assertive) { _queue.Clear(); if (_playCo != null) StopCoroutine(_playCo); _playCo = null; } _queue.Enqueue((text, d, priority)); // 同步触发 TTS var reader = ServiceLocator.GetOrDefault(null); reader?.Speak(text, priority); if (_playCo == null) _playCo = StartCoroutine(PlayLoop()); } private IEnumerator PlayLoop() { while (_queue.Count > 0) { var (text, d, _) = _queue.Dequeue(); yield return ShowOne(text, d); } _playCo = null; } private IEnumerator ShowOne(string text, float duration) { if (_label != null) _label.text = text; if (_root != null) _root.SetActive(true); yield return Fade(0f, 1f, _fadeDuration); yield return new WaitForSecondsRealtime(Mathf.Max(0f, duration - _fadeDuration * 2f)); yield return Fade(1f, 0f, _fadeDuration); if (_root != null) _root.SetActive(false); } private IEnumerator Fade(float from, float to, float dur) { if (_canvasGroup == null || dur <= 0f) { if (_canvasGroup != null) _canvasGroup.alpha = to; yield break; } float t = 0f; while (t < dur) { t += Time.unscaledDeltaTime; _canvasGroup.alpha = Mathf.Lerp(from, to, t / dur); yield return null; } _canvasGroup.alpha = to; } } }