Files
ichni_Official/docs/Plan 剧情系统重构(故事树 + Yarn Spinner 对话,VN 风格).md
SoulliesOfficial 7b7d069b84 StorySystem
2026-07-07 01:28:27 -04:00

34 KiB
Raw Blame History

Plan: 剧情系统重构(故事树 + Yarn Spinner 对话VN 风格)

Context

目标项目: ichni OfficialUnity 6000.3URPWwise。剧情系统为 Official 独有(IchniCreatorStudioStory 模块),因此本次重构不涉及双项目同步,也不涉及 BM 类。

要做什么、为什么: 现有剧情系统在引入 Yarn Spinner 之前编写,对话处理(DialogManager + 自定义文本解析 + DynamicExpresso 解释器 + DialogUI)已过时。本计划从零重构整个剧情系统:数据、故事树 UI、存档、对话全部重来改用 Yarn Spinner 3.2.1 驱动对话,并采用视觉小说VN风格呈现(多角色立绘、打字机、表现类 Yarn 命令),对话文本经 Unity Localization 统一管理多语言。

当前系统关键事实(将被替换):

  • StoryDataSerializedScriptableObject)按章节定义 DialogBlockData/SongBlockData/TutorialBlockData/initialBlocksblockName/blockID/blockSize/blockPosition/nextBlocks)。
  • StorylineMonoBehaviour)从 StoryData 或 ES3 存档构建节点图,管理 storyBlocks/connectors,按章节存取;StoryBlockUIBaseDialogBlockUI/SongBlockUI/TutorialBlockUI,点击判断 StoryBlockState { Locked, Current, Completed }
  • DialogManager:自定义解析器([段落]{函数}speaker:句子$Choice(...)$Condition{...}、内联 {@表达式}),从 Resources/Story/[chapter]/Dialogs/ 加载 TextAsset
  • StoryInterpretersDynamicExpressoSetVariable/GetVariable/GenerateDialogBlock/GenerateSongBlock/SetUnlockKey——不仅分支对话,还动态生成故事树节点、解锁歌曲。
  • DialogUIDialogUIPage/DialogContentFrame/DialogTextUI/ChoiceGroupUI/ChoiceButtonUI历史记录式scrollback呈现点击推进。
  • 存档:GameSaveManager.StorySaveModuleblock 存档、storyVariablesselectedChoicesES3 持久化到 persistentDataPath/StorySaves/[Chapter].json
  • 剧情系统实例位于 MenuScene.unityStoryManagerDialogManager 均在其中)。

Yarn Spinner 3.2.1 关键 API已核实包源码

  • DialogueRunnerStartDialogue(nodeName)UnityEvent onDialogueStart / onDialogueCompleteAddCommandHandler(name, Delegate)dialogueViewsDialoguePresenterBase 列表)。异步架构(YarnTask)。
  • DialoguePresenterBaseRuntime/Views/DialoguePresenterBase.csoverride YarnTask RunLineAsync(LocalizedLine, LineCancellationToken)YarnTask<DialogueOption?> RunOptionsAsync(DialogueOption[], LineCancellationToken)YarnTask OnDialogueStartedAsync()YarnTask OnDialogueCompleteAsync();可选 OnNodeEnter/OnNodeExit
  • VariableStorageBehaviourRuntime/Storage/VariableStorageBehaviour.cs):自定义变量存储基类。
  • UnityLocalisedLineProviderRuntime/LineProviders/UnityLocalisedLineProvider.cs.Installed.cs 分部类需安装 com.unity.localization 才编译):桥接 Unity Localization String Table。
  • 表现命令用 [YarnCommand("name")] 静态/实例方法或 AddCommandHandler 注册。

