Files
zeling_v2/Assets/_Game/Scripts/Dialogue/DialogueUI.cs
Joywayer 446fd5dcd0 feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management
- Implemented WorldStateFlagAttribute to mark string fields as world state flags.
- Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states.
- Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector.
- Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors.
- Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars.
- Established QuestModule for managing QuestSO assets, including objectives and branches.
- Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
2026-05-24 00:36:11 +08:00

132 lines
5.5 KiB
C#
Raw 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.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Localization;
namespace BaseGames.Dialogue
{
/// <summary>
/// 对话框 UI 组件(架构 14_NarrativeModule §5
/// 挂载在 Canvas_Overlay 下的 DialogueBox 子对象。
/// 负责打字机效果、头像显示、继续提示。
/// </summary>
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;
// 缓存单个 WaitForSecondsRealtime避免 TypeLine 每字符 new 分配。
// 当 delay 值改变时才重新创建(不同行可能有不同打字速度)。
private WaitForSecondsRealtime _cachedTypeDelay;
private float _cachedTypeDelayValue = -1f;
/// <summary>当前是否仍在执行打字机效果。</summary>
public bool IsTyping { get; private set; }
// ── 公开 API ──────────────────────────────────────────────────────
/// <summary>显示一行对话并开始打字机效果。</summary>
public void ShowLine(DialogueLine line)
{
_currentLine = line;
if (_rootPanel != null) _rootPanel.SetActive(true);
if (_continuePrompt != null) _continuePrompt.SetActive(false);
// 说话人名称actor 优先,回退到直接字段)
string resolvedNameKey = line.ResolvedNameKey;
bool hasSpeaker = !string.IsNullOrEmpty(resolvedNameKey);
if (_speakerNamePanel != null) _speakerNamePanel.SetActive(hasSpeaker);
if (hasSpeaker && _speakerNameText != null)
_speakerNameText.text = LocalizationManager.Get(resolvedNameKey, "Dialogue");
// 头像actor 优先,回退到直接字段)
var resolvedPortrait = line.ResolvedPortrait;
if (_speakerPortrait != null)
{
_speakerPortrait.gameObject.SetActive(resolvedPortrait != null);
if (resolvedPortrait != null) _speakerPortrait.sprite = resolvedPortrait;
}
// 语音播放
if (_voiceSource != null)
{
_voiceSource.Stop();
if (line.voiceClip != null)
{
_voiceSource.clip = line.voiceClip;
_voiceSource.Play();
}
}
// 开始打字机协程
if (_typingCoroutine != null) StopCoroutine(_typingCoroutine);
_typingCoroutine = StartCoroutine(TypeLine(line));
}
/// <summary>立即显示全部文字(跳过打字机效果)。</summary>
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);
}
/// <summary>隐藏对话框面板。</summary>
public void Hide()
{
if (_rootPanel != null) _rootPanel.SetActive(false);
}
// ── 内部协程 ──────────────────────────────────────────────────────
private IEnumerator TypeLine(DialogueLine line)
{
IsTyping = true;
float delay = line.typewriterDelay > 0f ? line.typewriterDelay : DefaultTypewriterDelay;
// 复用缓存的 WaitForSecondsRealtime仅当 delay 值变化时才重新 new
if (_cachedTypeDelay == null || !Mathf.Approximately(_cachedTypeDelayValue, delay))
{
_cachedTypeDelay = new WaitForSecondsRealtime(delay);
_cachedTypeDelayValue = delay;
}
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 _cachedTypeDelay;
}
IsTyping = false;
if (_continuePrompt != null) _continuePrompt.SetActive(true);
_typingCoroutine = null;
}
}
}