chore: initial commit
This commit is contained in:
540
Docs/Design/34_EventChainSystem.md
Normal file
540
Docs/Design/34_EventChainSystem.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# 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
|
||||
/// <summary>
|
||||
/// 事件链触发条件基类。
|
||||
/// EventChainManager 调用 IsMet() 评估,调用 Register/Unregister 订阅相关事件。
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// 事件链执行动作基类。
|
||||
/// ExecuteAsync 可以是即时的或协程(如等待过场动画播放完)。
|
||||
/// </summary>
|
||||
public abstract class ChainAction : ScriptableObject
|
||||
{
|
||||
/// <returns>Coroutine IEnumerator(可 yield 等待)</returns>
|
||||
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<string> OnBossDefeated;
|
||||
public event Action<string> OnCollectiblePickedUp;
|
||||
public event Action<int> OnAbilityUnlocked;
|
||||
public event Action<string> OnRoomEntered;
|
||||
public event Action<string> OnDialogueCompleted;
|
||||
|
||||
// 已完成链 ID(本帧可能触发 ChainCompletedCondition)
|
||||
public event Action<string> OnChainCompleted;
|
||||
|
||||
readonly HashSet<string> _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<string> 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.*` 常量,不手写字符串。
|
||||
Reference in New Issue
Block a user