feat(dialog): 升级Dialog系统到YarnSpinner 3.2

This commit is contained in:
2026-04-23 11:55:43 +08:00
parent cbae4cdb22
commit b447d4784c
9 changed files with 389 additions and 136 deletions
+14 -8
View File
@@ -2762,9 +2762,13 @@ MonoBehaviour:
m_GameObject: {fileID: 331068878}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8d3a78e02d644e96a9e0caa46f1e442b, type: 3}
m_Script: {fileID: 11500000, guid: a1b2c3d4e5f678901234567890123456, type: 3}
m_Name:
m_EditorClassIdentifier:
_textLocaleCode: zh-Hans
_assetLocaleCode: zh-Hans
_useFallback: 1
_fallbackLocaleCode: zh-Hans
--- !u!1 &343980037
GameObject:
m_ObjectHideFlags: 0
@@ -12746,15 +12750,17 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
yarnProject: {fileID: 0}
_variableStorage: {fileID: 314243163}
dialogueViews:
variableStorage: {fileID: 314243163}
lineProvider: {fileID: 331068880}
saliencyStrategy: 0
dialoguePresenters:
- {fileID: 1829169897}
- {fileID: 381795663}
startNode: Start
startAutomatically: 0
runSelectedOptionAsLine: 0
lineProvider: {fileID: 331068880}
verboseLogging: 1
autoStart: 0
startNode: Start
runSelectedOptionAsLine: 0
allowOptionFallthrough: 1
onNodeStart:
m_PersistentCalls:
m_Calls: []
@@ -12767,7 +12773,7 @@ MonoBehaviour:
onDialogueComplete:
m_PersistentCalls:
m_Calls: []
onCommand:
onUnhandledCommand:
m_PersistentCalls:
m_Calls: []
--- !u!1 &1937938009
@@ -0,0 +1,188 @@
using System.Collections.Generic;
using System.Threading;
using AibisDream.Framework;
using AibisDream.Utility;
using UnityEngine;
using Yarn.Unity;
using Yarn.Unity.Attributes;
namespace AibisDream
{
public class AibisLineProvider : LineProviderBehaviour
{
public override string LocaleCode
{
get => _textLocaleCode;
set => _textLocaleCode = value;
}
[SerializeField, Language]
private string _textLocaleCode = System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
[SerializeField, Language]
private string _assetLocaleCode = System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
[SerializeField]
private bool _useFallback = false;
[ShowIf(nameof(_useFallback))]
[SerializeField, Language]
private string _fallbackLocaleCode = System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
public string AssetLocaleCode
{
get => _assetLocaleCode;
set => _assetLocaleCode = value;
}
private Yarn.Markup.LineParser lineParser = new Yarn.Markup.LineParser();
private Yarn.Markup.BuiltInMarkupReplacer builtInReplacer = new Yarn.Markup.BuiltInMarkupReplacer();
void Awake()
{
lineParser.RegisterMarkerProcessor("select", builtInReplacer);
lineParser.RegisterMarkerProcessor("plural", builtInReplacer);
lineParser.RegisterMarkerProcessor("ordinal", builtInReplacer);
}
public override async YarnTask<LocalizedLine> GetLocalizedLineAsync(Yarn.Line line, CancellationToken cancellationToken)
{
string sourceLineID = line.ID;
string[] metadata = System.Array.Empty<string>();
if (YarnProject != null)
{
metadata = YarnProject.lineMetadata?.GetMetadata(line.ID) ?? System.Array.Empty<string>();
var shadowLineSource = YarnProject.lineMetadata?.GetShadowLineSource(line.ID);
if (shadowLineSource != null)
{
sourceLineID = shadowLineSource;
}
}
string text = GetLocalizedString(sourceLineID);
Object asset = await GetLocalizedAssetAsync(sourceLineID);
if (text == null)
{
Debug.LogWarning($"Localization {LocaleCode} does not contain an entry for line {line.ID}", this);
return LocalizedLine.InvalidLine;
}
// 对 substitutions 做本地化解析(保留旧 LocalisedLineProvider 的能力)
for (int i = 0; i < line.Substitutions.Length; i++)
{
line.Substitutions[i] = LocalizationKit.LocalizeParam(line.Substitutions[i]);
}
// 保留旧 LocalisedLineProvider 的文本转义处理
text = CommonUtil.Escape(text);
var parseResult = lineParser.ParseString(Yarn.Markup.LineParser.ExpandSubstitutions(text, line.Substitutions), LocaleCode);
return new LocalizedLine
{
Text = parseResult,
RawText = text,
TextID = line.ID,
Asset = asset,
Metadata = metadata,
};
}
private Yarn.Unity.Localization GetLocalization(string locale)
{
if (YarnProject == null)
{
throw new System.InvalidOperationException("Can't get localized line: no Yarn Project set");
}
Localization loc = YarnProject.GetLocalization(locale);
if (loc == null)
{
throw new System.InvalidOperationException($"Can't get localized line: Yarn Project has no localisation for {locale}");
}
return loc;
}
private string GetLocalizedString(string sourceLineID)
{
var baseLoc = GetLocalization(_textLocaleCode);
string localizedText = baseLoc.GetLocalizedString(sourceLineID);
if (localizedText != null)
{
return localizedText;
}
if (_useFallback)
{
var fallbackLoc = GetLocalization(_fallbackLocaleCode);
return fallbackLoc.GetLocalizedString(sourceLineID);
}
return null;
}
private async YarnTask<Object> GetLocalizedAssetAsync(string sourceLineID)
{
var baseLoc = GetLocalization(_assetLocaleCode);
Object result = await baseLoc.GetLocalizedObjectAsync<Object>(sourceLineID);
if (result != null)
{
return result;
}
if (_useFallback)
{
var fallbackLoc = GetLocalization(_fallbackLocaleCode);
return await fallbackLoc.GetLocalizedObjectAsync<Object>(sourceLineID);
}
return null;
}
public async override YarnTask PrepareForLinesAsync(IEnumerable<string> lineIDs, CancellationToken cancellationToken)
{
if (YarnProject == null)
{
return;
}
var assetLocalization = YarnProject.GetLocalization(AssetLocaleCode);
if (assetLocalization.UsesAddressableAssets)
{
var tasks = new List<YarnTask<Object>>();
foreach (var id in lineIDs)
{
var task = assetLocalization.GetLocalizedObjectAsync<Object>(id);
tasks.Add(task);
}
await YarnTask.WhenAll(tasks);
}
else
{
return;
}
}
public override void RegisterMarkerProcessor(string attributeName, Yarn.Markup.IAttributeMarkerProcessor markerProcessor)
{
lineParser.RegisterMarkerProcessor(attributeName, markerProcessor);
}
public override void DeregisterMarkerProcessor(string attributeName)
{
lineParser.DeregisterMarkerProcessor(attributeName);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f678901234567890123456
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -2,21 +2,23 @@
using AibisDream.Framework;
using AibisDream.Kit;
using UnityEngine;
using UnityEngine.Events;
using Yarn.Unity;
namespace AibisDream
{
public class DialogController : Singleton<DialogController>
{
private const string DefaultDialogueLocaleCode = "zh-Hans";
private DialogueRunner _dialogueRunner;
private LocalisedLineProvider _lineProvider;
[SerializeField] private LineAdvanceInput lineAdvanceInput;
private void Start()
{
_dialogueRunner = GetComponentInChildren<DialogueRunner>();
_lineProvider = GetComponentInChildren<LocalisedLineProvider>();
_dialogueRunner.onDialogueStart ??= new UnityEvent();
_dialogueRunner.onDialogueComplete ??= new UnityEvent();
_dialogueRunner.onDialogueStart.AddListener(() =>
{
@@ -72,13 +74,13 @@ namespace AibisDream
public void StartDialog(YarnProject yarnProject)
{
_dialogueRunner.SetProject(yarnProject);
_lineProvider.InitStringTable(yarnProject.name);
_dialogueRunner.StartDialogue("Start");
ConfigureLineProviderLocale();
_ = _dialogueRunner.StartDialogue("Start");
}
public void StopDialog()
{
_dialogueRunner.Stop();
_ = _dialogueRunner.Stop();
StorageSystem.Instance.Clear();
DialogUIManager.Instance.HideDialog();
}
@@ -86,8 +88,8 @@ namespace AibisDream
public void LoadDialog(YarnProject yarnProject)
{
_dialogueRunner.SetProject(yarnProject);
_lineProvider.InitStringTable(yarnProject.name);
_dialogueRunner.StartDialogue("Center");
ConfigureLineProviderLocale();
_ = _dialogueRunner.StartDialogue("Center");
}
public void StartDialogNode(string nodeName)
@@ -101,7 +103,7 @@ namespace AibisDream
if (CheckIfNodeExistInCurrentYarnProject(nodeName))
{
_dialogueRunner.StartDialogue(nodeName);
_ = _dialogueRunner.StartDialogue(nodeName);
}
else
{
@@ -111,7 +113,13 @@ namespace AibisDream
private bool CheckIfNodeExistInCurrentYarnProject(string nodeName)
{
YarnProject currentProject = _dialogueRunner.yarnProject;
var currentProject = _dialogueRunner.YarnProject;
if (currentProject == null)
{
Debug.LogWarning("当前没有可用的 YarnProject,无法检查节点");
return false;
}
string[] nodeNames = currentProject.NodeNames;
if (Array.Exists(nodeNames, item => item == nodeName))
{
@@ -124,6 +132,14 @@ namespace AibisDream
}
}
private void ConfigureLineProviderLocale()
{
if (_dialogueRunner.LineProvider is LineProviderBehaviour lineProvider)
{
lineProvider.LocaleCode = DefaultDialogueLocaleCode;
}
}
#endregion
#region
+75 -28
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using AibisDream.Utility;
using UnityEngine;
@@ -6,22 +5,79 @@ using Yarn.Unity;
namespace AibisDream
{
public class LineRunner : DialogueViewBase
public class LineRunner : DialoguePresenterBase
{
[SerializeField] private LineAdvanceInput advanceInput;
[SerializeField] private DialogViewType defaultViewType = DialogViewType.StandardBubble;
private readonly Dictionary<DialogViewType, ILineView> _screenViewDict = new();
public override void RunLine(LocalizedLine dialogueLine, Action onDialogueLineFinished)
public override YarnTask OnDialogueStartedAsync()
{
LineSyncToken lineSyncEvent;
return YarnTask.CompletedTask;
}
public override YarnTask OnDialogueCompleteAsync()
{
HideDialog();
return YarnTask.CompletedTask;
}
public override async YarnTask RunLineAsync(LocalizedLine dialogueLine, LineCancellationToken token)
{
var lineSyncEvent = BuildSyncToken(dialogueLine);
DialogController.Instance.lineSyncToken = lineSyncEvent;
DispatchToView(dialogueLine, lineSyncEvent);
// 最后交给推进处理
advanceInput.RunLine(lineSyncEvent);
using var skipReg = token.HurryUpToken.Register(() =>
{
if (lineSyncEvent.state == LineSyncState.PlayText)
{
lineSyncEvent.TrySkip();
}
});
using var advanceReg = token.NextContentToken.Register(() =>
{
if (lineSyncEvent.state != LineSyncState.Advanced)
{
lineSyncEvent.ForceAdvance();
}
});
await lineSyncEvent.AwaitCompletion();
}
private LineSyncToken BuildSyncToken(LocalizedLine dialogueLine)
{
if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Screen)
{
// 自动推进视图
lineSyncEvent = LineSyncToken.CreateAuto(dialogueLine, onDialogueLineFinished);
DialogController.Instance.lineSyncToken = lineSyncEvent;
return LineSyncToken.CreateAuto(dialogueLine);
}
if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Task)
{
return LineSyncToken.CreateImmediate(dialogueLine);
}
if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Default)
{
// 手动推进视图
return LineSyncToken.CreateDefault(dialogueLine);
}
// 指定了视图类型
return LineSyncToken.CreateDefault(dialogueLine);
}
private void DispatchToView(LocalizedLine dialogueLine, LineSyncToken lineSyncEvent)
{
if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Screen)
{
if (_screenViewDict.TryGetValue(DialogViewType.Screen, out var view))
{
view.RunLine(lineSyncEvent);
@@ -30,11 +86,11 @@ namespace AibisDream
{
Debug.LogWarning($"未注册的视图类型: {dialogueLine.GetCharacterVo().dialogViewType}");
}
return;
}
else if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Task)
if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Task)
{
lineSyncEvent = LineSyncToken.CreateImmediate(dialogueLine, onDialogueLineFinished);
DialogController.Instance.lineSyncToken = lineSyncEvent;
if (_screenViewDict.TryGetValue(DialogViewType.Task, out var view))
{
view.RunLine(lineSyncEvent);
@@ -43,39 +99,30 @@ namespace AibisDream
{
Debug.LogWarning($"未注册的视图类型: {dialogueLine.GetCharacterVo().dialogViewType}");
}
return;
}
else if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Default)
{
// 手动推进视图
lineSyncEvent = LineSyncToken.CreateDefault(dialogueLine, onDialogueLineFinished);
if (dialogueLine.GetCharacterVo().dialogViewType == DialogViewType.Default)
{
if (_screenViewDict.TryGetValue(defaultViewType, out var view))
{
DialogController.Instance.lineSyncToken = lineSyncEvent;
view.RunLine(lineSyncEvent);
}
else
{
Debug.LogWarning($"未注册的视图类型: {defaultViewType}");
}
return;
}
if (_screenViewDict.TryGetValue(dialogueLine.GetCharacterVo().dialogViewType, out var specificView))
{
specificView.RunLine(lineSyncEvent);
}
else
{
// 指定了视图类型
lineSyncEvent = LineSyncToken.CreateDefault(dialogueLine, onDialogueLineFinished);
if (_screenViewDict.TryGetValue(dialogueLine.GetCharacterVo().dialogViewType, out var view))
{
DialogController.Instance.lineSyncToken = lineSyncEvent;
view.RunLine(lineSyncEvent);
}
else
{
Debug.LogWarning($"未注册的视图类型: {dialogueLine.GetCharacterVo().dialogViewType}");
}
Debug.LogWarning($"未注册的视图类型: {dialogueLine.GetCharacterVo().dialogViewType}");
}
// 最后交给推进处理
advanceInput.RunLine(lineSyncEvent);
}
public void LoadBubbleData(BubbleSlotGroupData data)
@@ -14,6 +14,7 @@ namespace AibisDream
{
private Action _nextStep;
private readonly ITextShown _textShown;
private readonly TaskCompletionSource<bool> _completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
public LineInfo lineInfo;
public LineSyncState state;
@@ -27,11 +28,21 @@ namespace AibisDream
return new LineSyncToken(lineInfo, nextStep, new AutoNextTextShown());
}
public static LineSyncToken CreateAuto(LocalizedLine lineInfo)
{
return new LineSyncToken(lineInfo, null, new AutoNextTextShown());
}
public static LineSyncToken CreateImmediate(LocalizedLine lineInfo, Action nextStep)
{
return new LineSyncToken(lineInfo, nextStep, new ImmediateTextShown());
}
public static LineSyncToken CreateImmediate(LocalizedLine lineInfo)
{
return new LineSyncToken(lineInfo, null, new ImmediateTextShown());
}
public static LineSyncToken CreateDefault(LocalizedLine lineInfo, Action nextStep)
{
// 如果标记了 auto_next,使用自动推进逻辑
@@ -42,6 +53,16 @@ namespace AibisDream
return new LineSyncToken(lineInfo, nextStep, new DefaultTextShown());
}
public static LineSyncToken CreateDefault(LocalizedLine lineInfo)
{
// 如果标记了 auto_next,使用自动推进逻辑
if (lineInfo.IsAutoSkipLine())
{
return new LineSyncToken(lineInfo, null, new AutoNextTextShown());
}
return new LineSyncToken(lineInfo, null, new DefaultTextShown());
}
public LineSyncToken(LocalizedLine lineInfo, Action nextStep, ITextShown textShown)
{
this.lineInfo = LineInfo.Generate(lineInfo);
@@ -96,6 +117,7 @@ namespace AibisDream
OnAdvance = null;
_nextStep?.Invoke();
state = LineSyncState.Advanced;
_completion.TrySetResult(true);
}
private void Dispose()
@@ -121,6 +143,11 @@ namespace AibisDream
state = LineSyncState.PlayText;
EnumEventSystem.Global.Send(DialogEventEnum.LineStart, lineInfo);
}
public Task AwaitCompletion()
{
return _completion.Task;
}
}
public struct LineInfo
@@ -1,84 +0,0 @@
using System.Linq;
using UnityEngine.Localization.Settings;
using Yarn.Unity;
using AibisDream.Framework;
using AibisDream.Utility;
namespace AibisDream
{
public class LocalisedLineProvider : LineProviderBehaviour
{
private LocalizationTable _localizationTable;
public override string LocaleCode => LocalizationSettings.SelectedLocale.Identifier.Code;
// 本地化表存在即可用
public override bool LinesAvailable => _localizationTable != null;
public override LocalizedLine GetLocalizedLine(Yarn.Line line)
{
var text = line.ID;
if (_localizationTable != null)
{
text = _localizationTable[line.ID]?.LocalizedValue ??
$"Error: Missing localisation for line {line.ID} in string table {_localizationTable.LocaleIdentifier}";
}
LocalizeSubstitutions(line);
var resText = CommonUtil.Escape(text);
// Construct the localized line
LocalizedLine localizedLine = new LocalizedLine
{
TextID = line.ID,
RawText = resText,
Substitutions = line.Substitutions,
Metadata = YarnProject.lineMetadata.GetMetadata(line.ID)
};
// Attempt to fetch metadata tags for this line from the string
// table
if (_localizationTable == null) return localizedLine;
var metadata = _localizationTable[line.ID]?.SharedEntry.Metadata
.GetMetadata<Yarn.Unity.UnityLocalization.LineMetadata>();
if (metadata != null && localizedLine.Metadata != null)
{
localizedLine.Metadata = localizedLine.Metadata.Concat(metadata.tags).ToArray();
}
else if (localizedLine.Metadata == null && metadata != null)
{
localizedLine.Metadata = metadata.tags;
}
return localizedLine;
}
private void LocalizeSubstitutions(Yarn.Line line)
{
for (var i = 0; i < line.Substitutions.Length; i++)
{
line.Substitutions[i] = LocalizationKit.LocalizeParam(line.Substitutions[i], _localizationTable);
}
}
/// <summary>
/// 初始化StringTable
/// </summary>
/// <param name="tableName">本地化表名称</param>
/// <returns>协程</returns>
public void InitStringTable(string tableName)
{
// 释放
_localizationTable?.Dispose();
// 加载
_localizationTable = new LocalizationTable(tableName);
}
public string GetStringTableName()
{
return _localizationTable.TableName;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 8d3a78e02d644e96a9e0caa46f1e442b
timeCreated: 1745489761
+49 -4
View File
@@ -1,12 +1,14 @@
using System;
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using AibisDream.Framework;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream
{
public class OptionView : DialogueViewBase
public class OptionView : DialoguePresenterBase
{
private HashSet<string> _selectedIds = new();
[SerializeField] private DefaultOptions defaultOptions;
@@ -14,15 +16,35 @@ namespace AibisDream
[SerializeField] private OnelineOptions onelineOptions;
[SerializeField] private DialogViewType defaultViewType = DialogViewType.StandardBubble;
public override YarnTask OnDialogueStartedAsync()
{
return YarnTask.CompletedTask;
}
public override YarnTask OnDialogueCompleteAsync()
{
HideDialog();
return YarnTask.CompletedTask;
}
public override YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
return YarnTask.CompletedTask;
}
public void LoadBubbles(BubbleSlotGroupData data)
{
defaultViewType = data.dialogViewType;
}
public override void RunOptions(DialogueOption[] dialogueOptions, Action<int> onOptionSelected)
public override async YarnTask<DialogueOption?> RunOptionsAsync(DialogueOption[] dialogueOptions, LineCancellationToken cancellationToken)
{
EnumEventSystem.Global.Send(DialogEventEnum.OptionShow);
var options = DialogOption.GeneOptions(dialogueOptions, onOptionSelected, _selectedIds);
var optionSelectedSource = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
var options = DialogOption.GeneOptions(dialogueOptions, optionId =>
{
optionSelectedSource.TrySetResult(optionId);
}, _selectedIds);
var viewType = GetCurrentViewType(options);
@@ -42,6 +64,29 @@ namespace AibisDream
Debug.Log($"选项{options[0].Character.key}没有找到选项框{viewType}");
break;
}
using var cancellationReg = cancellationToken.NextContentToken.Register(() =>
{
optionSelectedSource.TrySetCanceled(cancellationToken.NextContentToken);
});
try
{
var selectedOptionId = await optionSelectedSource.Task;
foreach (var dialogueOption in dialogueOptions)
{
if (dialogueOption.DialogueOptionID == selectedOptionId)
{
return dialogueOption;
}
}
return null;
}
catch (TaskCanceledException)
{
return null;
}
}
private DialogViewType GetCurrentViewType(DialogOption[] options)