# 50 · 叙事设计系统(Narrative Design System) > **命名空间** `BaseGames.Narrative` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.Dialogue`(对话系统)· `BaseGames.Quest`(任务链)· `BaseGames.World`(WorldStateRegistry) > **关联** 15_DialogueSystem · 34_EventChainSystem · 38_QuestSystem · 18_CutsceneSystem --- ## 目录 1. [叙事设计哲学](#1-叙事设计哲学) 2. [故事状态机(NarrativeStateMachine)](#2-故事状态机narrativestatemachine) 3. [世界状态注册表(WorldStateRegistry)](#3-世界状态注册表worldstateregistry) 4. [NPC 响应层:台词版本系统](#4-npc-响应层台词版本系统) 5. [NPC 位置迁移系统](#5-npc-位置迁移系统) 6. [Lore 碎片系统](#6-lore-碎片系统) 7. [隐性叙事(Environmental Storytelling)](#7-隐性叙事environmental-storytelling) 8. [分支结局门控](#8-分支结局门控) 9. [叙事时序图(世界事件一览)](#9-叙事时序图世界事件一览) 10. [SaveData 集成](#10-savedata-集成) 11. [事件频道](#11-事件频道) 12. [编辑器友好设计](#12-编辑器友好设计) --- ## 1. 叙事设计哲学 泽灵的叙事体系以《空洞骑士》为精神基础,以**隐性叙事优先、台词碎片化、世界响应动态化**为三大原则: | 原则 | 含义 | 实现方式 | |------|------|---------| | **隐性叙事优先** | 故事通过场景、尸骸、环境物件传达,而非大段台词 | `EnvNarrativeMarker`(见 §7) | | **台词碎片化** | NPC 每次对话只透露一点信息,多次交互拼凑完整故事 | `DialogueSequenceSO` 多版本 | | **世界响应动态化** | 击败 Boss、完成任务、拿到关键道具后,世界会发生可见变化 | `WorldStateRegistry` + NPC 迁移 | **刻意不做的设计**: - ❌ 不设主线任务追踪(类 RPG 的"前往X地点"箭头)——探索本身是奖励 - ❌ 不设全程语音——文字台词更具像素风氛围,也节省本地化成本 - ❌ 不设强制叙事打断——所有对话均由玩家主动触发 --- ## 2. 故事状态机(NarrativeStateMachine) ### 2.1 主线故事节点 游戏世界的叙事分为若干**故事节点(NarrativeNodeSO)**,按玩家行动依次解锁: ```csharp [CreateAssetMenu(menuName = "Narrative/NarrativeNode")] public class NarrativeNodeSO : ScriptableObject { [Header("标识")] public string nodeId; // 如 "Node_ForestBossDefeated" public string displayName; // 编辑器显示名(不出现在游戏中) [Header("解锁条件")] public string[] prerequisiteNodeIds; // 所有前置节点必须已激活 public string requiredBossDefeated; // 可选:需击败某 Boss public string requiredItemCollected; // 可选:需获取某道具 public string requiredQuestId; // 可选:需完成某任务 [Header("激活效果")] public NarrativeEffect[] effects; // 激活时执行的效果列表(见下方) [Header("关联")] [TextArea(2, 5)] public string designerNotes; // 设计师批注(不进游戏) } [Serializable] public class NarrativeEffect { public NarrativeEffectType type; public string parameter; // 根据 type 含义不同 // type 枚举: // SetWorldState → parameter = "BossX_Defeated" // MigrateNPC → parameter = "NPC_MerchantA → Location_Cave_HiddenRoom" // UnlockLoreItem → parameter = "Lore_ForestAncientTablet_01" // TriggerEventChain → parameter = eventChain ID // OpenSecretArea → parameter = scene address + 门 lockId } ``` ### 2.2 主线节点时序 ``` [游戏开始] │ ▼ Node_GameStart │ 玩家进入 Forest 区域 ▼ Node_ForestExplored │ 击败蛛网守卫 ▼ Node_ForestBossDefeated ──→ 幻境碎片 #1 可收集 │ 森林 NPC 迁移(商人移至洞穴入口) │ 进入 Cave 区域 ▼ Node_CaveEntered ──→ NPC 台词版本切换 │ 击败蚀骨蠕虫 ▼ Node_CaveBossDefeated ──→ 幻境碎片 #2 可收集 │ 解锁废墟冲刺能力房间 ▼ Node_DashAbilityUnlocked │ 进入 Ruins 区域 ▼ Node_RuinsEntered │ 击败废墟遗骑士 ▼ Node_RuinsBossDefeated ──→ 幻境碎片 #3(触发隐藏记忆序列) │ ▼ Node_AbyssRevealed ──→ 开启深渊地图显示 │ 击败深渊之喉 ▼ Node_AbyssBossDefeated ──→ 分支节点(见 §8) │ ▼ Node_CoreUnlocked ──→ 最终区域开放 │ 击败最终 Boss ▼ Node_TrueEndingGate(需满足额外条件,见 §8) 或 Node_NormalEnding ``` --- ## 3. 世界状态注册表(WorldStateRegistry) `WorldStateRegistry` 是全局单例(SO 形式,零耦合注入),存储所有已激活的世界状态标志: ```csharp [CreateAssetMenu(menuName = "Narrative/WorldStateRegistry")] public class WorldStateRegistry : ScriptableObject { // 运行时标志集合(字符串 Key) private readonly HashSet _activeFlags = new(); // 设置标志(由 NarrativeStateMachine 调用) public void SetFlag(string flagKey) => _activeFlags.Add(flagKey); // 查询标志(NPC、环境物件等查询) public bool HasFlag(string flagKey) => _activeFlags.Contains(flagKey); // 运行时重置(场景卸载时不重置,存档后持久化) public void LoadFromSave(IEnumerable flags) { _activeFlags.Clear(); foreach (var f in flags) _activeFlags.Add(f); } public IReadOnlyCollection GetAllFlags() => _activeFlags; } ``` ### 标志命名规范 ``` <类别>_<对象>_<状态> Boss_SpiderGuard_Defeated NPC_MerchantA_Migrated_Cave Area_AbyssBottomLake_Revealed Item_ForestFragmentStone_Collected Quest_FindMushroom_Completed Lore_CaveAncientTablet_01_Read ``` --- ## 4. NPC 响应层:台词版本系统 每个 NPC 根据当前 `WorldStateRegistry` 标志,动态选择对话版本: ```csharp /// /// 扩展 InteractableNPC,支持根据世界状态标志切换对话版本 /// public class NarrativeNPC : InteractableNPC { [Header("台词版本集")] [SerializeField] DialogueVersion[] _dialogueVersions; // 从高到低优先级排列 [SerializeField] DialogueSequenceSO _defaultDialogue; // 无条件满足时的默认台词 [SerializeField] WorldStateRegistry _worldState; // SO 注入 protected override DialogueSequenceSO GetCurrentDialogue() { // 从最高优先级版本开始检查,返回第一个满足条件的版本 foreach (var version in _dialogueVersions) { if (version.CheckConditions(_worldState)) return version.dialogue; } return _defaultDialogue; } } [Serializable] public class DialogueVersion { public string versionLabel; // 编辑器显示名(如"森林Boss击败后") public DialogueSequenceSO dialogue; public string[] requiredFlags; // 全部满足才激活此版本(AND 关系) public string[] blockedByFlags; // 有任意一个 = 此版本不激活(NOT 关系) public bool CheckConditions(WorldStateRegistry registry) { foreach (var f in requiredFlags) if (!registry.HasFlag(f)) return false; foreach (var f in blockedByFlags) if (registry.HasFlag(f)) return false; return true; } } ``` ### NPC 台词版本示例:流浪商人 MerchantA | 版本 | 触发条件 | 台词主题 | |------|---------|---------| | `default` | 无条件 | 初见寒暄,暗示森林危险 | | `forest_boss_dead` | `Boss_SpiderGuard_Defeated` | 感谢玩家,给出洞穴线索 | | `cave_entered` | `Area_Cave_Entered` | 已迁移到洞穴,新的商品 | | `ruins_boss_dead` | `Boss_RuinsKnight_Defeated` | 讲述古代遗迹的秘密碎片 | | `all_bosses_dead` | 四大 Boss 均击败 | 最终台词,暗示结局方向 | --- ## 5. NPC 位置迁移系统 部分 NPC 会随着故事进程**物理移动到地图的不同位置**,增强世界活性感: ```csharp [CreateAssetMenu(menuName = "Narrative/NPCMigration")] public class NPCMigrationConfigSO : ScriptableObject { public string npcId; [Serializable] public struct MigrationStep { public string triggerFlag; // 触发迁移的 WorldState 标志 public string targetSceneAddress; // 目标场景 public Vector2 spawnPosition; // 目标房间内的具体位置 [TextArea(1, 2)] public string migrationNote; // 设计注释 } public MigrationStep[] steps; } ``` **NPC 迁移流程**: ``` 1. NarrativeStateMachine 激活 Node(如 Node_ForestBossDefeated) 2. 执行 NarrativeEffect[type=MigrateNPC] 3. NPCMigrationManager 查询该 NPC 的 MigrationConfigSO 4. 找到 triggerFlag 匹配的 MigrationStep 5. 更新 SaveData.npcLocations[npcId] = targetSceneAddress + position 6. 下次玩家进入目标场景时,NPCSpawner 从 SaveData 读取位置实例化 NPC ``` ### NPC 迁移时序表 | NPC | 初始位置 | 第一次迁移 | 第二次迁移 | 最终位置 | |-----|---------|-----------|-----------|---------| | 流浪商人 MerchantA | Forest_Main(树干旁) | Forest Boss 击败后 → Cave_Entrance(洞穴入口灯旁) | Ruins Boss 击败后 → Ruins_Hub(废墟核心广场) | Core_Antechamber(最终区域前厅)| | 神秘老者 ElderSage | Forest_Shrine(森林神社) | 获得冲刺后 → Ruins_Library(废墟图书馆废墟)| — | 同位置,台词持续更新 | | 流亡骑士 KnightExile | — (初始不存在)| Cave Boss 击败后解锁 → Cave_HiddenRoom | Abyss Boss 击败后 → Abyss_Plateau | 仅在 True Ending 路线出现最终台词 | --- ## 6. Lore 碎片系统 碎片化叙事道具——分散在世界各处的铭文、遗物、幻境记忆,拼凑世界背景故事: ### 6.1 LoreItemSO 数据结构 ```csharp [CreateAssetMenu(menuName = "Narrative/LoreItem")] public class LoreItemSO : ScriptableObject { [Header("标识")] public string loreId; // 如 "Lore_Cave_BonePillar_01" public string displayTitle; // 如 "锈蚀的骸骨柱" public Sprite thumbnail; // 图鉴中的缩略图 [Header("分类")] public LoreCategory category; // Inscription / Relic / Memory / EchoFragment public string regionId; // 所属区域 [Header("内容(本地化 Key)")] public string descLocKey; // 一般描述(拾取时弹出) public string[] loreParagraphKeys; // 详细 Lore 文段(图鉴中逐段展开) [Header("解锁条件")] public string requiredWorldFlag; // 空 = 无条件可收集 // 非空 = 需要某世界状态才可见/可收集 [Header("图鉴集成")] public string codexSectionId; // 对应图鉴章节(见 §6.3) public int codexOrderIndex; // 章节内排序 } ``` ### 6.2 Lore 类型与分布 | 类型 | 数量目标 | 分布规则 | 收集方式 | |------|---------|---------|---------| | **Inscription(铭文)** | 25~30 块 | 每区域 5~6 块,墙壁/地面/石碑 | 靠近自动触发 | | **Relic(遗物)** | 15~20 件 | 隐藏房间/精英怪掉落 | 拾取入背包 | | **Memory(幻境记忆)** | 5 段 | 每个主线 Boss 击败后解锁 | 与特定地点互动 | | **EchoFragment(幻境碎片)** | 10 个 | 散布全图秘密位置 | 拾取(与 §8 真结局联动)| ### 6.3 Lore 图鉴(Codex) 玩家收集的 Lore 道具自动归入图鉴界面(暂停菜单 → 图鉴): ``` 图鉴章节结构: ├─ 泽灵的历史 (Memory 类型,共 5 段) ├─ 扎根森林的传说 (Forest 区域 Inscription + Relic,共 8 条) ├─ 腐蚀洞穴的过去 (Cave 区域,共 7 条) ├─ 坍塌废墟的秘密 (Ruins 区域,共 8 条) ├─ 深渊裂隙的秘密 (Abyss 区域,共 6 条) └─ 幻境碎片 (EchoFragment,共 10 个,与真结局联动) ``` 图鉴中已收集的条目显示完整文字,未收集的显示 `???`(同时显示所在区域提示)。 --- ## 7. 隐性叙事(Environmental Storytelling) 不通过对话或文字,而通过**场景中的视觉摆放**传达叙事信息: ### 7.1 EnvNarrativeMarker 场景中的叙事标记组件,供关卡设计师放置在场景物件上,标注其叙事含义: ```csharp /// /// 仅在编辑器中存在,用于标记场景中具有叙事意义的物件 /// 运行时不执行任何逻辑,纯粹供团队沟通和 QA 检视用 /// public class EnvNarrativeMarker : MonoBehaviour { #if UNITY_EDITOR [TextArea(2, 6)] public string narrativeMeaning; // 这个物件传达什么叙事信息 public NarrativeImportance importance; // 核心/支线/氛围 public string relatedLoreId; // 关联的 LoreItemSO(可选) void OnDrawGizmos() { // 在 Scene 视图中用半透明图标标记叙事物件 Gizmos.color = importance == NarrativeImportance.Core ? new Color(1f, 0.3f, 0.3f, 0.8f) : new Color(0.3f, 0.8f, 1f, 0.6f); Gizmos.DrawIcon(transform.position + Vector3.up * 0.5f, "NarrativeMarker.png", true); } #endif } ``` ### 7.2 隐性叙事设计规范 | 规范 | 说明 | |------|------| | **尸体方向** | 倒在洞穴入口的人类骸骨,脸朝外(说明是从里面跑出来的,不是进去的)| | **武器磨损程度** | 废墟中的武器架,越靠近 Boss 房越锈蚀/越破损 | | **蜡烛的点燃状态** | 商人驻扎过的房间会有蜡烛燃烧,表示曾有人居住 | | **植物入侵** | 废墟区域越靠近核心,植物侵蚀越严重(时间流逝的隐喻)| | **涂鸦/铭刻** | 普通壁刻用中性色调,紧迫的警告用红色/破损字体 | --- ## 8. 分支结局门控 ### 8.1 结局类型 | 结局 | 触发条件 | 描述 | |------|---------|------| | **普通结局** | 击败最终 Boss | 泽灵完成使命,但世界的真相未被揭示 | | **真结局** | 普通结局条件 + 收集全部 10 个 EchoFragment + 完成 `Quest_KnightExile_Final` | 揭示世界背景真相,展示泽灵的真实身份 | | **隐藏结局** | 真结局条件 + 与全部 NPC 达到最高好感度 + 图鉴全收集 | 额外epilogue,完整故事补完 | ### 8.2 EndingGate 组件 ```csharp public class EndingGate : MonoBehaviour { [Header("结局配置")] [SerializeField] EndingType _endingType; [Header("真结局条件")] [SerializeField] int _requiredEchoFragments = 10; // 需要的幻境碎片数量 [SerializeField] string _requiredQuestId = "Quest_KnightExile_Final"; [Header("隐藏结局额外条件")] [SerializeField] int _requiredNPCMaxAffinity = 4; // 需要达到满好感的 NPC 数 [SerializeField] bool _requireFullCodex = true; // 是否需要图鉴全收集 [Header("依赖")] [SerializeField] WorldStateRegistry _worldState; [SerializeField] SaveData _saveData; // 注入 public EndingType EvaluateEnding() { bool trueConditions = _saveData.CollectedLoreIds.Count(id => id.StartsWith("Lore_Echo_")) >= _requiredEchoFragments && _saveData.CompletedQuestIds.Contains(_requiredQuestId); if (trueConditions) { bool hiddenConditions = _saveData.NPCMaxAffinityCount >= _requiredNPCMaxAffinity && (!_requireFullCodex || _saveData.IsCodexComplete()); return hiddenConditions ? EndingType.Hidden : EndingType.True; } return EndingType.Normal; } } ``` ### 8.3 结局提示设计规则 - **不明示**:游戏中不告诉玩家"收集X个碎片解锁真结局",仅通过 NPC 台词暗示("据说幻境碎片记录着世界诞生之初的秘密……") - **图鉴暗示**:图鉴章节"幻境碎片"收集到 5/10 时出现文字:"似乎还有另一半……" - **过场动画差异**:普通结局 vs 真结局的过场动画在相同骨架上有额外镜头和台词 --- ## 9. 叙事时序图(世界事件一览) ``` 故事时间线(按玩家行动推进) ══════════════════════════════════════════════════════════════ 开始 │ ──────────────────────────────────────── Forest 时期 ───── ├ 玩家习得基础移动技能 ├ 遇见 MerchantA(首次) ├ 收集 Forest 铭文 1-5(可选) ├ 击败蛛网守卫 │ → MerchantA 迁移至 Cave 入口 │ → Memory #1 解锁(森林守护的来源) │ → Cave 大门开启 │ ──────────────────────────────────────── Cave 时期 ──────── ├ ElderSage(可选,先遇见) ├ 收集 Cave 铭文 1-6(可选) ├ 击败蚀骨蠕虫 │ → Memory #2 解锁(洞穴的腐蚀起源) │ → KnightExile 出现在 Cave 隐秘房间 │ → 冲刺能力 Ruins 入口解锁 │ ──────────────────────────────────────── Ruins 时期 ─────── ├ 获得冲刺能力 │ → ElderSage 迁移至 Ruins 图书馆 ├ EchoFragment #1-3 可收集(隐藏位置) ├ 击败废墟遗骑士 │ → Memory #3 解锁(触发隐藏幻境序列) │ → 深渊地图出现 │ ──────────────────────────────────────── Abyss 时期 ─────── ├ EchoFragment #4-7 可收集 ├ KnightExile 迁移至 Abyss 高台(若已完成 Cave 任务链) ├ 击败深渊之喉 │ → Memory #4 解锁 │ → 分支:已收集 ≥ 8 EchoFragments → 解锁真结局隐藏路线 │ ──────────────────────────────────────── Core 时期 ──────── ├ EchoFragment #8-10 可收集(核心区域秘密房间) ├ KnightExile 在 Core 前厅等待(True Ending 路线) ├ 击败最终 Boss │ → 根据 EndingGate 判断 → Normal / True / Hidden 结局 └ 游戏结束 ``` --- ## 10. SaveData 集成 新增叙事专属存档字段: ```csharp public class NarrativeSaveData { // 已激活的故事节点 [JsonProperty("activeNarrativeNodes")] public List ActiveNarrativeNodes { get; set; } = new(); // 已激活的世界状态标志 [JsonProperty("worldStateFlags")] public List WorldStateFlags { get; set; } = new(); // NPC 当前位置(覆盖默认生成位置) [JsonProperty("npcLocations")] public Dictionary NpcLocations { get; set; } = new(); // 已收集的 Lore 道具 ID 集合 [JsonProperty("collectedLoreIds")] public HashSet CollectedLoreIds { get; set; } = new(); // 幻境碎片计数(True Ending 门控) [JsonProperty("echoFragmentCount")] public int EchoFragmentCount { get; set; } = 0; } [Serializable] public class NPCLocationData { [JsonProperty("sceneAddress")] public string SceneAddress { get; set; } [JsonProperty("position")] public SerializableVector2 Position { get; set; } } ``` --- ## 11. 事件频道 | 频道 SO | 类型 | 触发时机 | |---------|------|---------| | `OnNarrativeNodeActivated` | `StringEventChannelSO` | 故事节点激活(传入 nodeId)| | `OnWorldFlagSet` | `StringEventChannelSO` | WorldState 标志被设置(传入 flagKey)| | `OnLoreItemCollected` | `StringEventChannelSO` | Lore 道具被收集(传入 loreId)| | `OnNPCMigrated` | `StringEventChannelSO` | NPC 完成位置迁移(传入 npcId)| | `OnEchoFragmentCollected` | `IntEventChannelSO` | 幻境碎片收集(传入当前总数)| --- ## 12. 编辑器友好设计 - **NarrativeNodeSO** Inspector:显示依赖图(prerequisites 可视化有向图,UI Toolkit 实现) - **NarrativeNPC** Inspector:实时预览当前世界状态下会显示哪个台词版本(Play Mode 中显示生效版本名称) - **WorldStateRegistry** Inspector:列出所有已激活标志(Play Mode 只读,支持手动强制设置标志用于测试) - **LoreItem 场景标记**:在 Scene View 中用书本图标标记已放置 Lore 的位置,鼠标悬停显示 loreId 和收集状态 - **Codex 完成度 HUD**:编辑器模式在 Scene 右下角显示"Lore: 12/45"实时统计 --- *本文档版本 1.0 · 2026-04 · 关联 15_DialogueSystem / 34_EventChainSystem / 38_QuestSystem / 31_SaveDataSchema*