# 34 · 事件链系统(Event Chain System) > **命名空间** `BaseGames.EventChain` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.Progression` · `BaseGames.World`(SaveManager)· `BaseGames.Dialogue`(可选) --- ## 目录 1. [系统总览](#1-系统总览) 2. [EventChainSO — 链式数据](#2-eventchainsо--链式数据) 3. [ChainCondition — 触发条件](#3-chaincondition--触发条件) 4. [ChainAction — 执行动作](#4-chainaction--执行动作) 5. [EventChainManager](#5-eventchainmanager) 6. [SaveData 集成](#6-savedata-集成) 7. [典型用例](#7-典型用例) 8. [事件频道](#8-事件频道) 9. [编辑器友好设计](#9-编辑器友好设计) --- ## 1. 系统总览 事件链系统解决"游戏世界状态级联变化"的问题:击败 Boss → 开启大门 → 更新地图 → 改变 NPC 台词 → 触发过场动画。这些联动如果硬编码在各系统中会产生高耦合;事件链系统将其外置为纯数据配置,所有逻辑通过 SO 驱动。 ``` 事件链系统职责: ├─ EventChainSO → 描述"当 Condition 满足时,依次执行 Actions" ├─ ChainCondition → 抽象基类,多种内置实现(事件触发/标志位/数值比较) ├─ ChainAction → 抽象基类,多种内置实现(开门/切换NPC对话/触发过场) ├─ EventChainManager → 运行时监听事件,评估链条,按序执行动作 └─ ChainStateRegistry → 追踪每条链的执行状态(未触发/执行中/已完成) ``` **零耦合原则**:`EventChainManager` 通过 SO 事件频道感知世界变化,通过 `ChainAction` 的接口方法触发目标系统,不持有任何具体系统的直接引用。 --- ## 2. EventChainSO — 链式数据 ```csharp [CreateAssetMenu(menuName = "EventChain/EventChain")] public class EventChainSO : ScriptableObject { [Header("基础")] public string chainId; // 全局唯一,如 "Chain_BossForest_Defeated" public bool repeatable; // false = 只触发一次(触发后 SaveData 记录) public float actionDelay = 0f; // 各 action 之间的延迟(秒),0 = 并发执行 [Header("触发条件(全部满足才触发)")] public ChainCondition[] conditions; [Header("执行动作(顺序执行)")] public ChainAction[] actions; } ``` --- ## 3. ChainCondition — 触发条件 ```csharp /// /// 事件链触发条件基类。 /// EventChainManager 调用 IsMet() 评估,调用 Register/Unregister 订阅相关事件。 /// public abstract class ChainCondition : ScriptableObject { public abstract void Register(EventChainManager manager); public abstract void Unregister(EventChainManager manager); public abstract bool IsMet(); } ``` ### 3.1 内置条件类型 | 条件 SO 类 | 参数 | 触发时机 | 示例 | |-----------|------|---------|------| | `BossDefeatedCondition` | `bossId: string` | `OnBossDefeated` 事件 | Boss_Forest 击败后 | | `FlagSetCondition` | `flagId: string` | 持续评估 | 某个全局标志位已设置 | | `AbilityUnlockedCondition` | `abilityType: AbilityType` | `OnAbilityUnlocked` 事件 | 获得冲刺能力后 | | `CollectibleCollectedCondition` | `itemId: string` | `OnCollectiblePickedUp` 事件 | 拾取关键道具后 | | `RoomEnteredCondition` | `sceneName: string` | `OnRoomEntered` 事件 | 玩家进入特定房间 | | `DialogueCompletedCondition` | `npcId`, `sequenceId` | `OnDialogueCompleted` 事件 | 完成某段 NPC 对话 | | `ChainCompletedCondition` | `chainId: string` | `OnChainCompleted` 事件 | 另一条事件链完成后 | ### 3.2 示例:BossDefeatedCondition ```csharp [CreateAssetMenu(menuName = "EventChain/Condition/BossDefeated")] public class BossDefeatedCondition : ChainCondition { public string bossId; bool _met; public override void Register(EventChainManager m) => m.OnBossDefeated += Check; public override void Unregister(EventChainManager m) => m.OnBossDefeated -= Check; public override bool IsMet() => _met; void Check(string id) { if (id == bossId) _met = true; } } ``` --- ## 4. ChainAction — 执行动作 ```csharp /// /// 事件链执行动作基类。 /// ExecuteAsync 可以是即时的或协程(如等待过场动画播放完)。 /// public abstract class ChainAction : ScriptableObject { /// Coroutine IEnumerator(可 yield 等待) public abstract IEnumerator ExecuteAsync(MonoBehaviour runner); } ``` ### 4.1 内置动作类型 | 动作 SO 类 | 参数 | 效果 | |-----------|------|------| | `OpenDoorAction` | `doorId: string` | 触发 `OnDoorOpened.Raise(doorId)` | | `SetFlagAction` | `flagId: string`, `value: bool` | 设置全局标志位并写 SaveData | | `UpdateMapAction` | `regionId: string` | 将区域标记为可见(地图联动)| | `PlayCutsceneAction` | `cutsceneId: string` | 触发 `CutsceneManager.Play()`,等待播放完成 | | `ChangeNPCDialogueAction` | `npcId`, `newSequenceId` | 更换 NPC 的当前对话 SO | | `SpawnObjectAction` | `prefab: GameObject`, `position` | 在指定位置生成对象 | | `WaitAction` | `seconds: float` | 纯等待(过场间隙) | | `RaiseEventAction` | `eventChannelSO` | 触发任意 VoidEventChannelSO | | `UnlockAbilityAction` | `abilityType: AbilityType` | 向玩家授予新能力 | | `PlayAudioAction` | `audioEventSO` | 播放 BGM/SFX 切换 | ### 4.2 示例:OpenDoorAction ```csharp [CreateAssetMenu(menuName = "EventChain/Action/OpenDoor")] public class OpenDoorAction : ChainAction { public string doorId; [SerializeField] StringEventChannelSO _onDoorOpened; public override IEnumerator ExecuteAsync(MonoBehaviour runner) { _onDoorOpened.Raise(doorId); yield break; // 即时执行,不阻塞后续动作 } } ``` ### 4.3 示例:PlayCutsceneAction(阻塞直到播完) ```csharp [CreateAssetMenu(menuName = "EventChain/Action/PlayCutscene")] public class PlayCutsceneAction : ChainAction { public string cutsceneId; [SerializeField] CutsceneEventChannelSO _onPlayCutscene; public override IEnumerator ExecuteAsync(MonoBehaviour runner) { bool done = false; _onPlayCutscene.Raise(cutsceneId, () => done = true); // 完成回调 yield return new WaitUntil(() => done); } } ``` --- ## 5. EventChainManager ```csharp namespace BaseGames.EventChain { public class EventChainManager : MonoBehaviour { [Header("所有事件链")] [SerializeField] EventChainSO[] _chains; [Header("事件频道(中继)")] [SerializeField] StringEventChannelSO _onBossDefeated; [SerializeField] StringEventChannelSO _onCollectiblePickedUp; [SerializeField] IntEventChannelSO _onAbilityUnlocked; [SerializeField] StringEventChannelSO _onRoomEntered; [SerializeField] StringEventChannelSO _onDialogueCompleted; // 中继事件,供 ChainCondition 订阅 public event Action OnBossDefeated; public event Action OnCollectiblePickedUp; public event Action OnAbilityUnlocked; public event Action OnRoomEntered; public event Action OnDialogueCompleted; // 已完成链 ID(本帧可能触发 ChainCompletedCondition) public event Action OnChainCompleted; readonly HashSet _completedChains = new(); void Awake() { // 从 SaveData 加载已完成链 foreach (var id in SaveManager.Instance.GetCompletedChains()) _completedChains.Add(id); } void OnEnable() { _onBossDefeated.OnEventRaised += id => { OnBossDefeated?.Invoke(id); EvaluateAll(); }; _onCollectiblePickedUp.OnEventRaised += id => { OnCollectiblePickedUp?.Invoke(id); EvaluateAll(); }; _onAbilityUnlocked.OnEventRaised += v => { OnAbilityUnlocked?.Invoke(v); EvaluateAll(); }; _onRoomEntered.OnEventRaised += id => { OnRoomEntered?.Invoke(id); EvaluateAll(); }; _onDialogueCompleted.OnEventRaised += id => { OnDialogueCompleted?.Invoke(id); EvaluateAll(); }; foreach (var chain in _chains) foreach (var cond in chain.conditions) cond.Register(this); } void OnDisable() { foreach (var chain in _chains) foreach (var cond in chain.conditions) cond.Unregister(this); } void EvaluateAll() { foreach (var chain in _chains) { if (!chain.repeatable && _completedChains.Contains(chain.chainId)) continue; // 一次性链已完成,跳过 if (Array.TrueForAll(chain.conditions, c => c.IsMet())) StartCoroutine(ExecuteChain(chain)); } } IEnumerator ExecuteChain(EventChainSO chain) { // 标记为进行中(防止重入) if (!chain.repeatable) _completedChains.Add(chain.chainId); foreach (var action in chain.actions) { yield return action.ExecuteAsync(this); if (chain.actionDelay > 0f) yield return new WaitForSeconds(chain.actionDelay); } // 链完成 SaveManager.Instance.SetChainCompleted(chain.chainId); OnChainCompleted?.Invoke(chain.chainId); } } } ``` --- ## 6. SaveData 集成 在 `31_SaveDataSchema_Unified.md` 基础上新增 `eventChains` 字段: ```json "eventChains": { "completedChains": [ "Chain_BossForest_Defeated", "Chain_Forest_DoorOpened" ], "flags": { "ForestBossDefeated": true, "TownNPCMetFirstTime": true, "AbyssUnlocked": false } } ``` ```csharp // SaveManager 扩展 public IEnumerable GetCompletedChains() => _saveData.eventChains.completedChains; public void SetChainCompleted(string chainId) { if (!_saveData.eventChains.completedChains.Contains(chainId)) { _saveData.eventChains.completedChains.Add(chainId); WriteDirty(); } } public bool GetFlag(string flagId) => _saveData.eventChains.flags.TryGetValue(flagId, out bool v) && v; public void SetFlag(string flagId, bool value) { _saveData.eventChains.flags[flagId] = value; WriteDirty(); } ``` --- ## 7. 典型用例 ### 7.1 击败 Boss → 开门 → 更新地图 → 改变 NPC 台词 **资产**:`Chain_BossForest_Defeated.asset` ``` chainId: "Chain_BossForest_Defeated" repeatable: false actionDelay: 0.5s conditions: [0] BossDefeatedCondition { bossId = "Boss_Forest" } actions: [0] WaitAction { seconds = 1.5 } ← 等待 Boss 死亡演出 [1] PlayCutsceneAction { cutsceneId = "CS_ForestBossDefeated" } [2] OpenDoorAction { doorId = "Door_Forest_ToRuins" } [3] UpdateMapAction { regionId = "Ruins" } [4] SetFlagAction { flagId = "ForestBossDefeated", value = true } [5] ChangeNPCDialogueAction { npcId = "NPC_Elder", newSequenceId = "Dialogue_Elder_PostForestBoss" } [6] PlayAudioAction { audioEventSO = BGMSnapshot_Victory } ``` ### 7.2 首次进入区域 → 触发介绍过场 **资产**:`Chain_FirstEnterCave.asset` ``` chainId: "Chain_FirstEnterCave" repeatable: false conditions: [0] RoomEnteredCondition { sceneName = "Room_Cave_01" } actions: [0] PlayCutsceneAction { cutsceneId = "CS_CaveIntro" } ``` ### 7.3 收集所有魅力 → 解锁隐藏区域 **资产**:`Chain_AllCharmsCollected.asset` ``` chainId: "Chain_AllCharmsCollected" repeatable: false conditions: [0] FlagSetCondition { flagId = "AllCharmsCollected" } ← 由 EquipmentManager 设置 actions: [0] SpawnObjectAction { prefab = SecretDoor_Prefab, position = (45, -12) } [1] RaiseEventAction { eventChannelSO = OnSecretUnlocked } ``` ### 7.4 完成对话 → 解锁商店新货物 **资产**:`Chain_NPCShopUnlock.asset` ``` chainId: "Chain_NPCShopUnlock" repeatable: false conditions: [0] DialogueCompletedCondition { npcId = "NPC_Merchant", sequenceId = "Dial_Merchant_Quest01" } actions: [0] SetFlagAction { flagId = "ShopStock_Tier2_Unlocked", value = true } ``` --- ## 8. 事件频道 | 频道资产 | 类型 | 发布方 | 主要订阅方 | |---------|------|--------|----------| | `OnChainCompleted.asset` | `StringEventChannelSO` | `EventChainManager` | 其他 ChainCondition(链间依赖)| | `OnDoorOpened.asset` | `StringEventChannelSO` | `OpenDoorAction` | `DoorController`(物理开门动画)| | `OnFlagChanged.asset` | `StringEventChannelSO` | `SetFlagAction` | `InteractableNPC`(条件对话)| | `OnSecretUnlocked.asset` | `VoidEventChannelSO` | 链中 `RaiseEventAction` | 隐藏区域管理器 | --- ## 9. 编辑器友好设计 ### EventChain Visualizer(EditorWindow) 菜单 `Zeling / Tools / EventChain Visualizer`,展示所有链的当前状态: ``` ┌─ EventChain Visualizer ─────────────────────────────────────┐ │ ● Chain_BossForest_Defeated [✅ 已完成] │ │ ● Chain_FirstEnterCave [✅ 已完成] │ │ ● Chain_AllCharmsCollected [⏳ 条件未满足] │ │ └ FlagSetCondition: AllCharmsCollected → false │ │ ● Chain_NPCShopUnlock [⏳ 条件未满足] │ │ └ DialogueCompleted: NPC_Merchant/Dial_Merchant_Quest01 │ │ │ │ [重置全部] [模拟触发 ___________] │ └─────────────────────────────────────────────────────────────┘ ``` ### 新建事件链 SOP 1. 右键 `Assets/Data/EventChains/` → `Create → EventChain/EventChain` 2. 命名规范:`Chain_{触发者}_{效果}.asset` 3. 按需从 `Create → EventChain/Condition/` 创建条件 SO 4. 按需从 `Create → EventChain/Action/` 创建动作 SO 5. 将链 SO 拖入 `EventChainManager._chains` 数组 6. 在 EventChain Visualizer 中模拟验证 ### 动作执行时序 Gizmos 在 Scene 视图中,`SpawnObjectAction` 的 `position` 字段显示为黄色菱形 Gizmo,辅助关卡设计师定位。 --- ## 10. 链执行诊断与失败排查 事件链触发失败是关卡设计中最常见的调试场景。以下提供系统化诊断手段。 ### 10.1 EventChainManager 诊断日志 `EventChainManager` 在以下时机输出带 `[EventChain]` 前缀的 Debug 日志(仅在 `UNITY_EDITOR || DEVELOPMENT_BUILD` 下有效): ```csharp // EventChainManager 内部日志钩子(已内置,无需手动添加) void EvaluateAll() { foreach (var chain in _chains) { #if UNITY_EDITOR || DEVELOPMENT_BUILD if (!chain.repeatable && _completedChains.Contains(chain.chainId)) { Debug.Log($"[EventChain] Skip (already completed): {chain.chainId}"); continue; } #else if (!chain.repeatable && _completedChains.Contains(chain.chainId)) continue; #endif bool allMet = Array.TrueForAll(chain.conditions, c => c.IsMet()); #if UNITY_EDITOR || DEVELOPMENT_BUILD if (!allMet) { var unmet = chain.conditions.Where(c => !c.IsMet()) .Select(c => c.GetType().Name); Debug.Log($"[EventChain] Blocked '{chain.chainId}': unmet → {string.Join(", ", unmet)}"); } #endif if (allMet) StartCoroutine(ExecuteChain(chain)); } } IEnumerator ExecuteChain(EventChainSO chain) { Debug.Log($"[EventChain] START '{chain.chainId}'"); // ...(原有逻辑) foreach (var action in chain.actions) { Debug.Log($"[EventChain] ► Action: {action.GetType().Name}"); yield return action.ExecuteAsync(this); // ... } Debug.Log($"[EventChain] DONE '{chain.chainId}'"); } ``` ### 10.2 EventChain Visualizer 诊断流程 ``` ┌─ EventChain Visualizer ─────────────────────────────────────────────┐ │ [运行时模式] 当前帧: 1234 存档槽: 0 │ │ │ │ ● Chain_BossForest_Defeated [✅ 已完成] │ │ ● Chain_FirstEnterCave [✅ 已完成] │ │ ● Chain_AllCharmsCollected [⏳ 条件未满足] │ │ └ FlagSetCondition: AllCharmsCollected → ✗ false │ │ ● Chain_NPCShopUnlock [⏳ 条件未满足] │ │ └ DialogueCompleted: NPC_Merchant/Dial_Merchant_Quest01 → ✗ │ │ ● Chain_HiddenRoom_Unlock [🔄 执行中] Action[2]/4 │ │ │ │ [强制触发 Chain_AllCharmsCollected] [重置全部] [导出状态 JSON] │ └─────────────────────────────────────────────────────────────────────┘ ``` **诊断步骤**(链不触发时的标准流程): | 步骤 | 操作 | 目的 | |------|------|------| | 1 | 打开 EventChain Visualizer | 查看链当前状态 | | 2 | 展开目标链的条件折叠 | 确认哪个 Condition 未满足 | | 3 | 点击"强制触发"验证 Actions | 排除 Condition 问题,隔离 Action 问题 | | 4 | 查看 Console 中 `[EventChain]` 日志 | 确认链是否进入 `EvaluateAll` 评估 | | 5 | 检查链是否在 Manager._chains 数组 | 遗漏注册是最常见原因 | | 6 | 检查 `repeatable = false` 且已完成 | 一次性链不会重复触发 | ### 10.3 常见链失败原因速查 | 症状 | 最可能原因 | 修复方向 | |------|-----------|---------| | 链完全不触发 | ① 未加入 Manager._chains ② 相关事件频道未连线(SO 字段为空)| 检查 Manager Inspector 和 SO 引用 | | 条件一直未满足 | `FlagSetCondition` 的 flagId 拼写与 `SetFlagAction` 不一致 | 使用 `AddressKeys` 或常量集中定义 flagId,避免手写字符串 | | Actions 执行一半停止 | `PlayCutsceneAction.ExecuteAsync` 回调未触发(`done = false` 永远不变)| 检查 CutsceneManager 是否正确调用完成回调 | | 链触发两次 | `repeatable = true` 且触发事件会多次广播 | 设为 `false` 或在 Action 中添加防重入检查 | | 链完成但 SaveData 未记录 | `SaveManager.SetChainCompleted()` 内部 `WriteDirty()` 未被调用 | 检查 SaveManager 的 WriteDirty 逻辑 | ### 10.4 flagId 集中定义规范 为避免 flagId 字符串散落在多个 SO 中难以追查,所有全局标志位 ID 在专用静态类中集中声明: ```csharp // Assets/Scripts/Core/GameFlags.cs public static class GameFlags { // Boss public const string ForestBossDefeated = "ForestBossDefeated"; public const string CaveBossDefeated = "CaveBossDefeated"; // NPC 状态 public const string TownNPCMetFirstTime = "TownNPCMetFirstTime"; public const string MerchantQuest01Done = "MerchantQuest01Done"; // 隐藏区域 public const string AllCharmsCollected = "AllCharmsCollected"; public const string HiddenRoomUnlocked = "HiddenRoomUnlocked"; // 商店 public const string ShopStockTier2Unlocked = "ShopStock_Tier2_Unlocked"; } ``` **规范**:`SetFlagAction`、`FlagSetCondition`、`SetDebugFlag` 一律引用 `GameFlags.*` 常量,不手写字符串。