34 KiB
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):overrideYarnTask RunLineAsync(LocalizedLine, LineCancellationToken)、YarnTask<DialogueOption?> 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注册。
设计决策(已与你确认):
- 全部从零重构(数据、UI、存档)。
- VN 风格呈现 + 多角色立绘 + 表现类 Yarn 命令(如立绘跳动/移动)。
- 混合式解锁:故事树默认在
StoryData静态定义;同时允许 Yarn 命令在特殊情况下插入/解锁 block。 - 完整表现命令集 + 支持多立绘的 VN Presenter;立绘渲染做抽象(当前 Sprite,未来可换 Live2D/Spine)。
- 对话本地化桥接 Unity Localization(弃用 I2),由 Unity Localization 统一管理对话与 UI 文本。
- 仅优先文本块(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<string> nextBlockIds、UnlockCondition unlockCondition。- 类型专属数据(用 Odin
[ShowIf]按type显示):string yarnNodeName+string titleKey(Text);string songName(Song 占位);string tutorialName(Tutorial 占位)。 [Serializable] class UnlockCondition:List<string> requiredCompletedBlockIds;List<VariableCondition> variableConditions;方法bool IsSatisfied(Func<string,int> getVar, Func<string,bool> 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<StoryBlockDefinition> blocks、YarnProject 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 displayNameKey(Unity Localization 键)、PortraitStyle portraitStyle(enum { Sprite, Live2D, Spine }预留)、SerializedDictionary<string, Sprite> emotionSprites(Sprite 方案)、GameObject dynamicPortraitPrefab(Live2D/Spine 预留,实现IPortraitRenderer)。**CharacterRegistry([CreateAssetMenu(menuName="Ichni/Story/CharacterRegistry")] SerializedScriptableObject):**Dictionary<string, CharacterData> 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<string> nextBlockIds; }(合并旧的按类型拆分的多个 Save 类为单一带blockId的 DTO;类型信息从StoryData反查,存档只存运行态);[Serializable] class ChapterStorySave { string chapterIndex; List<StoryBlockSave> blocks; }。 - 依赖: A1(
StoryBlockState、block 类型引用)。
A5. Story/Tree/StoryManager.cs(重建,覆盖现有)
- 描述: 剧情系统协调器(单例)。路径
/ichni Official/Assets/Scripts/Story/Tree/StoryManager.cs。 - 遵循模式: 参考现有
StoryManager单例 +[Button] ClearAllStorySave();建议继承SLSUtilities.General.Singleton<StoryManager>(项目已有单例基类)。 - 内容: 持有
StoryTreeController treeController、StoryDialogueController dialogueController、StoryUIPage storyUIPage、CharacterRegistry characterRegistry、Dictionary<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.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):设该 blockCompleted,遍历所有Lockedblock 用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。
- 解锁判定:
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或 viewVariableStorageBehaviour.cs核对当前签名)。 - 要点: Yarn 变量以
$前缀命名;本存储把布尔/数值/字符串映射到storyVariables(现为Dictionary<string,int>,需扩展为支持多类型,见 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<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.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。 - 命令集:
<<unlock_block blockId>>→StoryTreeController.UnlockBlock<<insert_block blockId>>→ 从当前 block 生成后继("混合式"动态插入)→StoryTreeController.InsertBlock<<unlock_song songUnlockKey>>→ 复用旧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<string, string>序列化 + 类型标记,或三个分字典;供StoryVariableStorage(A8)读写。- 保留
selectedChoices语义(Yarn 选项选择记录),如需回放已完成对话可保留(见"实现说明")。 - 保留
SongSaveModule.storyUnlockKeys相关接口(A12 依赖)。 ClearAllStoryline()/ClearStoryKeys()接口保留,供StoryManager.ClearAllStorySave调用。
- block 存档改用
- 注意:
GameSaveManager含其它模块(SongSaveModule),改动仅限剧情相关字段,保持其余不变。实现前先完整 viewGameSaveManager.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<UnityAction> dialogueEndActions(保留旧dialogEndActions语义,供 A12unlock_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;等待玩家点击推进(点击→tokenhurry/advance)。RunOptionsAsync(DialogueOption[], LineCancellationToken):实例化选项按钮面板,等待选择,返回选中项;记录到selectedChoices。OnDialogueCompleteAsync():对话面板淡出。
- 要点: 全程
YarnTask异步;打字机需响应LineCancellationToken(点击加速/跳过)。实现步骤时先 viewDialoguePresenterBase.cs与LocalizedLine/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.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 行、<<show_portrait>>/<<jump_portrait>>等表现命令、->选项、<<if $var>>条件分支、<<set $var to ...>>、必要处<<unlock_block>>/<<unlock_song>>。用于验证管线。 - 依赖: 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 层新建
StoryDialogueRootGameObject,挂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: 对话完成 → 置 blockCompleted→ 执行结束回调 → 存档,迁移到StoryDialogueController.onDialogueComplete。 - Source:
/ichni Official/Assets/Scripts/Story/Dialog/StoryInterpreters.cs— Pattern: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.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;动态解锁由<<unlock_block>>/<<insert_block>>命令在对话中触发——两者都最终调用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 中覆盖表现命令、选项、<<if>> 条件、<<set>>、<<unlock_*>> 并验证
阶段 5 — 清理旧系统
grep 确认并改接 ChapterSelectionManager/Menu 等对 DialogManager 的调用点到 StoryDialogueController
删除 DialogManager.cs/DialogModule.cs/StoryInterpreters.cs/DialogUI/*/旧 Blocks/*/旧 Storyline.cs/旧 StoryData.cs/旧 StoryManager.cs
重新编译并清零剧情相关编译错误