微调
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Ichni.Story.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Tables;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using Yarn.Unity;
|
||||
using Yarn.Unity.UnityLocalization;
|
||||
|
||||
@@ -25,10 +26,6 @@ namespace Ichni.Story.Dialogue
|
||||
[Tooltip("故事树页面:对话进行时淡出、结束后淡入(可选)。")]
|
||||
public StoryUIPage storyUIPage;
|
||||
|
||||
[Header("Localization")]
|
||||
[Tooltip("所有章节 Yarn 台词共用的 Unity Localization String Table(建议命名 YarnLines)。留空时保留当前 UnityLocalisedLineProvider 的既有表配置。")]
|
||||
public LocalizedStringTable yarnLinesTable;
|
||||
|
||||
/// <summary>
|
||||
/// 对话结束后依次执行并清空的回调队列。供 <c>unlock_song</c> 等 Yarn 命令
|
||||
/// 把"解锁提示"排队到对话结束后弹出。
|
||||
@@ -43,6 +40,9 @@ namespace Ichni.Story.Dialogue
|
||||
private ChapterStorySave _activeDialogueSnapshot;
|
||||
private string _activeDialogueChapterIndex;
|
||||
private bool _isCancellingDialogue;
|
||||
private Coroutine _dialogueStartupCoroutine;
|
||||
|
||||
private const float DialogueTableLoadTimeoutSeconds = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// 当前对话对应的 TextBlock ID。Yarn 选项记忆与变量指令使用它定位所属章节 Block,
|
||||
@@ -62,7 +62,6 @@ namespace Ichni.Story.Dialogue
|
||||
private void Awake()
|
||||
{
|
||||
instance = this;
|
||||
EnsureUnityLocalisedLineProvider();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -110,33 +109,41 @@ namespace Ichni.Story.Dialogue
|
||||
{
|
||||
if (block == null) return;
|
||||
|
||||
Debug.Log($"[YarnDiag] StoryDialogueController.PlayBlock: block={block.blockId}, node={block.YarnNodeName}");
|
||||
|
||||
if (dialogueRunner == null)
|
||||
{
|
||||
Debug.LogError("[StoryDialogueController] 未配置 DialogueRunner,无法开始对话。");
|
||||
Debug.LogError("[YarnDiag] [ERROR] 未配置 DialogueRunner,无法开始对话!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogueRunner.IsDialogueRunning)
|
||||
if (dialogueRunner.IsDialogueRunning || _dialogueStartupCoroutine != null)
|
||||
{
|
||||
Debug.LogWarning("[StoryDialogueController] 已有对话在进行中,忽略本次点击。");
|
||||
Debug.LogWarning("[YarnDiag] [WARNING] 已有对话正在运行或启动,忽略本次点击!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(block.YarnNodeName))
|
||||
{
|
||||
Debug.LogWarning($"[StoryDialogueController] block '{block.blockId}' 未配置 yarnNodeName,无法开始对话。");
|
||||
Debug.LogWarning($"[YarnDiag] [WARNING] block '{block.blockId}' 未配置 yarnNodeName,无法开始对话!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将 DialogueRunner 的 YarnProject 对齐到当前章节(支持多章节共用一个 Runner)
|
||||
// 将 DialogueRunner 的 YarnProject 对齐到当前章节,并验证场景中持久化的 LineProvider 配置。
|
||||
EnsureProjectForCurrentChapter();
|
||||
if (!ValidateUnityLocalisedLineProvider())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[YarnDiag] YarnProject={dialogueRunner.YarnProject?.name}, NodesCount={(dialogueRunner.YarnProject?.Program != null ? dialogueRunner.YarnProject.Program.Nodes.Count : 0)}, LineProviderType={dialogueRunner.LineProvider.GetType().FullName}");
|
||||
|
||||
// StartDialogue 对缺失 Node 只会在内部抛出/记录错误;必须在开启事务前预检,
|
||||
// 否则会留下一个没有机会提交或回滚的半开事务。
|
||||
if (dialogueRunner.YarnProject?.Program == null ||
|
||||
!dialogueRunner.YarnProject.Program.Nodes.ContainsKey(block.YarnNodeName))
|
||||
{
|
||||
Debug.LogError($"[StoryDialogueController] Yarn Project 中不存在节点 '{block.YarnNodeName}',已取消启动 Block '{block.blockId}'。", dialogueRunner);
|
||||
Debug.LogError($"[YarnDiag] [ERROR] Yarn Project 中不存在节点 '{block.YarnNodeName}',已取消启动 Block '{block.blockId}'!", dialogueRunner);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -164,35 +171,139 @@ namespace Ichni.Story.Dialogue
|
||||
// 在 Yarn 命令或选项改变章节变量前先登记来源,并为 Marker 所在列创建快照。
|
||||
StoryProgress.BeginBlockMutation(_activeBlockId);
|
||||
|
||||
if (storyUIPage != null)
|
||||
storyUIPage.FadeOut();
|
||||
|
||||
// 启动异常会回滚本次章节事务;正常结束仍由 onDialogueComplete 统一提交。
|
||||
StartDialogueSafely(block.YarnNodeName).Forget();
|
||||
// Unity Localization 的 AsyncOperationHandle 原生支持协程等待。
|
||||
// 先在项目侧显式加载当前章节台词表,再启动 Yarn;不修改 Yarn Spinner Package,
|
||||
// 也不把 Unity Addressables Handle 转换为 YarnTask。
|
||||
_dialogueStartupCoroutine = StartCoroutine(StartDialogueSafely(block.YarnNodeName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以受保护方式启动 Yarn。缺失 Provider、Presenter 等启动级异常不应让 StorySave 的事务永久处于活动状态,
|
||||
/// 因而会被转换为 Failed 流程并完整恢复进入对话前的章节快照。
|
||||
/// 使用 Unity Localization 的公开 API 加载当前章节台词表。
|
||||
/// <para>
|
||||
/// <see cref="UnityLocalisedLineProvider.PrepareForLinesAsync"/> 在 Yarn Spinner 3.2.1 中是 no-op,
|
||||
/// 不能用来预载 String Table;因此这里直接等待 <see cref="UnityEngine.Localization.LocalizedStringTable.GetTableAsync"/>。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private async YarnTask StartDialogueSafely(string yarnNodeName)
|
||||
private IEnumerator StartDialogueSafely(string yarnNodeName)
|
||||
{
|
||||
LocalizationBootstrap.BeginInitialization();
|
||||
while (!LocalizationBootstrap.IsReady && !LocalizationBootstrap.HasFailed)
|
||||
{
|
||||
LocalizationBootstrap.RefreshState();
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (!LocalizationBootstrap.IsReady)
|
||||
{
|
||||
FailDialogueStartup($"Localization 初始化失败:{LocalizationBootstrap.FailureReason}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
StoryData storyData = StoryManager.instance?.treeController?.ActiveStoryData;
|
||||
if (storyData?.yarnLineStringTable == null || storyData.yarnLineStringTable.IsEmpty)
|
||||
{
|
||||
FailDialogueStartup(
|
||||
$"章节 '{storyData?.chapterIndex ?? "<null>"}' 未配置 Yarn Line String Table。");
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<StringTable> tableOperation;
|
||||
//Debug.Log(storyData.yarnLineStringTable.GetTable());
|
||||
try
|
||||
{
|
||||
await dialogueRunner.StartDialogue(yarnNodeName);
|
||||
// LocalizedStringTable 会自动使用 LocalizationSettings.SelectedLocale。
|
||||
// 这与 UnityLocalisedLineProvider 后续读取台词时使用的 Locale 保持一致。
|
||||
Debug.Log($"[YarnDiag] 开始加载 Yarn 台词表。Table={storyData.yarnLineStringTable.TableReference}, Locale={UnityEngine.Localization.Settings.LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>"}");
|
||||
tableOperation = storyData.yarnLineStringTable.GetTableAsync();
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
Debug.LogException(exception, dialogueRunner);
|
||||
// 启动阶段可能已经让部分 Presenter 进入展示状态;先请求停止,再立即清理事务。
|
||||
// 后续若收到 Runner 的完成事件,会因 activeBlockId 已被清空而被安全忽略。
|
||||
if (dialogueRunner != null && dialogueRunner.IsDialogueRunning)
|
||||
{
|
||||
dialogueRunner.GetComponent<VNDialoguePresenter>()?.CancelCurrentPresentation();
|
||||
dialogueRunner.Stop().Forget();
|
||||
}
|
||||
RestoreInterruptedDialogue("启动 Yarn 对话失败");
|
||||
FailDialogueStartup("创建 Yarn 台词表加载操作失败。", exception);
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"[YarnDiag] 开始加载章节台词表。Chapter={storyData.chapterIndex}, " +
|
||||
$"Table={storyData.yarnLineStringTable.TableReference}, " +
|
||||
$"Locale={UnityEngine.Localization.Settings.LocalizationSettings.SelectedLocale?.Identifier.Code ?? "<null>"}");
|
||||
|
||||
float loadStartedAt = Time.realtimeSinceStartup;
|
||||
float nextProgressLogAt = loadStartedAt + 5f;
|
||||
while (!tableOperation.IsDone)
|
||||
{
|
||||
float elapsed = Time.realtimeSinceStartup - loadStartedAt;
|
||||
if (elapsed >= DialogueTableLoadTimeoutSeconds)
|
||||
{
|
||||
FailDialogueStartup(
|
||||
$"加载 Yarn 台词表超时。Table={storyData.yarnLineStringTable.TableReference}, " +
|
||||
$"Elapsed={elapsed:F1}s, Percent={tableOperation.PercentComplete:P0}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (Time.realtimeSinceStartup >= nextProgressLogAt)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[YarnDiag] Yarn 台词表仍在加载。Elapsed={elapsed:F1}s, " +
|
||||
$"Percent={tableOperation.PercentComplete:P0}");
|
||||
nextProgressLogAt += 5f;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (tableOperation.Status != AsyncOperationStatus.Succeeded || tableOperation.Result == null)
|
||||
{
|
||||
FailDialogueStartup(
|
||||
$"Yarn 台词表加载失败。Table={storyData.yarnLineStringTable.TableReference}",
|
||||
tableOperation.OperationException);
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log(
|
||||
$"[YarnDiag] 章节台词表加载完成。Locale={tableOperation.Result.LocaleIdentifier.Code}, " +
|
||||
$"Entries={tableOperation.Result.Count}");
|
||||
|
||||
_dialogueStartupCoroutine = null;
|
||||
storyUIPage?.FadeOut();
|
||||
StartDialogueRunnerSafely(yarnNodeName).Forget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用 Yarn Spinner 的公开 <see cref="DialogueRunner.StartDialogue"/> API。
|
||||
/// 返回时表示所有 Presenter 已完成 OnDialogueStartedAsync,且 Runner 已调用 Dialogue.Continue。
|
||||
/// </summary>
|
||||
private async YarnTask StartDialogueRunnerSafely(string yarnNodeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.Log($"[YarnDiag] 调用 DialogueRunner.StartDialogue。Node={yarnNodeName}");
|
||||
await dialogueRunner.StartDialogue(yarnNodeName);
|
||||
Debug.Log(
|
||||
$"[YarnDiag] DialogueRunner 初始启动完成。Node={yarnNodeName}, " +
|
||||
$"IsDialogueRunning={dialogueRunner.IsDialogueRunning}");
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
FailDialogueStartup("启动 Yarn 对话失败。", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void FailDialogueStartup(string reason, System.Exception exception = null)
|
||||
{
|
||||
_dialogueStartupCoroutine = null;
|
||||
|
||||
if (exception != null)
|
||||
Debug.LogException(exception, dialogueRunner);
|
||||
|
||||
Debug.LogError($"[YarnDiag] {reason}", dialogueRunner);
|
||||
|
||||
if (dialogueRunner != null && dialogueRunner.IsDialogueRunning)
|
||||
{
|
||||
dialogueRunner.GetComponent<VNDialoguePresenter>()?.CancelCurrentPresentation();
|
||||
dialogueRunner.Stop().Forget();
|
||||
}
|
||||
|
||||
RestoreInterruptedDialogue(reason);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -202,12 +313,22 @@ namespace Ichni.Story.Dialogue
|
||||
/// </summary>
|
||||
public bool CancelActiveDialogue()
|
||||
{
|
||||
if (_isCancellingDialogue || string.IsNullOrEmpty(_activeBlockId) ||
|
||||
dialogueRunner == null || !dialogueRunner.IsDialogueRunning)
|
||||
if (_isCancellingDialogue || string.IsNullOrEmpty(_activeBlockId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_dialogueStartupCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_dialogueStartupCoroutine);
|
||||
_dialogueStartupCoroutine = null;
|
||||
RestoreInterruptedDialogue("玩家在对话启动阶段中途退出");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dialogueRunner == null || !dialogueRunner.IsDialogueRunning)
|
||||
return false;
|
||||
|
||||
_isCancellingDialogue = true;
|
||||
_dialogueEndActions.Clear();
|
||||
dialogueRunner.GetComponent<VNDialoguePresenter>()?.CancelCurrentPresentation();
|
||||
@@ -246,44 +367,29 @@ namespace Ichni.Story.Dialogue
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DialogueRunner.lineProvider 为 [SerializeReference] internal 字段。
|
||||
/// Inspector 的 "Use Unity Localised Line Provider" 按钮有时无法将 MonoBehaviour
|
||||
/// 引用正确持久化到该字段;此方法在运行时用反射补救,确保使用正确的 Provider。
|
||||
/// 验证 DialogueRunner 序列化的 LineProvider 是否为 UnityLocalisedLineProvider。
|
||||
/// <para>
|
||||
/// LineProvider 应通过 Yarn Spinner Inspector 持久化配置;运行时不使用反射修改 Package 内部字段,
|
||||
/// 这样 Editor、Mono 与 IL2CPP Build 使用完全相同的装配结果。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void EnsureUnityLocalisedLineProvider()
|
||||
private bool ValidateUnityLocalisedLineProvider()
|
||||
{
|
||||
if (dialogueRunner == null) return;
|
||||
|
||||
FieldInfo field = typeof(DialogueRunner).GetField(
|
||||
"lineProvider",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
if (field == null) return;
|
||||
|
||||
UnityLocalisedLineProvider provider = field.GetValue(dialogueRunner) as UnityLocalisedLineProvider;
|
||||
provider ??= dialogueRunner.GetComponent<UnityLocalisedLineProvider>();
|
||||
|
||||
if (provider != null)
|
||||
if (dialogueRunner == null)
|
||||
{
|
||||
if (field.GetValue(dialogueRunner) != provider)
|
||||
{
|
||||
field.SetValue(dialogueRunner, provider);
|
||||
Debug.Log("[StoryDialogueController] Assigned UnityLocalisedLineProvider to DialogueRunner.lineProvider.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Yarn 的 Localized Line Provider 本身只持有一个 String Table 引用;统一使用 YarnLines
|
||||
// 可避免在切换章节时依赖反射替换不同表。未配置时不覆盖旧场景,便于分步迁移现有 Chapter0_Lines。
|
||||
if (yarnLinesTable != null && !yarnLinesTable.IsEmpty)
|
||||
{
|
||||
FieldInfo tableField = typeof(UnityLocalisedLineProvider).GetField(
|
||||
"stringsTable", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
tableField?.SetValue(provider, yarnLinesTable);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (dialogueRunner.LineProvider is UnityLocalisedLineProvider)
|
||||
{
|
||||
Debug.LogWarning("[StoryDialogueController] UnityLocalisedLineProvider not found on DialogueRunner's GameObject. Localized line text will be unavailable.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.LogError(
|
||||
"[StoryDialogueController] DialogueRunner.lineProvider 未配置为 UnityLocalisedLineProvider。" +
|
||||
"请在 Yarn Spinner Inspector 中使用 Unity Localization Line Provider,并配置台词 String Table。",
|
||||
dialogueRunner);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleDialogueComplete()
|
||||
|
||||
Reference in New Issue
Block a user