2026-07-05 16:08:23 -04:00
using System.Collections.Generic ;
using System.Reflection ;
using Ichni.Story.UI ;
using UnityEngine ;
using UnityEngine.Events ;
using Yarn.Unity ;
using Yarn.Unity.UnityLocalization ;
namespace Ichni.Story.Dialogue
{
/// <summary>
/// 故事树 block 与 Yarn <see cref="DialogueRunner"/> 之间的桥接控制器。
/// <para>点击文本块 → 启动对应 Yarn 节点;对话结束 → 持久化剧情变量、标记 block 完成、
/// 重算解锁并存档、依次执行结束回调(如歌曲解锁提示,阶段 4 使用)。</para>
/// </summary>
public class StoryDialogueController : MonoBehaviour
{
public static StoryDialogueController instance ;
[Header("References")]
[Tooltip("驱动 Yarn 对话的 DialogueRunner。")]
public DialogueRunner dialogueRunner ;
[Tooltip("故事树页面:对话进行时淡出、结束后淡入(可选)。")]
public StoryUIPage storyUIPage ;
/// <summary>
/// 对话结束后依次执行并清空的回调队列。供 <c>unlock_song</c> 等 Yarn 命令(阶段 4)
/// 把"解锁提示"排队到对话结束后弹出。
/// </summary>
[System.NonSerialized] public List < UnityAction > dialogueEndActions = new List < UnityAction > ( ) ;
// 当前正在播放对话的 block id; 对话结束时据此标记完成
private string _activeBlockId ;
private void Awake ( )
{
instance = this ;
EnsureUnityLocalisedLineProvider ( ) ;
}
private void OnEnable ( )
{
if ( dialogueRunner ! = null & & dialogueRunner . onDialogueComplete ! = null )
dialogueRunner . onDialogueComplete . AddListener ( HandleDialogueComplete ) ;
}
private void OnDisable ( )
{
if ( dialogueRunner ! = null & & dialogueRunner . onDialogueComplete ! = null )
dialogueRunner . onDialogueComplete . RemoveListener ( HandleDialogueComplete ) ;
}
/// <summary>
/// 播放某文本块对应的 Yarn 节点。若对话已在进行、引用缺失或节点名为空则忽略。
/// </summary>
public void PlayBlock ( TextBlockView block )
{
if ( block = = null ) return ;
if ( dialogueRunner = = null )
{
Debug . LogError ( "[StoryDialogueController] 未配置 DialogueRunner, 无法开始对话。" ) ;
return ;
}
if ( dialogueRunner . IsDialogueRunning )
{
Debug . LogWarning ( "[StoryDialogueController] 已有对话在进行中,忽略本次点击。" ) ;
return ;
}
if ( string . IsNullOrEmpty ( block . YarnNodeName ) )
{
Debug . LogWarning ( $"[StoryDialogueController] block '{block.blockId}' 未配置 yarnNodeName, 无法开始对话。" ) ;
return ;
}
// 将 DialogueRunner 的 YarnProject 对齐到当前章节(支持多章节共用一个 Runner)
EnsureProjectForCurrentChapter ( ) ;
_activeBlockId = block . blockId ;
if ( storyUIPage ! = null )
storyUIPage . FadeOut ( ) ;
// StartDialogue 返回 YarnTask; 此处即发即忘, 完成由 onDialogueComplete 处理
dialogueRunner . StartDialogue ( block . YarnNodeName ) . Forget ( ) ;
}
// 将 DialogueRunner 的 YarnProject 切换为当前已构建章节的 StoryData.yarnProject( 若不同且未在运行)
private void EnsureProjectForCurrentChapter ( )
{
StoryTreeController tree = StoryManager . instance ! = null ? StoryManager . instance . treeController : null ;
StoryData data = tree ! = null ? tree . ActiveStoryData : null ;
if ( data = = null | | data . yarnProject = = null ) return ;
if ( dialogueRunner . YarnProject ! = data . yarnProject & & ! dialogueRunner . IsDialogueRunning )
dialogueRunner . SetProject ( data . yarnProject ) ;
}
/// <summary>
/// DialogueRunner.lineProvider 为 [SerializeReference] internal 字段。
/// Inspector 的 "Use Unity Localised Line Provider" 按钮有时无法将 MonoBehaviour
/// 引用正确持久化到该字段;此方法在运行时用反射补救,确保使用正确的 Provider。
/// </summary>
private void EnsureUnityLocalisedLineProvider ( )
{
if ( dialogueRunner = = null ) return ;
FieldInfo field = typeof ( DialogueRunner ) . GetField (
"lineProvider" ,
BindingFlags . NonPublic | BindingFlags . Instance ) ;
if ( field = = null ) return ;
// 已经是 UnityLocalisedLineProvider 则无需处理
if ( field . GetValue ( dialogueRunner ) is UnityLocalisedLineProvider ) return ;
UnityLocalisedLineProvider provider =
dialogueRunner . GetComponent < UnityLocalisedLineProvider > ( ) ;
if ( provider ! = null )
{
field . SetValue ( dialogueRunner , provider ) ;
Debug . Log ( "[StoryDialogueController] Assigned UnityLocalisedLineProvider to DialogueRunner.lineProvider." ) ;
}
else
{
Debug . LogWarning ( "[StoryDialogueController] UnityLocalisedLineProvider not found on DialogueRunner's GameObject. Localized line text will be unavailable." ) ;
}
}
private void HandleDialogueComplete ( )
{
// 持久化对话过程中写入的全局剧情变量
if ( GameSaveManager . instance ! = null & & GameSaveManager . instance . StorySaveModule ! = null )
GameSaveManager . instance . StorySaveModule . SaveVariables ( ) ;
// 标记 block 完成 → 重算解锁 → 存档( OnBlockCompleted 内部已去重并保存章节进度)
if ( ! string . IsNullOrEmpty ( _activeBlockId ) & &
StoryManager . instance ! = null & & StoryManager . instance . treeController ! = null )
{
StoryManager . instance . treeController . OnBlockCompleted ( _activeBlockId ) ;
}
_activeBlockId = null ;
// 依次执行并清空结束回调(如歌曲解锁提示)
if ( dialogueEndActions . Count > 0 )
{
List < UnityAction > pending = new List < UnityAction > ( dialogueEndActions ) ;
dialogueEndActions . Clear ( ) ;
foreach ( UnityAction action in pending )
2026-07-07 01:28:27 -04:00
{
2026-07-05 16:08:23 -04:00
action ? . Invoke ( ) ;
2026-07-07 01:28:27 -04:00
}
StoryMessageBoxUIPage . instance . FadeIn ( ) ;
2026-07-05 16:08:23 -04:00
}
if ( storyUIPage ! = null )
2026-07-07 01:28:27 -04:00
{
2026-07-05 16:08:23 -04:00
storyUIPage . FadeIn ( ) ;
2026-07-07 01:28:27 -04:00
}
2026-07-05 16:08:23 -04:00
}
}
}