using System.Collections; using System.Text; using TMPro; using UnityEngine; using UnityEngine.UI; using BaseGames.Localization; namespace BaseGames.Dialogue { /// /// 对话框 UI 组件(架构 14_NarrativeModule §5)。 /// 挂载在 Canvas_Overlay 下的 DialogueBox 子对象。 /// 负责打字机效果、头像显示、继续提示。 /// public class DialogueUI : MonoBehaviour { [SerializeField] private GameObject _rootPanel; [SerializeField] private TMP_Text _speakerNameText; [SerializeField] private TMP_Text _dialogueText; [SerializeField] private GameObject _speakerNamePanel; // 无名称时隐藏整个名称框 [SerializeField] private GameObject _continuePrompt; // "▼" 图标,打字完成后显示 [SerializeField] private Image _speakerPortrait; // 角色头像框 [SerializeField] private AudioSource _voiceSource; // 语音播放源(可不配置) private Coroutine _typingCoroutine; private DialogueLine _currentLine; private const float DefaultTypewriterDelay = 0.03f; /// 当前是否仍在执行打字机效果。 public bool IsTyping { get; private set; } // ── 公开 API ────────────────────────────────────────────────────── /// 显示一行对话并开始打字机效果。 public void ShowLine(DialogueLine line) { _currentLine = line; _rootPanel.SetActive(true); _continuePrompt.SetActive(false); // 说话人名称 bool hasSpeaker = !string.IsNullOrEmpty(line.speakerNameKey); if (_speakerNamePanel != null) _speakerNamePanel.SetActive(hasSpeaker); if (hasSpeaker && _speakerNameText != null) _speakerNameText.text = LocalizationManager.Get(line.speakerNameKey, "Dialogue"); // 头像 if (_speakerPortrait != null) { _speakerPortrait.gameObject.SetActive(line.portraitSprite != null); if (line.portraitSprite != null) _speakerPortrait.sprite = line.portraitSprite; } // 语音播放 if (_voiceSource != null) { _voiceSource.Stop(); if (line.voiceClip != null) { _voiceSource.clip = line.voiceClip; _voiceSource.Play(); } } // 开始打字机协程 if (_typingCoroutine != null) StopCoroutine(_typingCoroutine); _typingCoroutine = StartCoroutine(TypeLine(line)); } /// 立即显示全部文字(跳过打字机效果)。 public void SkipTyping() { if (!IsTyping) return; if (_typingCoroutine != null) { StopCoroutine(_typingCoroutine); _typingCoroutine = null; } _voiceSource?.Stop(); if (_dialogueText != null) _dialogueText.text = LocalizationManager.Get(_currentLine.textKey ?? "", "Dialogue"); IsTyping = false; if (_continuePrompt != null) _continuePrompt.SetActive(true); } /// 隐藏对话框面板。 public void Hide() { if (_rootPanel != null) _rootPanel.SetActive(false); } // ── 内部协程 ────────────────────────────────────────────────────── private IEnumerator TypeLine(DialogueLine line) { IsTyping = true; float delay = line.typewriterDelay > 0f ? line.typewriterDelay : DefaultTypewriterDelay; string text = LocalizationManager.Get(line.textKey ?? "", "Dialogue"); // 性能:StringBuilder 避免每帧字符串分配(O(n²) → O(n)) var sb = new StringBuilder(text.Length); if (_dialogueText != null) _dialogueText.text = ""; foreach (char c in text) { sb.Append(c); if (_dialogueText != null) _dialogueText.SetText(sb); // TMP SetText(StringBuilder) 零分配 yield return new WaitForSecondsRealtime(delay); } IsTyping = false; if (_continuePrompt != null) _continuePrompt.SetActive(true); _typingCoroutine = null; } } }