- 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.
132 lines
5.5 KiB
C#
132 lines
5.5 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|