541 lines
20 KiB
Markdown
541 lines
20 KiB
Markdown
# 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.*` 常量,不手写字符串。
|