Files
Cielonos/Assets/Scripts/MainGame/Narrative/StoryDirector.cs
SoulliesOfficial 8186f54e90 新场景,剧情
2026-06-02 12:55:39 -04:00

208 lines
7.4 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;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using SLSUtilities.General;
using SLSUtilities.Narrative;
using Cielonos.UI;
using UnityEngine;
namespace Cielonos.MainGame.Narrative
{
/// <summary>
/// 全局剧情与对话路由器(单例)。
/// 所有触发源统一调用 StoryDirector.Instance.PlayStory(storyId) 来启动剧情。
/// </summary>
public class StoryDirector : Singleton<StoryDirector>
{
[Title("UI Pages")]
[Required]
[SerializeField] private DialogUIPage dialogUIPage;
private readonly Dictionary<string, NarrativeEntry> _entryLookup = new Dictionary<string, NarrativeEntry>();
// ── 事件 ──
public event Action OnDialogueStarted;
public event Action OnDialogueEnded;
/// <summary>当前是否有对话正在运行。</summary>
public bool IsDialogueActive => dialogUIPage != null && dialogUIPage.IsOpen;
protected override void Awake()
{
base.Awake();
}
private void Start()
{
InitializeEntries();
// 订阅 DialogUIPage 关闭事件以分发 OnDialogueEnded
if (dialogUIPage != null)
{
dialogUIPage.PageClosed += HandleDialogueClosed;
}
// 订阅通用剧情触发器事件,实现全局联动
NarrativeTrigger.OnNarrativeTriggerFired += PlayStory;
}
private void OnDestroy()
{
if (dialogUIPage != null)
{
dialogUIPage.PageClosed -= HandleDialogueClosed;
}
// 注销通用剧情触发器事件,防止内存泄漏
NarrativeTrigger.OnNarrativeTriggerFired -= PlayStory;
}
/// <summary>
/// 从全局剧情数据库中读取已注册的路由表,构建快速查找字典。
/// </summary>
[Button("重新加载路由注册表 (Reload Registry)", ButtonSizes.Small)]
public void InitializeEntries()
{
_entryLookup.Clear();
if (StorySystem.Instance == null || StorySystem.Database == null)
{
Debug.LogWarning("[StoryDirector] StorySystem 实例或 Database 未初始化,跳过路由加载。");
return;
}
if (StorySystem.Database.narrativeEntries == null)
{
Debug.LogWarning("[StoryDirector] 剧情路由表 (narrativeEntries) 列表为空。");
return;
}
foreach (var entry in StorySystem.Database.narrativeEntries)
{
if (entry == null) continue;
if (string.IsNullOrEmpty(entry.storyId))
{
Debug.LogWarning($"[StoryDirector] 存在未配置 Story ID 的 NarrativeEntry: {entry.name}");
continue;
}
if (!_entryLookup.TryAdd(entry.storyId, entry))
{
Debug.LogWarning($"[StoryDirector] 重复的 Story ID: '{entry.storyId}',已跳过资产: {entry.name}");
}
}
Debug.Log($"[StoryDirector] 成功初始化 {_entryLookup.Count} 个剧情路由入口。");
}
/// <summary>
/// 统一的对话启动入口。NPC、区域触发器、道具等交互源均通过此方法启动剧情。
/// </summary>
/// <param name="storyId">对应 NarrativeEntry 的 storyId 标识</param>
public void PlayStory(string storyId)
{
if (string.IsNullOrEmpty(storyId))
{
Debug.LogWarning("[StoryDirector] PlayStory 失败:传入的 storyId 为空。");
return;
}
if (IsDialogueActive)
{
Debug.LogWarning("[StoryDirector] 对话正在进行中,忽略新的 PlayStory 请求。");
return;
}
if (!_entryLookup.TryGetValue(storyId, out NarrativeEntry entry))
{
Debug.LogWarning($"[StoryDirector] 未找到 ID 为 '{storyId}' 的剧情路由表,尝试直接作为 Yarn 节点播放。");
PlayNode(storyId);
return;
}
// 1. 评估路由匹配(首个满足条件的路由)
string targetNode = string.Empty;
bool routeMatched = false;
foreach (var route in entry.routes)
{
if (route == null) continue;
if (NarrativeConditionEvaluator.Evaluate(route.conditions))
{
targetNode = route.targetNode;
routeMatched = true;
Debug.Log($"[StoryDirector] 剧情路由匹配成功!路由备注: '{route.editorNote}' -> 目标 Yarn 节点: '{targetNode}'");
break;
}
}
// 2. 如果路由均不满足,则使用 Fallback 兜底
if (!routeMatched)
{
targetNode = entry.fallbackNode;
Debug.Log($"[StoryDirector] 未匹配到任何满足的路由条件,使用 Fallback 兜底节点: '{targetNode}'");
}
// 3. 校验并播放目标节点
if (string.IsNullOrEmpty(targetNode))
{
Debug.LogWarning($"[StoryDirector] 剧情评估完毕,但无可播放的节点(未配置 Fallback且条件皆不满足。StoryId: {storyId}");
return;
}
PlayNode(targetNode);
}
/// <summary>
/// 绕过条件路由直接播放指定的 Yarn 节点名称(一般用于测试、特异流程或调试)。
/// </summary>
public void PlayNode(string yarnNode)
{
if (dialogUIPage == null)
{
Debug.LogError("[StoryDirector] PlayNode 失败DialogUIPage 引用未赋值!");
return;
}
if (IsDialogueActive)
{
Debug.LogWarning("[StoryDirector] 已经有对话正在进行中,忽略 PlayNode。");
return;
}
Debug.Log($"[StoryDirector] 开始播放剧情节点: '{yarnNode}'");
OnDialogueStarted?.Invoke();
// 打开 UI 页面并引导其启动特定节点
dialogUIPage.Open(yarnNode);
}
private void HandleDialogueClosed()
{
Debug.Log("[StoryDirector] 对话 UI 已关闭,结束当前剧情。");
OnDialogueEnded?.Invoke();
}
#if UNITY_EDITOR
[Title("调试与验证 (Debug & Test)", titleAlignment: TitleAlignments.Centered)]
[BoxGroup("调试测试")]
[HorizontalGroup("调试测试/Group")]
[HideLabel]
[Tooltip("输入要测试的 NarrativeEntry 的 storyId")]
[InlineButton("TestPlayStory", "测试播放")]
[SerializeField] private string testStoryId;
[HorizontalGroup("调试测试/Group", Width = 120)]
[GUIColor(0.4f, 0.9f, 0.4f)]
public void TestPlayStory()
{
if (string.IsNullOrEmpty(testStoryId))
{
Debug.LogWarning("[StoryDirector] 请在文本框中输入要测试的 storyId");
return;
}
PlayStory(testStoryId);
}
#endif
}
}