Files
ichni_Official/Assets/Scripts/NewStorySystem/Dialogue/VNDialoguePresenter.cs
SoulliesOfficial 7b7d069b84 StorySystem
2026-07-07 01:28:27 -04:00

273 lines
10 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.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Yarn.Unity;
using Ichni.Story.UI;
namespace Ichni.Story.Dialogue
{
/// <summary>
/// 视觉小说VN风格的 Yarn 对话呈现器。
/// <para>
/// 职责:
/// <list type="bullet">
/// <item>对话流控制:打字机效果、点击推进、等待选项。</item>
/// <item>将所有的 UI 引用委托给 <see cref="DialogUIPage"/> 进行管理。</item>
/// </list>
/// </para>
/// </summary>
public class VNDialoguePresenter : DialoguePresenterBase
{
[Header("UI 管理")]
[Tooltip("负责当前对话界面的 UI Page 容器。如果为空,将尝试获取全局单例。")]
[SerializeField] private DialogUIPage uiPage;
[Header("打字效果")]
[Tooltip("每秒显示的字符数。设为 0 则瞬间显示全文。")]
[SerializeField] private float lettersPerSecond = 40f;
// ── 私有状态 ─────────────────────────────────────────────────────────────
/// <summary>
/// 当前行的打字效果是否已完成。
/// 用于区分"加速打字(第一次点击)"与"推进到下一行(第二次点击)"两个阶段。
/// </summary>
private bool _typewriterComplete;
/// <summary>
/// 是否处于快进状态(遇到选项或玩家再次点击时会打断)。
/// </summary>
private bool _isFastForwarding;
/// <summary>
/// 选项选择的异步结果来源。RunOptionsAsync 等待其 Task 完成,
/// 点击按钮时 TrySetResult 触发完成。
/// </summary>
private YarnTaskCompletionSource<DialogueOption?> _optionTcs;
/// <summary>
/// 对应 StoryDialogueRoot 上的 DialogueRunner。
/// </summary>
private DialogueRunner _runner;
private DialogUIPage Page => uiPage != null ? uiPage : DialogUIPage.instance;
// ── 生命周期 ─────────────────────────────────────────────────────────────
private void Awake()
{
_runner = GetComponent<DialogueRunner>();
if (_runner == null)
Debug.LogWarning("[VNDialoguePresenter] 未在同 GameObject 上找到 DialogueRunner。" +
"请确认 VNDialoguePresenter 挂载在 StoryDialogueRoot 上。");
}
private void Start()
{
if (Page == null) return;
// 启动时隐藏选项容器
if (Page.choiceFrame != null) Page.choiceFrame.SetActive(false);
// 注册推进按钮回调
if (Page.advanceButton != null)
Page.advanceButton.onClick.AddListener(OnAdvanceButtonClicked);
// 注册快进按钮回调
if (Page.fastForwardButton != null)
Page.fastForwardButton.onClick.AddListener(OnFastForwardButtonClicked);
}
private void OnDestroy()
{
if (Page != null)
{
if (Page.advanceButton != null)
Page.advanceButton.onClick.RemoveListener(OnAdvanceButtonClicked);
if (Page.fastForwardButton != null)
Page.fastForwardButton.onClick.RemoveListener(OnFastForwardButtonClicked);
}
}
// ── DialoguePresenterBase 实现 ───────────────────────────────────────────
public override async YarnTask OnDialogueStartedAsync()
{
if (Page != null)
{
var tcs = new YarnTaskCompletionSource<bool>();
Page.PlayFadeIn(() => tcs.TrySetResult(true));
await tcs.Task;
}
}
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
if (Page == null) return;
// 确保选项区不可见;台词区始终可见
if (Page.choiceFrame != null) Page.choiceFrame.SetActive(false);
// ── 说话者名称 ──
bool hasSpeaker = !string.IsNullOrEmpty(line.CharacterName);
if (Page.speakerContainer != null) Page.speakerContainer.SetActive(hasSpeaker);
if (Page.speakerText != null) Page.speakerText.text = hasSpeaker ? line.CharacterName : string.Empty;
// ── 立绘高亮 ──
Page.portraitStage?.HighlightSpeaker(line.CharacterName);
// ── 打字效果 ──
string fullText = line.TextWithoutCharacterName.Text;
_typewriterComplete = false;
if (Page.dialogueText != null)
{
Page.dialogueText.text = fullText;
Page.dialogueText.maxVisibleCharacters = 0;
}
if (_isFastForwarding)
{
// 瞬间显示全文
if (Page.dialogueText != null) Page.dialogueText.maxVisibleCharacters = int.MaxValue;
_typewriterComplete = true;
// 极短的延迟,防止瞬间刷过几百句话造成卡顿,也给玩家微小的视觉感知
await YarnTask.Delay(50).SuppressCancellationThrow();
if (_isFastForwarding && _runner != null)
{
_runner.RequestNextLine();
}
}
else
{
// 正常逐字播放
await TypewriterAsync(fullText, token.HurryUpToken);
// 确保全文可见
if (Page.dialogueText != null)
Page.dialogueText.maxVisibleCharacters = int.MaxValue;
_typewriterComplete = true;
}
// ── 等待玩家推进 ──
await YarnTask.WaitUntilCanceled(token.NextContentToken).SuppressCancellationThrow();
}
public override async YarnTask<DialogueOption?> RunOptionsAsync(
DialogueOption[] options,
LineCancellationToken token)
{
if (Page == null) return null;
// 遇到选项强制打断快进
_isFastForwarding = false;
// 清空台词区,准备显示选项
if (Page.speakerContainer != null) Page.speakerContainer.SetActive(false);
if (Page.dialogueText != null) Page.dialogueText.text = string.Empty;
// 显示选项容器
if (Page.choiceFrame != null) Page.choiceFrame.SetActive(true);
_optionTcs = new YarnTaskCompletionSource<DialogueOption?>();
WaitForExternalCancelAsync(token.NextContentToken).Forget();
// ── 配置每个选项按钮 ──
for (int i = 0; i < Page.choiceButtons.Length; i++)
{
if (Page.choiceButtons[i] == null) continue;
if (i < options.Length)
Page.choiceButtons[i].Setup(options[i], opt => _optionTcs.TrySetResult(opt));
else
Page.choiceButtons[i].Cleanup();
}
// ── 等待玩家选择 ──
DialogueOption? selected = await _optionTcs.Task;
// 清理所有按钮并隐藏选项容器
foreach (ChoiceButton btn in Page.choiceButtons)
btn?.Cleanup();
if (Page.choiceFrame != null) Page.choiceFrame.SetActive(false);
return selected;
}
public override async YarnTask OnDialogueCompleteAsync()
{
if (Page != null)
{
var tcs = new YarnTaskCompletionSource<bool>();
Page.PlayFadeOut(() => tcs.TrySetResult(true));
await tcs.Task;
}
}
// ── 内部方法 ─────────────────────────────────────────────────────────────
private void OnAdvanceButtonClicked()
{
// 如果玩家在快进时点击屏幕/推进键,则打断快进状态并停止当前动作
if (_isFastForwarding)
{
_isFastForwarding = false;
return;
}
if (_runner == null) return;
if (!_typewriterComplete)
_runner.RequestHurryUpLine(); // 第一次点击:跳过打字效果
else
_runner.RequestNextLine(); // 第二次点击:推进到下一行
}
private void OnFastForwardButtonClicked()
{
_isFastForwarding = true;
if (_runner == null) return;
// 立即跳过当前的等待状态
if (!_typewriterComplete)
_runner.RequestHurryUpLine();
else
_runner.RequestNextLine();
}
private async YarnTask TypewriterAsync(string text, CancellationToken hurryUpToken)
{
if (Page.dialogueText == null) return;
if (lettersPerSecond <= 0f)
{
Page.dialogueText.maxVisibleCharacters = int.MaxValue;
return;
}
int delayMs = Mathf.Max(1, Mathf.RoundToInt(1000f / lettersPerSecond));
for (int i = 1; i <= text.Length; i++)
{
if (hurryUpToken.IsCancellationRequested) return;
Page.dialogueText.maxVisibleCharacters = i;
await YarnTask.Delay(delayMs, hurryUpToken).SuppressCancellationThrow();
if (hurryUpToken.IsCancellationRequested) return;
}
}
private async YarnTask WaitForExternalCancelAsync(CancellationToken externalToken)
{
await YarnTask.WaitUntilCanceled(externalToken).SuppressCancellationThrow();
_optionTcs?.TrySetResult(null);
}
}
}