## Plan: 剧情系统重构(故事树 + Yarn Spinner 对话,VN 风格) ## Context **目标项目:** `ichni Official`(Unity 6000.3,URP,Wwise)。剧情系统为 Official 独有(`IchniCreatorStudio` 无 `Story` 模块),因此本次重构**不涉及双项目同步**,也不涉及 BM 类。 **要做什么、为什么:** 现有剧情系统在引入 Yarn Spinner 之前编写,对话处理(`DialogManager` + 自定义文本解析 + DynamicExpresso 解释器 + `DialogUI`)已过时。本计划**从零重构**整个剧情系统:数据、故事树 UI、存档、对话,全部重来,改用 **Yarn Spinner 3.2.1** 驱动对话,并采用**视觉小说(VN)风格**呈现(多角色立绘、打字机、表现类 Yarn 命令),对话文本经 **Unity Localization** 统一管理多语言。 **当前系统关键事实(将被替换):** - `StoryData`(`SerializedScriptableObject`)按章节定义 `DialogBlockData`/`SongBlockData`/`TutorialBlockData`/`initialBlocks`(`blockName`/`blockID`/`blockSize`/`blockPosition`/`nextBlocks`)。 - `Storyline`(`MonoBehaviour`)从 `StoryData` 或 ES3 存档构建节点图,管理 `storyBlocks`/`connectors`,按章节存取;`StoryBlockUIBase` → `DialogBlockUI`/`SongBlockUI`/`TutorialBlockUI`,点击判断 `StoryBlockState { Locked, Current, Completed }`。 - `DialogManager`:自定义解析器(`[段落]{函数}`、`speaker:句子`、`$Choice(...)`、`$Condition{...}`、内联 `{@表达式}`),从 `Resources/Story/[chapter]/Dialogs/` 加载 `TextAsset`。 - `StoryInterpreters`(DynamicExpresso):`SetVariable`/`GetVariable`/`GenerateDialogBlock`/`GenerateSongBlock`/`SetUnlockKey`——不仅分支对话,还动态生成故事树节点、解锁歌曲。 - `DialogUI`(`DialogUIPage`/`DialogContentFrame`/`DialogTextUI`/`ChoiceGroupUI`/`ChoiceButtonUI`):历史记录式(scrollback)呈现,点击推进。 - 存档:`GameSaveManager.StorySaveModule`(block 存档、`storyVariables`、`selectedChoices`),ES3 持久化到 `persistentDataPath/StorySaves/[Chapter].json`。 - 剧情系统实例位于 `MenuScene.unity`(`StoryManager`、`DialogManager` 均在其中)。 **Yarn Spinner 3.2.1 关键 API(已核实包源码):** - `DialogueRunner`:`StartDialogue(nodeName)`;`UnityEvent onDialogueStart` / `onDialogueComplete`;`AddCommandHandler(name, Delegate)`;`dialogueViews`(`DialoguePresenterBase` 列表)。异步架构(`YarnTask`)。 - `DialoguePresenterBase`(`Runtime/Views/DialoguePresenterBase.cs`):override `YarnTask RunLineAsync(LocalizedLine, LineCancellationToken)`、`YarnTask RunOptionsAsync(DialogueOption[], LineCancellationToken)`、`YarnTask OnDialogueStartedAsync()`、`YarnTask OnDialogueCompleteAsync()`;可选 `OnNodeEnter/OnNodeExit`。 - `VariableStorageBehaviour`(`Runtime/Storage/VariableStorageBehaviour.cs`):自定义变量存储基类。 - `UnityLocalisedLineProvider`(`Runtime/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. **仅优先文本块(TextBlock)**;Song/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 StoryBlockDefinition`:`string blockId`(唯一,替代旧 `blockName`/`blockID` 二元)、`StoryBlockType type`、`Vector2 position`、`Vector2 size`、`StoryBlockState initialState`、`List nextBlockIds`、`UnlockCondition unlockCondition`。 - 类型专属数据(用 Odin `[ShowIf]` 按 `type` 显示):`string yarnNodeName` + `string titleKey`(Text);`string songName`(Song 占位);`string tutorialName`(Tutorial 占位)。 - `[Serializable] class UnlockCondition`:`List requiredCompletedBlockIds`;`List variableConditions`;方法 `bool IsSatisfied(Func getVar, Func isCompleted)`。 - `[Serializable] class VariableCondition`:`string variableName`、`enum Comparison { Equal, NotEqual, Greater, GreaterOrEqual, Less, LessOrEqual }`、`int value`;`bool 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 : SerializedScriptableObject`:`string chapterIndex`、`List blocks`、`YarnProject yarnProject`(该章节编译产物引用)、`AssetReference/string` 本地化 String Table 集合名(供 Presenter/Provider 使用)。 - **公共 API:** `StoryBlockDefinition GetBlock(string blockId)`、`IEnumerable GetInitialBlocks()`、`IEnumerable 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 displayNameKey`(Unity Localization 键)、`PortraitStyle portraitStyle`(`enum { Sprite, Live2D, Spine }` 预留)、`SerializedDictionary emotionSprites`(Sprite 方案)、`GameObject dynamicPortraitPrefab`(Live2D/Spine 预留,实现 `IPortraitRenderer`)。 - `**CharacterRegistry`(`[CreateAssetMenu(menuName="Ichni/Story/CharacterRegistry")] SerializedScriptableObject`):** `Dictionary characters`;`bool 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 nextBlockIds; }`(合并旧的按类型拆分的多个 Save 类为单一带 `blockId` 的 DTO;类型信息从 `StoryData` 反查,存档只存运行态);`[Serializable] class ChapterStorySave { string chapterIndex; List blocks; }`。 - **依赖:** A1(`StoryBlockState`、block 类型引用)。 #### A5. `Story/Tree/StoryManager.cs`(重建,覆盖现有) - **描述:** 剧情系统协调器(单例)。路径 `/ichni Official/Assets/Scripts/Story/Tree/StoryManager.cs`。 - **遵循模式:** 参考现有 `StoryManager` 单例 + `[Button] ClearAllStorySave()`;建议继承 `SLSUtilities.General.Singleton`(项目已有单例基类)。 - **内容:** 持有 `StoryTreeController treeController`、`StoryDialogueController dialogueController`、`StoryUIPage storyUIPage`、`CharacterRegistry characterRegistry`、`Dictionary 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.cs` 的 `SetUpStoryline`/`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 blocks`。 - **解锁判定:** `IsSatisfied` 的 `getVar` 委托指向 `StoryVariableStorage`(A8),`isCompleted` 指向已完成 block 集合。 - **依赖:** A1、A2、A4、A8、`GameSaveManager.StorySaveModule`(重建后,见 A13)、`ChapterSelectionManager`。 #### A7. `Story/Tree/StoryBlockView.cs` + `TextBlockView.cs` + `SongBlockView.cs` + `TutorialBlockView.cs` + `BlockConnectorView.cs`(重建自 Blocks/*) - `**StoryBlockView`(抽象,`MonoBehaviour`):** `string blockId`、`StoryBlockState state`、`RectTransform inPort/outPort`、`Button button`;`virtual void ApplyState(StoryBlockState)`(更新视觉:锁定/当前/已完成外观);`abstract StoryBlockSave GetSave()`;`abstract void OnClicked()`。 - `**TextBlockView`:** 覆盖 `OnClicked()`:`state == Locked` → 忽略;否则 `StoryManager.instance.dialogueController.PlayBlock(this)`(传 `yarnNodeName`)。参考旧 `DialogBlockUI` 的 `button.onClick` + 状态判断。 - `**SongBlockView` / `TutorialBlockView`:** 占位实现——显示标题/状态,`OnClicked()` 暂仅 `Debug.Log`(保留旧 `SongBlockUI`/`TutorialBlockUI` 视觉字段,交互逻辑注释或占位)。 - `**BlockConnectorView`:** 沿用旧 `BlockConnectorUI`,用 `UILineRenderer` 在 `outPort`→`inPort` 画曲线;`SetCurve()`。 - **依赖:** A6、A9(对话触发)。 #### A8. `Story/Dialogue/StoryVariableStorage.cs`(新建) - **描述:** 自定义 Yarn 变量存储,桥接 `GameSaveManager.StorySaveModule.storyVariables`,ES3 持久化。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryVariableStorage.cs`。 - **遵循模式:** 继承 `Yarn.Unity.VariableStorageBehaviour`,实现其抽象/虚方法(`SetValue`/`TryGetValue`/`Clear`/`Contains` 等——**实现步骤时先 `printAsset` 或 view `VariableStorageBehaviour.cs` 核对当前签名**)。 - **要点:** Yarn 变量以 `$` 前缀命名;本存储把布尔/数值/字符串映射到 `storyVariables`(现为 `Dictionary`,需扩展为支持多类型,见 A13)。变量读写触发 `StorySaveModule` 持久化。 - **公共 API(供 A6 解锁判定复用):** `int GetIntVariable(string name)`、`bool HasVariable(string name)`。 - **依赖:** A13(`StorySaveModule`)。 #### 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)`(说话者高亮/变暗)。 - `**SpritePortraitRenderer`(`MonoBehaviour, IPortraitRenderer`):** 持有 `UnityEngine.UI.Image`;`Show` 从 `CharacterData.emotionSprites` 取 Sprite;`MoveTo`/`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 activePortraits`;依据 `CharacterData.portraitStyle` 实例化对应 renderer 到槽位。 - **依赖:** A3、A9。 #### A11. `Story/Dialogue/StoryPresentationCommands.cs`(新建) - **描述:** 表现类 Yarn 命令集合(`[YarnCommand]`)。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryPresentationCommands.cs`。 - **命令集(完整表现集):** 均委托到 `VNPortraitStage`/`VNDialoguePresenter`: - `<>` / `<>` / `<>` - `<>` / `<>` / `<>` / `<>` - `<>`(背景切换) - `<>`(Yarn 内置也可,视需要) - **注册方式:** 实例方法用 `[YarnCommand("show_portrait")]` 挂到与 `DialogueRunner` 同场景的组件上(Yarn 自动发现),或在 `StoryDialogueController.Awake` 用 `dialogueRunner.AddCommandHandler(...)` 显式注册。**实现步骤时先核对 `DialogueRunner.ActionRegistration.cs` 的 `AddCommandHandler` 重载与 `[YarnCommand]` 自动注册要求。** - **依赖:** A10、A12。 #### A12. `Story/Dialogue/StoryTreeCommands.cs`(新建) - **描述:** 影响故事树/解锁的 Yarn 命令(替代旧 `StoryInterpreters` 的 `GenerateDialogBlock`/`GenerateSongBlock`/`SetUnlockKey`)。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryTreeCommands.cs`。 - **命令集:** - `<>` → `StoryTreeController.UnlockBlock` - `<>` → 从当前 block 生成后继("混合式"动态插入)→ `StoryTreeController.InsertBlock` - `<>` → 复用旧 `SetUnlockKey` 逻辑:写入 `SongSaveModule.storyUnlockKeys`,并把"解锁提示消息"入队到对话结束后执行(见 A14 结束回调)。 - **依赖:** A6、A14、`GameSaveManager.SongSaveModule`、`ChapterSelectionManager`。 #### A13. `Scripts/Saving/GameSaveManager.cs` → `StorySaveModule`(重建剧情存档模块) - **描述:** 重建 `StorySaveModule`,适配新 DTO 与多类型变量。路径 `/ichni Official/Assets/Scripts/Saving/GameSaveManager.cs`(`StorySaveModule` 分部/内嵌类)。 - **改动:** - block 存档改用 `ChapterStorySave`/`StoryBlockSave`(A4),ES3 存 `persistentDataPath/StorySaves/[Chapter].json`(沿用旧路径约定)。 - `storyVariables` 扩展为支持 Yarn 多类型(float/bool/string)——建议 `Dictionary` 序列化 + 类型标记,或三个分字典;供 `StoryVariableStorage`(A8)读写。 - 保留 `selectedChoices` 语义(Yarn 选项选择记录),如需回放已完成对话可保留(见"实现说明")。 - 保留 `SongSaveModule.storyUnlockKeys` 相关接口(A12 依赖)。 - `ClearAllStoryline()` / `ClearStoryKeys()` 接口保留,供 `StoryManager.ClearAllStorySave` 调用。 - **注意:** `GameSaveManager` 含其它模块(`SongSaveModule`),改动**仅限剧情相关字段**,保持其余不变。**实现前先完整 view `GameSaveManager.cs` 的 `StorySaveModule` 现状。** #### 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 dialogueEndActions`(保留旧 `dialogEndActions` 语义,供 A12 `unlock_song` 入队解锁提示)。 - **事件接线:** 订阅 `dialogueRunner.onDialogueComplete`:若当前 block 为 `Current` → `treeController.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.CharacterName` 查 `CharacterRegistry` 取显示名(Unity Localization),高亮说话者立绘(`portraitStage.HighlightSpeaker`),打字机逐字显示 `line.TextWithoutCharacterName`;等待玩家点击推进(点击→`token` hurry/advance)。 - `RunOptionsAsync(DialogueOption[], LineCancellationToken)`:实例化选项按钮面板,等待选择,返回选中项;记录到 `selectedChoices`。 - `OnDialogueCompleteAsync()`:对话面板淡出。 - **要点:** 全程 `YarnTask` 异步;打字机需响应 `LineCancellationToken`(点击加速/跳过)。**实现步骤时先 view `DialoguePresenterBase.cs` 与 `LocalizedLine`/`DialogueOption` 类型核对属性名(`CharacterName`/`TextWithoutCharacterName`/`Text`)。** - **依赖:** A3、A10、Unity Localization。 #### A16. `Story/Dialogue/StoryFunctions.cs`(新建,可选) - **描述:** `[YarnFunction]` 辅助,让 `.yarn` 的 `<>` 能读取剧情状态。路径 `/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.cs`、`Story/Dialog/DialogModule.cs`、`Story/Dialog/StoryInterpreters.cs`;`Story/StoryUI/DialogUI/*`(`DialogUIPage`/`DialogContentFrame`/`DialogTextUI`/`ChoiceGroupUI`/`ChoiceButtonUI`);`Story/StoryUI/Blocks/*` 旧类(迁移到 A7 后删除);旧 `Story/StoryData/StoryData.cs`、旧 `Story/Dialog/StoryManager.cs`、旧 `Story/StoryUI/Storyline.cs`(被 A2/A5/A6 取代)。 - **注意:** DynamicExpresso 若无其它使用者,可保留包(供将来),但剧情不再依赖它。删除前 `grep` 确认无外部引用(尤其 `ChapterSelectionManager`/`Menu` 对 `DialogManager` 的调用点,需改为 `StoryDialogueController`)。 ------ ### B. Text Files(`.yarn` 脚本) #### B1. 示例章节 Yarn 脚本 - **描述:** 每个 TextBlock 对应一个 Yarn 节点。路径示例 `/ichni Official/Assets/Story/Chapters/Chapter1/Chapter1.yarn`(新目录,脱离旧 `Resources/Story/.../Dialogs`)。 - **内容:** 为若干 TextBlock 各写一个 `title:` 节点,含 speaker 行、`<>`/`<>` 等表现命令、`->` 选项、`<>` 条件分支、`<>`、必要处 `<>`/`<>`。用于验证管线。 - **依赖:** A11/A12 命令、A8 变量。 ------ ### C. Unity Assets(ScriptableObject / 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. `StoryData`(Chapter1)资产 - **名称:** `/ichni Official/Assets/Story/Chapters/Chapter1/Chapter1_StoryData.asset`。 - **描述:** 定义 Chapter1 故事树 block、连接、初始状态、`yarnNodeName`、解锁条件,引用 C1 YarnProject。 - **Bezi Actions:** `createOrUpdateScriptableObject`(`scriptableObjectType: "Ichni.Story.StoryData"`)。 - **依赖:** A2、C1。 #### C3. `CharacterRegistry` + 角色资产 - **名称:** `/ichni Official/Assets/Story/Characters/CharacterRegistry.asset`。 - **描述:** 登记若干测试角色(characterId、displayNameKey、emotion→Sprite)。 - **Bezi Actions:** `createOrUpdateScriptableObject`(`scriptableObjectType: "Ichni.Story.CharacterRegistry"`)。 - **依赖:** A3。 ------ ### D. Unity Actions(场景 / 预制体 / UI 接线,均在 `MenuScene.unity`) #### D1. Yarn 运行时对象 - **描述:** 在剧情 UI 层新建 `StoryDialogueRoot` GameObject,挂 `DialogueRunner` + `UnityLocalisedLineProvider` + `StoryVariableStorage` + `StoryDialogueController` + `StoryPresentationCommands` + `StoryTreeCommands`;`DialogueRunner.yarnProject` 指向 C1,`dialogueViews` 加入 D2 的 `VNDialoguePresenter`,`variableStorage` 指向 `StoryVariableStorage`,`lineProvider` 指向 `UnityLocalisedLineProvider`。 - **Bezi Actions:** `createGameObject`、`addOrUpdateComponent`(逐个组件;`DialogueRunner` 字段引用用 `UnityObjectReference`)。 - **依赖:** A8/A11/A12/A14/A15、C1。 #### D2. VN 对话 UI - **描述:** 构建 VN 对话面板:全屏遮罩/背景 `Image`、立绘舞台(Left/Center/Right 槽位 `Image`)、对话框(说话者名 `TextMeshProUGUI` + 正文 `TextMeshProUGUI` + 继续指示)、选项面板(竖直布局,运行时实例化选项按钮)。挂 `VNDialoguePresenter`、`VNPortraitStage`,槽位挂 `SpritePortraitRenderer`。 - **Bezi Actions:** `createGameObject`、`addOrUpdateComponent`(`UnityEngine.UI.Image`/`TMPro.TextMeshProUGUI`/`UnityEngine.UI.Button`)、`setRectTransformLayout`(锚点/拉伸)。 - **依赖:** A9/A10/A15。 #### D3. 故事树 UI 与 block 预制体 - **描述:** 构建/复用故事树滚动视图(`ScrollRect content`);创建 `TextBlockView`/`SongBlockView`/`TutorialBlockView`/`BlockConnectorView` 预制体(含 `Button`、`inPort`/`outPort`、状态视觉),供 `StoryTreeController` 实例化。将 `StoryManager`/`StoryTreeController`/`StoryUIPage` 组件接线到既有剧情 UI 对象。 - **Bezi Actions:** `createGameObject`、`addOrUpdateComponent`、`setRectTransformLayout`、`createPrefab`(block 预制体)。 - **依赖:** 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 的 `.yarn`;Inspector 中设 Localisation 为 Unity 并关联 String Table Collection;导入生成基础语言行。(Bezi Actions 无 YarnProject 创建能力。) #### E3. 核对 Yarn 命令/呈现器 API 签名 - **描述:** 实现 A8/A11/A15 前,在 Editor 中确认 `VariableStorageBehaviour`、`DialoguePresenterBase`、`LocalizedLine`、`DialogueOption`、`DialogueRunner.AddCommandHandler`/`[YarnCommand]` 的确切签名(版本 3.2.1),避免 API 漂移导致编译错误。 ------ ## Key Patterns to Reuse - **Source:** `/ichni Official/Assets/Scripts/Story/StoryUI/Storyline.cs` — **Pattern:** 故事树构建、连接线生成、按章节 ES3 存取、`ScrollRect` 布局,迁移到 `StoryTreeController`。 - **Source:** `/ichni Official/Assets/Scripts/Story/Dialog/DialogManager.cs`(L138-152 完成分支、`dialogEndActions`)— **Pattern:** 对话完成 → 置 block `Completed` → 执行结束回调 → 存档,迁移到 `StoryDialogueController.onDialogueComplete`。 - **Source:** `/ichni Official/Assets/Scripts/Story/Dialog/StoryInterpreters.cs` — **Pattern:** `SetUnlockKey` 的歌曲解锁 + 消息入队逻辑,迁移到 `StoryTreeCommands.unlock_song`。 - **Source:** `SLSUtilities/General/Singleton` — **Pattern:** `StoryManager` 单例,替换手写 `instance = this`。 - **Source:** 旧 `StoryData.cs` 的 Odin `[FoldoutGroup("$blockName")]`/`[InlineProperty]` — **Pattern:** 新 `StoryBlockDefinition` 的 Inspector 组织。 - **Source:** `DialogCharacter`(旧 `DialogModule.cs`:`name`/`title`/`emotions`)— **Pattern:** 演进为 `CharacterData`(`characterId`/`displayNameKey`/`emotionSprites`)。 ------ ## Implementation Notes - **Yarn 3.x 异步:** Presenter 全部 `YarnTask`;打字机与"点击推进"须响应 `LineCancellationToken`(hurry-up / cancel),否则点击无法加速或跳过。 - **命令注册:** `[YarnCommand]` 自动发现要求承载组件与 `DialogueRunner` 处于可解析范围;若自动发现不稳定,改用 `DialogueRunner.AddCommandHandler` 显式注册(在 `StoryDialogueController.Awake`)。 - **变量类型:** Yarn 变量支持 number/bool/string;旧 `storyVariables` 仅 `int`,须扩展多类型存储(A13),否则 `StoryVariableStorage` 无法承载布尔/字符串。 - **本地化桥接:** speaker 名解析来自 Yarn 行的 `CharacterName`;显示名走 Unity Localization(`CharacterData.displayNameKey`),正文走 `UnityLocalisedLineProvider` 关联的 String Table。 - **立绘抽象:** 不要在 Presenter/命令中直接写 `Image`;一律经 `IPortraitRenderer`,为 Live2D/Spine 预留。DOTween 已在项目中,用于跳动/移动/晃动。 - **混合式解锁:** 静态解锁在 `OnBlockCompleted` 重算 `UnlockCondition`;动态解锁由 `<>`/`<>` 命令在对话中触发——两者都最终调用 `StoryTreeController`,保证存档一致。 - **存档兼容:** 这是破坏性重构,旧 `StorySaves/*.json`/`Choices.json` 结构不兼容;建议在 `StorySaveModule` 加版本号或提供一次性清档(`ClearAllStorySave` 已存在)。 - **场景范围:** 所有场景接线在 `MenuScene.unity`;剧情为 Official 独有,无需同步到 `IchniCreatorStudio`。 - **可运行性分阶段:** 每阶段完成后项目应可编译、可进入剧情页。先落数据/树(占位对话),再接 Yarn 管线,再做 VN 呈现与命令,最后清理旧系统。 ------ ## TODO **阶段 1 — 数据与故事树骨架(可编译、可显示占位树)** - 新建 `StoryBlockDefinition.cs`(block 定义 + `UnlockCondition`/`VariableCondition` + `StoryBlockType`) - 重建 `StoryData.cs`(章节 block 列表 + `YarnProject` 引用 + 查询 API) - 新建 `CharacterData.cs` + `CharacterRegistry.cs`(含 `portraitStyle` 预留) - 重建 `StorySave.cs`(`StoryBlockSave`/`BlockConnectorSave`/`ChapterStorySave`/`StoryVariablesSave`) - 重建 `StoryManager.cs`(单例协调器 + `StoryBlockState`) - 重建 `StoryTreeController.cs`(构建/解锁重算/存取,从 `Storyline` 迁移) - 重建 `StoryBlockView` + `TextBlockView`/`SongBlockView`/`TutorialBlockView`/`BlockConnectorView` - 用 `createOrUpdateScriptableObject` 生成 `Chapter1_StoryData.asset` 与 `CharacterRegistry.asset`(用户已手动创建三个 SO 资产) - 用 Bezi Actions 搭建故事树滚动视图与 block 预制体,接线 `StoryManager`/`StoryTreeController`/`StoryUIPage`(改为用户手动制作,见阶段总结的手动步骤) - (提前合并原阶段 5 清理)删除旧剧情/对话脚本网:`DialogManager`/`DialogModule`/`StoryInterpreters`/`Storyline`/旧 `StoryManager`/旧 Block UIs/`DialogUI/*`/旧 `StoryData`/旧 `StorySave`;`MenuManager` 移除失效 `dialogUIPage` 字段 **阶段 2 — Yarn 对话管线(点击文本块可跑通 Yarn 节点)** - 安装 `com.unity.localization` 并创建 Locale / String Table Collection(手动) - 手动创建 `Chapter0.yarnproject` 并设为 Unity Localisation,编写示例 `Chapter0.yarn` - 新建 `StoryVariableStorage.cs`(`VariableStorageBehaviour` 桥接存档,多类型变量) - 重建 `GameSaveManager.StorySaveModule`(新 DTO + 多类型 `storyVariables`,保留 `SongSaveModule` 不变)——已在阶段 1 StorySave 步骤中一并完成:树+选项按章节(单 key 容器),变量全局(float/string/bool 三类型,匹配 Yarn 契约) - 新建 `StoryDialogueController.cs`(`PlayBlock` + `onDialogueComplete` → 置 `Completed`/重算解锁/存档/结束回调) - 在 `MenuScene` 搭建 `StoryDialogueRoot`(`DialogueRunner` + `StoryVariableStorage` + 控制器),接线到 `Chapter0.yarnproject`。注:项目未安装 `com.unity.localization`,`Chapter0.yarnproject` 使用 Yarn 内置本地化;`DialogueRunner` 在无 lineProvider 时会自动创建 `BuiltinLocalisedLineProvider`,故阶段 2 无需接 Unity Localization。呈现器(dialoguePresenters)留空,待阶段 3 接入 VN 呈现器。 **阶段 3 — VN 呈现与立绘** - 新建 `VNDialoguePresenter.cs`(`DialoguePresenterBase`:打字机行、说话者显示名、选项面板、淡入淡出,响应 `LineCancellationToken`) - 新建 `IPortraitRenderer.cs` + `SpritePortraitRenderer.cs`(DOTween 跳动/移动/晃动) - 新建 `VNPortraitStage.cs`(多槽位管理,按 `portraitStyle` 实例化 renderer) - 用 Bezi Actions 搭建 VN 对话 UI(背景/立绘槽位/对话框/选项面板),接入 `DialogueRunner.dialogueViews` **阶段 4 — 表现与树命令、条件** - 新建 `StoryPresentationCommands.cs`(`show/hide/move/jump/shake_portrait`、`set_emotion`、`set_background`) - 新建 `StoryTreeCommands.cs`(`unlock_block`/`insert_block`/`unlock_song`,迁移旧 `SetUnlockKey` 歌曲解锁+消息入队) - 新建 `StoryFunctions.cs`(`[YarnFunction]` `is_completed`/`story_var`,按需) - 在示例 `.yarn` 中覆盖表现命令、选项、`<>` 条件、`<>`、`<>` 并验证 **阶段 5 — 清理旧系统** - `grep` 确认并改接 `ChapterSelectionManager`/`Menu` 等对 `DialogManager` 的调用点到 `StoryDialogueController` - 删除 `DialogManager.cs`/`DialogModule.cs`/`StoryInterpreters.cs`/`DialogUI/*`/旧 `Blocks/*`/旧 `Storyline.cs`/旧 `StoryData.cs`/旧 `StoryManager.cs` - 重新编译并清零剧情相关编译错误