548 lines
21 KiB
Markdown
548 lines
21 KiB
Markdown
# 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<string> _activeFlags = new();
|
||
|
||
// 设置标志(由 NarrativeStateMachine 调用)
|
||
public void SetFlag(string flagKey) => _activeFlags.Add(flagKey);
|
||
|
||
// 查询标志(NPC、环境物件等查询)
|
||
public bool HasFlag(string flagKey) => _activeFlags.Contains(flagKey);
|
||
|
||
// 运行时重置(场景卸载时不重置,存档后持久化)
|
||
public void LoadFromSave(IEnumerable<string> flags)
|
||
{
|
||
_activeFlags.Clear();
|
||
foreach (var f in flags) _activeFlags.Add(f);
|
||
}
|
||
|
||
public IReadOnlyCollection<string> 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
|
||
/// <summary>
|
||
/// 扩展 InteractableNPC,支持根据世界状态标志切换对话版本
|
||
/// </summary>
|
||
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
|
||
/// <summary>
|
||
/// 仅在编辑器中存在,用于标记场景中具有叙事意义的物件
|
||
/// 运行时不执行任何逻辑,纯粹供团队沟通和 QA 检视用
|
||
/// </summary>
|
||
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<string> ActiveNarrativeNodes { get; set; } = new();
|
||
|
||
// 已激活的世界状态标志
|
||
[JsonProperty("worldStateFlags")]
|
||
public List<string> WorldStateFlags { get; set; } = new();
|
||
|
||
// NPC 当前位置(覆盖默认生成位置)
|
||
[JsonProperty("npcLocations")]
|
||
public Dictionary<string, NPCLocationData> NpcLocations { get; set; } = new();
|
||
|
||
// 已收集的 Lore 道具 ID 集合
|
||
[JsonProperty("collectedLoreIds")]
|
||
public HashSet<string> 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*
|