设计决策(已与你确认):

  1. 全部从零重构数据、UI、存档
  2. VN 风格呈现 + 多角色立绘 + 表现类 Yarn 命令(如立绘跳动/移动)。
  3. 混合式解锁:故事树默认在 StoryData 静态定义;同时允许 Yarn 命令在特殊情况下插入/解锁 block。
  4. 完整表现命令集 + 支持多立绘的 VN Presenter立绘渲染做抽象(当前 Sprite未来可换 Live2D/Spine
  5. 对话本地化桥接 Unity Localization(弃用 I2由 Unity Localization 统一管理对话与 UI 文本。
  6. 仅优先文本块TextBlockSong/Tutorial block 作为占位符。

新目录结构(/ichni Official/Assets/Scripts/Story/

Story/
├── Data/                          # 数据与 ScriptableObject
│   ├── StoryData.cs               # 章节故事树定义(重建)
│   ├── StoryBlockDefinition.cs    # block 定义基类 + Text/Song/Tutorial + 解锁条件
│   ├── CharacterData.cs           # 单个角色定义id/显示名Key/立绘/情绪)
│   └── CharacterRegistry.cs       # 角色注册表id -> CharacterData
├── Tree/                          # 故事树运行时 + UI
│   ├── StoryManager.cs            # 协调器(重建)
│   ├── StoryTreeController.cs     # 构建树/状态/解锁重算/存取(原 Storyline
│   ├── StoryBlockView.cs          # block 视图基类(原 StoryBlockUIBase
│   ├── TextBlockView.cs           # 文本块(原 DialogBlockUI
│   ├── SongBlockView.cs           # 占位
│   ├── TutorialBlockView.cs       # 占位
│   └── BlockConnectorView.cs      # 连接线(原 BlockConnectorUI
├── Dialogue/                      # Yarn 集成
│   ├── StoryDialogueController.cs # block → DialogueRunner 桥接
│   ├── VNDialoguePresenter.cs     # DialoguePresenterBase 实现VN
│   ├── VNPortraitStage.cs         # 立绘舞台(多槽位)
│   ├── IPortraitRenderer.cs       # 立绘渲染抽象接口
│   ├── SpritePortraitRenderer.cs  # Sprite 实现
│   ├── StoryVariableStorage.cs    # VariableStorageBehaviour 桥接存档
│   ├── StoryPresentationCommands.cs # [YarnCommand] 表现命令
│   ├── StoryTreeCommands.cs       # [YarnCommand] 解锁/生成 block/解锁歌曲
│   └── StoryFunctions.cs          # [YarnFunction] 条件辅助
├── Save/
│   └── StorySave.cs               # 存档 DTO重建
└── UI/
    └── StoryUIPage.cs             # 故事树页面(适配保留)

参考页面:@ichni-Project-Architecture-Report


Modifications

A. Scripts

A1. Story/Data/StoryBlockDefinition.cs(新建)

  • 描述: 章节故事树中单个 block 的纯数据定义 + 解锁条件。路径 /ichni Official/Assets/Scripts/Story/Data/StoryBlockDefinition.cs
  • 遵循模式: 参考旧 StoryData.cs[Serializable]/[InlineProperty]/[FoldoutGroup("$blockName")] Odin 风格。
  • 内容:
    • enum StoryBlockType { Text, Song, Tutorial }
    • [Serializable] class StoryBlockDefinitionstring blockId(唯一,替代旧 blockName/blockID 二元)、StoryBlockType typeVector2 positionVector2 sizeStoryBlockState initialStateList<string> nextBlockIdsUnlockCondition unlockCondition
    • 类型专属数据(用 Odin [ShowIf]type 显示):string yarnNodeName + string titleKeyTextstring songNameSong 占位);string tutorialNameTutorial 占位)。
    • [Serializable] class UnlockConditionList<string> requiredCompletedBlockIdsList<VariableCondition> variableConditions;方法 bool IsSatisfied(Func<string,int> getVar, Func<string,bool> isCompleted)
    • [Serializable] class VariableConditionstring variableNameenum Comparison { Equal, NotEqual, Greater, GreaterOrEqual, Less, LessOrEqual }int valuebool Evaluate(int actual)
  • 公共 API bool UnlockCondition.IsSatisfied(...)bool VariableCondition.Evaluate(int)

A2. Story/Data/StoryData.cs(重建,覆盖现有)

  • 描述: 章节故事树 ScriptableObject。路径 /ichni Official/Assets/Scripts/Story/Data/StoryData.cs
  • 内容: [CreateAssetMenu(menuName="Ichni/Story/StoryData")] class StoryData : SerializedScriptableObjectstring chapterIndexList<StoryBlockDefinition> blocksYarnProject yarnProject(该章节编译产物引用)、AssetReference/string 本地化 String Table 集合名(供 Presenter/Provider 使用)。
  • 公共 API StoryBlockDefinition GetBlock(string blockId)IEnumerable<StoryBlockDefinition> GetInitialBlocks()IEnumerable<StoryBlockDefinition> GetNextBlocks(string blockId)
  • 依赖: A1、Yarn YarnProject

A3. Story/Data/CharacterData.cs + CharacterRegistry.cs(新建)

  • 描述: 角色立绘/情绪注册表VN Presenter 需要)。路径 /ichni Official/Assets/Scripts/Story/Data/
  • **CharacterData[Serializable]** string characterId(与 Yarn 行的 speaker 名对应)、string displayNameKeyUnity Localization 键)、PortraitStyle portraitStyleenum { Sprite, Live2D, Spine } 预留)、SerializedDictionary<string, Sprite> emotionSpritesSprite 方案)、GameObject dynamicPortraitPrefabLive2D/Spine 预留,实现 IPortraitRenderer)。
  • **CharacterRegistry[CreateAssetMenu(menuName="Ichni/Story/CharacterRegistry")] SerializedScriptableObject** Dictionary<string, CharacterData> charactersbool TryGet(string id, out CharacterData data)
  • 说明: 立绘表现不直接耦合 Sprite交由 IPortraitRenderer(见 A9读取 CharacterData 决定渲染方式,未来接入 Live2D/Spine 只需新增 renderer 实现。

A4. Story/Save/StorySave.cs(重建,覆盖现有)

  • 描述: 存档 DTO。路径 /ichni Official/Assets/Scripts/Story/Save/StorySave.cs
  • 内容: [Serializable] class StoryBlockSave { string blockId; Vector2 position; StoryBlockState state; List<string> nextBlockIds; }(合并旧的按类型拆分的多个 Save 类为单一带 blockId 的 DTO类型信息从 StoryData 反查,存档只存运行态);[Serializable] class ChapterStorySave { string chapterIndex; List<StoryBlockSave> blocks; }
  • 依赖: A1StoryBlockState、block 类型引用)。

