Files
Cielonos/.agent/skills-Cielonos/unity-technician/knowledge/YarnSpinnerLargeScale.md
SoulliesOfficial 8186f54e90 新场景,剧情
2026-06-02 12:55:39 -04:00

168 lines
6.3 KiB
Markdown
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.
# Yarn Spinner 大型叙事与本地化系统架构3A 工程规范)
在大规模、高复杂度且需支持多国语言文本/音频本地化的互动叙事项目中,传统的纯 Yarn 内部变量存储会成为架构瓶颈。本文件沉淀了面向 3A 叙事系统的工程设计方案、SSOT单一事实来源数据存储架构、Addressables 异步音频加载,以及 Unity 官方本地化包与 Wwise 音频集成规范。
---
## 核心架构原则 (Core Architectural Principles)
1. **导演与管家解耦 (Director & Butler Decoupling)**
Yarn 脚本纯粹充当“导演”控制剧情流向与台词显示C# 全局数据中心充当“管家”掌控玩家属性、道具、标志位等事实数据。Yarn 脚本通过 C# 特性接口进行只读查询与写入。
2. **单一事实来源 (Single Source of Truth, SSOT)**
剧情相关的全局变量在 C# 侧通过强类型(如 ScriptableObject 或 GameManager集中保存与游戏的存读档Save/Load系统一键对接不依赖 Yarn 的 InMemory 字典。
3. **异步按需载入 (Asynchronous On-Demand Loading)**
数以万计的配音音频资源VO严禁显式挂载必须基于 Yarn Project 自动分配的 **Line ID (Line Tag)** 通过 `Addressables` 寻址异步载入,播放完成后立即释放,防止爆内存。
---
## 核心类与接口设计
### 1. `PlayerStoryStateSO` (ScriptableObject)
- **职责**:保存全局剧情进度、完成的任务列表、解锁的地图标志位。
- **最佳实践**:内部使用高检索效率的 `HashSet<string>`,提供强类型的增删查 API同时支持一键 JSON 序列化持久化。
### 2. `AudioLinePresenter` (DialoguePresenterBase)
- **职责**v3.x 异步核心 UI 展示。根据台词的 `line.LineID` 从 Addressables 动态加载并播放对应语种的配音,并将文字打字机渲染时间与音频物理长度精确匹配。
---
## 3A 本地化与音频同步黄金代码模板
### 1. C# 剧情全局状态中心 (SO 模式)
```csharp
using System.Collections.Generic;
using UnityEngine;
using Yarn.Unity;
[CreateAssetMenu(fileName = "StoryState", menuName = "Narrative/Story State")]
public class StoryStateSO : ScriptableObject
{
public static StoryStateSO Instance;
public int gold = 100;
[SerializeField] private List<string> unlockedFlags = new List<string>();
private HashSet<string> _flagSet = new HashSet<string>();
public void Initialize()
{
Instance = this;
_flagSet = new HashSet<string>(unlockedFlags);
}
public bool HasFlag(string flagId) => _flagSet.Contains(flagId);
public void SetFlag(string flagId)
{
if (_flagSet.Add(flagId))
{
unlockedFlags.Add(flagId);
}
}
}
public static class YarnNarrativeBridge
{
[YarnFunction("check_flag")]
public static bool CheckFlag(string flagId)
{
return StoryStateSO.Instance != null && StoryStateSO.Instance.HasFlag(flagId);
}
[YarnCommand("set_flag")]
public static void SetFlag(string flagId)
{
StoryStateSO.Instance?.SetFlag(flagId);
}
}
```
---
### 2. 基于 Addressables 异步多语言音频控制台词组件 (v3.x Presenter)
```csharp
using System.Threading;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Localization.Settings;
using Yarn.Unity;
public class AddressableAudioPresenter : DialoguePresenterBase
{
[SerializeField] private TMPro.TextMeshProUGUI subtitleText;
[SerializeField] private AudioSource voSource;
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// 1. 获取当前玩家设置的语言代码 (如 "zh-CN", "en-US")
string langCode = LocalizationSettings.SelectedLocale.Identifier.Code;
string addressablePath = $"VO/{langCode}/{line.LineID}";
AudioClip loadedVo = null;
try
{
var opHandle = Addressables.LoadAssetAsync<AudioClip>(addressablePath);
loadedVo = await opHandle.Task;
}
catch (System.Exception)
{
Debug.LogWarning($"[VO] 寻址地址不存在: {addressablePath}");
}
// 2. 音频驱动打字机渲染
if (loadedVo != null)
{
voSource.clip = loadedVo;
voSource.Play();
await SyncTextWithTypewriter(line.TextWithoutCharacterName.Text, loadedVo.length, token.Token);
Addressables.Release(loadedVo); // 精准释放,内存无忧
}
else
{
float speedBasedDuration = line.TextWithoutCharacterName.Text.Length * 0.08f;
await SyncTextWithTypewriter(line.TextWithoutCharacterName.Text, speedBasedDuration, token.Token);
}
// 3. 等待确认
while (!Input.GetKeyDown(KeyCode.Space) && !token.Token.IsCancellationRequested)
{
await YarnTask.Yield();
}
}
private async YarnTask SyncTextWithTypewriter(string text, float targetDuration, CancellationToken ct)
{
subtitleText.text = "";
float delayPerChar = targetDuration / text.Length;
for (int i = 0; i < text.Length; i++)
{
if (ct.IsCancellationRequested)
{
subtitleText.text = text;
voSource.Stop();
break;
}
subtitleText.text += text[i];
await YarnTask.Delay((int)(delayPerChar * 1000));
}
}
}
```
---
## 性能避坑与工程优化红线 (Performance & Engine Optimization)
1. **禁止热加载 Yarn Project**
在加载新的 Yarn 脚本时,尽量避免在运行时频繁销毁和重建 `DialogueRunner`,而是仅在使用完后清空 `VariableStorage`。采用 Addressables 加载 `YarnProject` 资产,以场景级别进行卸载。
2. **Odin Inspector 可视化优化**
为 ScriptableObject 添加 Odin 标注特性(如 `[Searchable]`),在剧情标志位堆积至成百上千个时,能支持运行时高检索,方便策划进行精准断点调试。
3. **文本本地化包 (Localization Tables) 的 GC 问题**
Unity 官方多语言表格底层存在查询开销。在长台词切换高频场景下建议使用内存缓存Cache避免每一帧都去底层查询翻译 String造成大面积 GC Alloc。