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

6.3 KiB
Raw Blame History

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 模式)

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)

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。