A5. Story/Tree/StoryManager.cs(重建,覆盖现有)

  • 描述: 剧情系统协调器(单例)。路径 /ichni Official/Assets/Scripts/Story/Tree/StoryManager.cs
  • 遵循模式: 参考现有 StoryManager 单例 + [Button] ClearAllStorySave();建议继承 SLSUtilities.General.Singleton<StoryManager>(项目已有单例基类)。
  • 内容: 持有 StoryTreeController treeControllerStoryDialogueController dialogueControllerStoryUIPage storyUIPageCharacterRegistry characterRegistryDictionary<string, StoryData> storyDatas。保留 enum StoryBlockState { Locked, Current, Completed }(移到此文件或独立文件)。保留 ClearAllStorySave()(改为调用新 StorySaveModule 接口)。
  • 公共 API void OpenChapter(string chapterIndex)(构建树)、StoryData GetStoryData(string chapterIndex)

A6. Story/Tree/StoryTreeController.cs(重建自 Storyline.cs

  • 描述:StoryData/存档构建 block 视图与连接线,管理状态、解锁重算与存取。路径 /ichni Official/Assets/Scripts/Story/Tree/StoryTreeController.cs
  • 遵循模式: 参考旧 Storyline.csSetUpStoryline/Generate*Block/GenerateConnector/SaveStoryline/ResetStory 结构与 ScrollRect content 布局逻辑。
  • 公共 API
    • void BuildChapter(string chapterIndex):优先 LoadChapter,无存档则 ResetToInitial
    • StoryBlockView GenerateBlock(StoryBlockDefinition def) / void GenerateConnector(string fromBlockId, string toBlockId)
    • void OnBlockCompleted(string blockId):设该 block Completed,遍历所有 Locked block 用 UnlockCondition.IsSatisfied(...) 重算,满足者置 Current(并按需生成尚未显示的后继 block/连接线),刷新连接线,保存。
    • void UnlockBlock(string blockId) / void InsertBlock(StoryBlockDefinition def, string fromBlockId):供 Yarn 树命令A11调用实现"混合式"动态解锁/插入。
    • void SaveChapter() / void LoadChapter(string chapterIndex) / void ResetToInitial(string chapterIndex)
    • StoryBlockView currentBlock { get; }IReadOnlyList<StoryBlockView> blocks
  • 解锁判定: IsSatisfiedgetVar 委托指向 StoryVariableStorageA8isCompleted 指向已完成 block 集合。
  • 依赖: A1、A2、A4、A8、GameSaveManager.StorySaveModule(重建后,见 A13ChapterSelectionManager

A7. Story/Tree/StoryBlockView.cs + TextBlockView.cs + SongBlockView.cs + TutorialBlockView.cs + BlockConnectorView.cs(重建自 Blocks/*

  • **StoryBlockView(抽象,MonoBehaviour** string blockIdStoryBlockState stateRectTransform inPort/outPortButton buttonvirtual void ApplyState(StoryBlockState)(更新视觉:锁定/当前/已完成外观);abstract StoryBlockSave GetSave()abstract void OnClicked()
  • **TextBlockView** 覆盖 OnClicked()state == Locked → 忽略;否则 StoryManager.instance.dialogueController.PlayBlock(this)(传 yarnNodeName)。参考旧 DialogBlockUIbutton.onClick + 状态判断。
  • **SongBlockView / TutorialBlockView** 占位实现——显示标题/状态,OnClicked() 暂仅 Debug.Log(保留旧 SongBlockUI/TutorialBlockUI 视觉字段,交互逻辑注释或占位)。
  • **BlockConnectorView** 沿用旧 BlockConnectorUI,用 UILineRendereroutPortinPort 画曲线;SetCurve()
  • 依赖: A6、A9对话触发

A8. Story/Dialogue/StoryVariableStorage.cs(新建)

  • 描述: 自定义 Yarn 变量存储,桥接 GameSaveManager.StorySaveModule.storyVariablesES3 持久化。路径 /ichni Official/Assets/Scripts/Story/Dialogue/StoryVariableStorage.cs
  • 遵循模式: 继承 Yarn.Unity.VariableStorageBehaviour,实现其抽象/虚方法(SetValue/TryGetValue/Clear/Contains 等——实现步骤时先 printAsset 或 view VariableStorageBehaviour.cs 核对当前签名)。
  • 要点: Yarn 变量以 $ 前缀命名;本存储把布尔/数值/字符串映射到 storyVariables(现为 Dictionary<string,int>,需扩展为支持多类型,见 A13。变量读写触发 StorySaveModule 持久化。
  • 公共 API供 A6 解锁判定复用): int GetIntVariable(string name)bool HasVariable(string name)
  • 依赖: A13StorySaveModule)。

A9. Story/Dialogue/IPortraitRenderer.cs + SpritePortraitRenderer.cs(新建)

  • 描述: 立绘渲染抽象 + Sprite 实现。路径 /ichni Official/Assets/Scripts/Story/Dialogue/
  • **IPortraitRenderer** void Show(CharacterData character, string emotion)void Hide()void SetEmotion(string emotion)void MoveTo(Vector2 anchoredPos, float duration)void PlayEffect(string effect, float intensity)(如 "jump"/"shake"/"nod")、void SetActiveVisual(bool speaking)(说话者高亮/变暗)。
  • **SpritePortraitRendererMonoBehaviour, IPortraitRenderer** 持有 UnityEngine.UI.ImageShowCharacterData.emotionSprites 取 SpriteMoveTo/PlayEffect 用 DOTween项目已含 DOTweenPro实现跳动/移动/晃动。
  • 说明: 未来 Live2D/Spine 只需新增实现 IPortraitRenderer 的组件,CharacterData.portraitStyle/dynamicPortraitPrefab 决定实例化哪个 renderer。

A10. Story/Dialogue/VNPortraitStage.cs(新建)

  • 描述: 管理多立绘槽位(如 Left/Center/Right按 characterId 分配/复用 renderer。路径 /ichni Official/Assets/Scripts/Story/Dialogue/VNPortraitStage.cs
  • 公共 API void ShowPortrait(string characterId, string slot, string emotion)void HidePortrait(string characterId)void HideAll()void MovePortrait(string characterId, string slot, float duration)void PlayEffect(string characterId, string effect, float intensity)void HighlightSpeaker(string characterId)
  • 内部: Dictionary<string, IPortraitRenderer> activePortraits;依据 CharacterData.portraitStyle 实例化对应 renderer 到槽位。
  • 依赖: A3、A9。

A11. Story/Dialogue/StoryPresentationCommands.cs(新建)

  • 描述: 表现类 Yarn 命令集合([YarnCommand])。路径 /ichni Official/Assets/Scripts/Story/Dialogue/StoryPresentationCommands.cs
  • 命令集(完整表现集): 均委托到 VNPortraitStage/VNDialoguePresenter
    • <<show_portrait characterId slot emotion>> / <<hide_portrait characterId>> / <<hide_all_portraits>>
    • <<move_portrait characterId slot duration>> / <<jump_portrait characterId intensity>> / <<shake_portrait characterId intensity>> / <<set_emotion characterId emotion>>
    • <<set_background bgId duration>>(背景切换)
    • <<wait seconds>>Yarn 内置也可,视需要)
  • 注册方式: 实例方法用 [YarnCommand("show_portrait")] 挂到与 DialogueRunner 同场景的组件上Yarn 自动发现),或在 StoryDialogueController.AwakedialogueRunner.AddCommandHandler(...) 显式注册。实现步骤时先核对 DialogueRunner.ActionRegistration.csAddCommandHandler 重载与 [YarnCommand] 自动注册要求。
  • 依赖: A10、A12。

A12. Story/Dialogue/StoryTreeCommands.cs(新建)

  • 描述: 影响故事树/解锁的 Yarn 命令(替代旧 StoryInterpretersGenerateDialogBlock/GenerateSongBlock/SetUnlockKey)。路径 /ichni Official/Assets/Scripts/Story/Dialogue/StoryTreeCommands.cs
  • 命令集:
    • <<unlock_block blockId>>StoryTreeController.UnlockBlock
    • <<insert_block blockId>> → 从当前 block 生成后继("混合式"动态插入)→ StoryTreeController.InsertBlock
    • <<unlock_song songUnlockKey>> → 复用旧 SetUnlockKey 逻辑:写入 SongSaveModule.storyUnlockKeys,并把"解锁提示消息"入队到对话结束后执行(见 A14 结束回调)。
  • 依赖: A6、A14、GameSaveManager.SongSaveModuleChapterSelectionManager

A13. Scripts/Saving/GameSaveManager.csStorySaveModule(重建剧情存档模块)

  • 描述: 重建 StorySaveModule,适配新 DTO 与多类型变量。路径 /ichni Official/Assets/Scripts/Saving/GameSaveManager.csStorySaveModule 分部/内嵌类)。
  • 改动:
    • block 存档改用 ChapterStorySave/StoryBlockSaveA4ES3 存 persistentDataPath/StorySaves/[Chapter].json(沿用旧路径约定)。
    • storyVariables 扩展为支持 Yarn 多类型float/bool/string——建议 Dictionary<string, string> 序列化 + 类型标记,或三个分字典;供 StoryVariableStorageA8读写。
    • 保留 selectedChoices 语义Yarn 选项选择记录),如需回放已完成对话可保留(见"实现说明")。
    • 保留 SongSaveModule.storyUnlockKeys 相关接口A12 依赖)。
    • ClearAllStoryline() / ClearStoryKeys() 接口保留,供 StoryManager.ClearAllStorySave 调用。
  • 注意: GameSaveManager 含其它模块(SongSaveModule),改动仅限剧情相关字段,保持其余不变。实现前先完整 view GameSaveManager.csStorySaveModule 现状。

A14. Story/Dialogue/StoryDialogueController.cs(新建)

  • 描述: block → DialogueRunner 的桥接与生命周期协调。路径 /ichni Official/Assets/Scripts/Story/Dialogue/StoryDialogueController.cs
  • 公共 API
    • void PlayBlock(TextBlockView block):记录当前 block 上下文,storyUIPage 淡出/对话页淡入,dialogueRunner.StartDialogue(block.yarnNodeName)
    • List<UnityAction> dialogueEndActions(保留旧 dialogEndActions 语义,供 A12 unlock_song 入队解锁提示)。
  • 事件接线: 订阅 dialogueRunner.onDialogueComplete:若当前 block 为 CurrenttreeController.OnBlockCompleted(blockId);清空/执行 dialogueEndActions(弹解锁提示);treeController.SaveChapter();对话页淡出、故事树页淡入。参考旧 DialogManager.PlayDialog 完成分支(旧 L138-152
  • 依赖: Yarn DialogueRunner、A6、A7、A12、StoryUIPage

A15. Story/Dialogue/VNDialoguePresenter.cs(新建)

  • 描述: VN 风格对话呈现器,继承 Yarn.Unity.DialoguePresenterBase。路径 /ichni Official/Assets/Scripts/Story/Dialogue/VNDialoguePresenter.cs
  • override
    • OnDialogueStartedAsync()显示对话面板DOTween 淡入)、portraitStage 就绪。
    • RunLineAsync(LocalizedLine line, LineCancellationToken token):从 line.CharacterNameCharacterRegistry 取显示名Unity Localization高亮说话者立绘portraitStage.HighlightSpeaker),打字机逐字显示 line.TextWithoutCharacterName;等待玩家点击推进(点击→token hurry/advance
    • RunOptionsAsync(DialogueOption[], LineCancellationToken):实例化选项按钮面板,等待选择,返回选中项;记录到 selectedChoices
    • OnDialogueCompleteAsync():对话面板淡出。
  • 要点: 全程 YarnTask 异步;打字机需响应 LineCancellationToken(点击加速/跳过)。实现步骤时先 view DialoguePresenterBase.csLocalizedLine/DialogueOption 类型核对属性名(CharacterName/TextWithoutCharacterName/Text)。
  • 依赖: A3、A10、Unity Localization。

A16. Story/Dialogue/StoryFunctions.cs(新建,可选)

  • 描述: [YarnFunction] 辅助,让 .yarn<<if>> 能读取剧情状态。路径 /ichni Official/Assets/Scripts/Story/Dialogue/StoryFunctions.cs
  • 示例: [YarnFunction("is_completed")] static bool IsCompleted(string blockId)[YarnFunction("story_var")] static int StoryVar(string name)
  • 说明: 多数条件可直接用 Yarn 变量($var+ StoryVariableStorage 完成;此文件仅在需要读取"block 完成状态"等非变量信息时补充。

A17. 删除/弃用旧脚本

  • 删除: Story/Dialog/DialogManager.csStory/Dialog/DialogModule.csStory/Dialog/StoryInterpreters.csStory/StoryUI/DialogUI/*DialogUIPage/DialogContentFrame/DialogTextUI/ChoiceGroupUI/ChoiceButtonUIStory/StoryUI/Blocks/* 旧类(迁移到 A7 后删除);旧 Story/StoryData/StoryData.cs、旧 Story/Dialog/StoryManager.cs、旧 Story/StoryUI/Storyline.cs(被 A2/A5/A6 取代)。
  • 注意: DynamicExpresso 若无其它使用者,可保留包(供将来),但剧情不再依赖它。删除前 grep 确认无外部引用(尤其 ChapterSelectionManager/MenuDialogManager 的调用点,需改为 StoryDialogueController)。

B. Text Files.yarn 脚本)

B1. 示例章节 Yarn 脚本

  • 描述: 每个 TextBlock 对应一个 Yarn 节点。路径示例 /ichni Official/Assets/Story/Chapters/Chapter1/Chapter1.yarn(新目录,脱离旧 Resources/Story/.../Dialogs)。
  • 内容: 为若干 TextBlock 各写一个 title: 节点,含 speaker 行、<<show_portrait>>/<<jump_portrait>> 等表现命令、-> 选项、<<if $var>> 条件分支、<<set $var to ...>>、必要处 <<unlock_block>>/<<unlock_song>>。用于验证管线。
  • 依赖: A11/A12 命令、A8 变量。

C. Unity AssetsScriptableObject / YarnProject

C1. Chapter1 YarnProject

  • 名称: /ichni Official/Assets/Story/Chapters/Chapter1/Chapter1.yarnproject
  • 描述: 编译 B1 的 .yarn,配置 Localisation = Unity(使用 UnityLocalisedLineProvider),关联 Unity Localization String Table Collection。
  • Bezi Actions 无直接创建 YarnProject 的 Action → 手动创建(见 E2

C2. StoryDataChapter1资产

  • 名称: /ichni Official/Assets/Story/Chapters/Chapter1/Chapter1_StoryData.asset
  • 描述: 定义 Chapter1 故事树 block、连接、初始状态、yarnNodeName、解锁条件,引用 C1 YarnProject。
  • Bezi Actions createOrUpdateScriptableObjectscriptableObjectType: "Ichni.Story.StoryData")。
  • 依赖: A2、C1。

C3. CharacterRegistry + 角色资产

  • 名称: /ichni Official/Assets/Story/Characters/CharacterRegistry.asset
  • 描述: 登记若干测试角色characterId、displayNameKey、emotion→Sprite
  • Bezi Actions createOrUpdateScriptableObjectscriptableObjectType: "Ichni.Story.CharacterRegistry")。
  • 依赖: A3。

D. Unity Actions场景 / 预制体 / UI 接线,均在 MenuScene.unity

D1. Yarn 运行时对象

  • 描述: 在剧情 UI 层新建 StoryDialogueRoot GameObjectDialogueRunner + UnityLocalisedLineProvider + StoryVariableStorage + StoryDialogueController + StoryPresentationCommands + StoryTreeCommandsDialogueRunner.yarnProject 指向 C1dialogueViews 加入 D2 的 VNDialoguePresentervariableStorage 指向 StoryVariableStoragelineProvider 指向 UnityLocalisedLineProvider
  • Bezi Actions createGameObjectaddOrUpdateComponent(逐个组件;DialogueRunner 字段引用用 UnityObjectReference)。
  • 依赖: A8/A11/A12/A14/A15、C1。

D2. VN 对话 UI

  • 描述: 构建 VN 对话面板:全屏遮罩/背景 Image、立绘舞台Left/Center/Right 槽位 Image)、对话框(说话者名 TextMeshProUGUI + 正文 TextMeshProUGUI + 继续指示)、选项面板(竖直布局,运行时实例化选项按钮)。挂 VNDialoguePresenterVNPortraitStage,槽位挂 SpritePortraitRenderer
  • Bezi Actions createGameObjectaddOrUpdateComponentUnityEngine.UI.Image/TMPro.TextMeshProUGUI/UnityEngine.UI.Button)、setRectTransformLayout(锚点/拉伸)。
  • 依赖: A9/A10/A15。

D3. 故事树 UI 与 block 预制体

  • 描述: 构建/复用故事树滚动视图(ScrollRect content);创建 TextBlockView/SongBlockView/TutorialBlockView/BlockConnectorView 预制体(含 ButtoninPort/outPort、状态视觉),供 StoryTreeController 实例化。将 StoryManager/StoryTreeController/StoryUIPage 组件接线到既有剧情 UI 对象。
  • Bezi Actions createGameObjectaddOrUpdateComponentsetRectTransformLayoutcreatePrefabblock 预制体)。
  • 依赖: A5/A6/A7、C2/C3。

E. Manual User Actions

E1. 安装 Unity Localization规划弃用 I2

  • 描述: 通过 Package Manager 安装 com.unity.localization(使 UnityLocalisedLineProvider.Installed.cs 生效);创建 Locale 与 String Table Collection对话文本 + UI 文本。I2 的实际移除可后置,避免一次性破坏现有 UI 文本;本计划仅让剧情对话走 Unity Localization。

E2. 创建并配置 YarnProject

  • 描述:Assets/Story/Chapters/Chapter1/ 右键创建 YarnProject纳入 B1 的 .yarnInspector 中设 Localisation 为 Unity 并关联 String Table Collection导入生成基础语言行。Bezi Actions 无 YarnProject 创建能力。)

E3. 核对 Yarn 命令/呈现器 API 签名

  • 描述: 实现 A8/A11/A15 前,在 Editor 中确认 VariableStorageBehaviourDialoguePresenterBaseLocalizedLineDialogueOptionDialogueRunner.AddCommandHandler/[YarnCommand] 的确切签名(版本 3.2.1),避免 API 漂移导致编译错误。

Key Patterns to Reuse

  • Source: /ichni Official/Assets/Scripts/Story/StoryUI/Storyline.csPattern: 故事树构建、连接线生成、按章节 ES3 存取、ScrollRect 布局,迁移到 StoryTreeController
  • Source: /ichni Official/Assets/Scripts/Story/Dialog/DialogManager.csL138-152 完成分支、dialogEndActions)— Pattern: 对话完成 → 置 block Completed → 执行结束回调 → 存档,迁移到 StoryDialogueController.onDialogueComplete
  • Source: /ichni Official/Assets/Scripts/Story/Dialog/StoryInterpreters.csPattern: SetUnlockKey 的歌曲解锁 + 消息入队逻辑,迁移到 StoryTreeCommands.unlock_song
  • Source: SLSUtilities/General/Singleton<T>Pattern: StoryManager 单例,替换手写 instance = this
  • Source:StoryData.cs 的 Odin [FoldoutGroup("$blockName")]/[InlineProperty]Pattern:StoryBlockDefinition 的 Inspector 组织。
  • Source: DialogCharacter(旧 DialogModule.csname/title/emotions)— Pattern: 演进为 CharacterDatacharacterId/displayNameKey/emotionSprites)。

Implementation Notes

  • Yarn 3.x 异步: Presenter 全部 YarnTask;打字机与"点击推进"须响应 LineCancellationTokenhurry-up / cancel否则点击无法加速或跳过。
  • 命令注册: [YarnCommand] 自动发现要求承载组件与 DialogueRunner 处于可解析范围;若自动发现不稳定,改用 DialogueRunner.AddCommandHandler 显式注册(在 StoryDialogueController.Awake)。
  • 变量类型: Yarn 变量支持 number/bool/stringstoryVariablesint须扩展多类型存储A13否则 StoryVariableStorage 无法承载布尔/字符串。
  • 本地化桥接: speaker 名解析来自 Yarn 行的 CharacterName;显示名走 Unity LocalizationCharacterData.displayNameKey),正文走 UnityLocalisedLineProvider 关联的 String Table。
  • 立绘抽象: 不要在 Presenter/命令中直接写 Image;一律经 IPortraitRenderer,为 Live2D/Spine 预留。DOTween 已在项目中,用于跳动/移动/晃动。
  • 混合式解锁: 静态解锁在 OnBlockCompleted 重算 UnlockCondition;动态解锁由 <<unlock_block>>/<<insert_block>> 命令在对话中触发——两者都最终调用 StoryTreeController,保证存档一致。
  • 存档兼容: 这是破坏性重构,旧 StorySaves/*.json/Choices.json 结构不兼容;建议在 StorySaveModule 加版本号或提供一次性清档(ClearAllStorySave 已存在)。
  • 场景范围: 所有场景接线在 MenuScene.unity;剧情为 Official 独有,无需同步到 IchniCreatorStudio
  • 可运行性分阶段: 每阶段完成后项目应可编译、可进入剧情页。先落数据/树(占位对话),再接 Yarn 管线,再做 VN 呈现与命令,最后清理旧系统。

TODO

阶段 1 — 数据与故事树骨架(可编译、可显示占位树)

新建 StoryBlockDefinition.csblock 定义 + UnlockCondition/VariableCondition + StoryBlockType

重建 StoryData.cs(章节 block 列表 + YarnProject 引用 + 查询 API

新建 CharacterData.cs + CharacterRegistry.cs(含 portraitStyle 预留)

重建 StorySave.csStoryBlockSave/BlockConnectorSave/ChapterStorySave/StoryVariablesSave

重建 StoryManager.cs(单例协调器 + StoryBlockState

重建 StoryTreeController.cs(构建/解锁重算/存取,从 Storyline 迁移)

重建 StoryBlockView + TextBlockView/SongBlockView/TutorialBlockView/BlockConnectorView

createOrUpdateScriptableObject 生成 Chapter1_StoryData.assetCharacterRegistry.asset(用户已手动创建三个 SO 资产)

用 Bezi Actions 搭建故事树滚动视图与 block 预制体,接线 StoryManager/StoryTreeController/StoryUIPage(改为用户手动制作,见阶段总结的手动步骤)

(提前合并原阶段 5 清理)删除旧剧情/对话脚本网:DialogManager/DialogModule/StoryInterpreters/Storyline/旧 StoryManager/旧 Block UIs/DialogUI/*/旧 StoryData/旧 StorySaveMenuManager 移除失效 dialogUIPage 字段

阶段 2 — Yarn 对话管线(点击文本块可跑通 Yarn 节点)

安装 com.unity.localization 并创建 Locale / String Table Collection手动

手动创建 Chapter0.yarnproject 并设为 Unity Localisation编写示例 Chapter0.yarn

新建 StoryVariableStorage.csVariableStorageBehaviour 桥接存档,多类型变量)

重建 GameSaveManager.StorySaveModule(新 DTO + 多类型 storyVariables,保留 SongSaveModule 不变)——已在阶段 1 StorySave 步骤中一并完成:树+选项按章节(单 key 容器变量全局float/string/bool 三类型,匹配 Yarn 契约)

新建 StoryDialogueController.csPlayBlock + onDialogueComplete → 置 Completed/重算解锁/存档/结束回调)

MenuScene 搭建 StoryDialogueRootDialogueRunner + StoryVariableStorage + 控制器),接线到 Chapter0.yarnproject。注:项目未安装 com.unity.localizationChapter0.yarnproject 使用 Yarn 内置本地化;DialogueRunner 在无 lineProvider 时会自动创建 BuiltinLocalisedLineProvider,故阶段 2 无需接 Unity Localization。呈现器dialoguePresenters留空待阶段 3 接入 VN 呈现器。

阶段 3 — VN 呈现与立绘

新建 VNDialoguePresenter.csDialoguePresenterBase:打字机行、说话者显示名、选项面板、淡入淡出,响应 LineCancellationToken

新建 IPortraitRenderer.cs + SpritePortraitRenderer.csDOTween 跳动/移动/晃动)

新建 VNPortraitStage.cs(多槽位管理,按 portraitStyle 实例化 renderer

用 Bezi Actions 搭建 VN 对话 UI背景/立绘槽位/对话框/选项面板),接入 DialogueRunner.dialogueViews

阶段 4 — 表现与树命令、条件

新建 StoryPresentationCommands.csshow/hide/move/jump/shake_portraitset_emotionset_background

新建 StoryTreeCommands.csunlock_block/insert_block/unlock_song,迁移旧 SetUnlockKey 歌曲解锁+消息入队)

新建 StoryFunctions.cs[YarnFunction] is_completed/story_var,按需)

在示例 .yarn 中覆盖表现命令、选项、<<if>> 条件、<<set>><<unlock_*>> 并验证

阶段 5 — 清理旧系统

grep 确认并改接 ChapterSelectionManager/Menu 等对 DialogManager 的调用点到 StoryDialogueController

删除 DialogManager.cs/DialogModule.cs/StoryInterpreters.cs/DialogUI/*/旧 Blocks/*/旧 Storyline.cs/旧 StoryData.cs/旧 StoryManager.cs

重新编译并清零剧情相关编译错误