# 14 · 进程系统(区域组织·能力门·Boss 进程) > **命名空间** `BaseGames.Progression` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.World`(SaveSystem)· `BaseGames.Player`(能力查询) --- ## 目录 1. [系统总览](#1-系统总览) 2. [区域(Region)划分](#2-区域region划分) 3. [能力门(AbilityGate)](#3-能力门abilitygate) 4. [进程锁(ProgressLock)](#4-进程锁progresslock) 5. [Boss 进程管理](#5-boss-进程管理) 6. [HP 容器升级流程](#6-hp-容器升级流程) 7. [收集品进度追踪](#7-收集品进度追踪) 8. [SaveData 字段扩展](#8-savedata-字段扩展) 9. [区域解锁与地图联动](#9-区域解锁与地图联动) 10. [事件频道](#10-事件频道) 11. [编辑器友好设计](#11-编辑器友好设计) --- ## 1. 系统总览 进程系统解决"类银河恶魔城游戏如何组织超大地图进程"的核心问题: ``` 进程系统职责: ├─ 区域划分 → RegionDefinitionSO 定义区域元数据 ├─ 能力门 → AbilityGate 阻挡没有对应能力的玩家 ├─ 进程锁 → ProgressLock 阻挡未满足条件的通路 ├─ Boss 进程 → Boss 击败解锁通往新区域的大门 ├─ HP 容器升级 → 拾取后 MaxHP++ 并触发 UI 演出 └─ 收集品追踪 → 完成度百分比(成就/结局条件) ``` --- ## 2. 区域(Region)划分 ### 区域枚举 游戏世界分为以下区域,按开放顺序排列: | 区域 ID | 中文名 | 特点 | Boss | 开放条件 | |---------|--------|------|------|---------| | `Forest` | 扎根森林 | 初始区域,基础敌人 | 蛛网守卫 | 无(起始区域)| | `Cave` | 腐蚀洞穴 | 黑暗区域,大量陷阱 | 蚀骨蠕虫 | 击败蛛网守卫 | | `Ruins` | 坍塌废墟 | 需要冲刺能力 | 废墟遗骑士 | 获得冲刺能力 | | `Abyss` | 深渊裂隙 | 需要双跳,垂直地图 | 深渊之喉 | 击败废墟遗骑士 | | `Core` | 核心熔炉 | 终局区域 | 最终Boss | 击败深渊之喉 | ### RegionDefinitionSO 每个区域一个 SO 资产,集中管理区域元数据: ```csharp [CreateAssetMenu(menuName = "Progression/RegionDefinition")] public class RegionDefinitionSO : ScriptableObject { public string regionId; // 如 "Cave"(与 AudioZone 的 regionId 一致) public string displayName; // 如 "腐蚀洞穴" public Color mapColor; // 地图 UI 上该区域的颜色标识 public Sprite mapIconSprite; // P1:地图图标 [Header("解锁条件")] public string requiredBossDefeated; // 空字符串 = 无条件 public AbilityType requiredAbility; // None = 无要求 [Header("关联房间")] public string[] roomSceneNames; // 该区域包含的所有场景名 public string bossSceneName; // Boss 房间场景名 public string entrySceneName; // 从外部进入该区域的第一个房间 } ``` 资产路径:`Assets/ScriptableObjects/Progression/Regions/` --- ## 3. 能力门(AbilityGate) `AbilityGate` 阻止未解锁对应能力的玩家通过,是 Metroidvania 空间门控的核心组件: ```csharp public class AbilityGate : MonoBehaviour { [SerializeField] AbilityType _requiredAbility; // 需要的能力类型 [SerializeField] GameObject _blockingObject; // 阻挡碰撞体(如悬崖、矮墙、荆棘) [SerializeField] GameObject _hintUI; // 提示 UI(如能力图标 + "???") void Start() { // 零耦合:从注入的 SaveData 查询,而非通过 SaveManager.Instance bool hasAbility = _saveData != null ? _saveData.HasAbility(_requiredAbility) : false; _blockingObject.SetActive(!hasAbility); _hintUI.SetActive(!hasAbility); } // _saveData 由 GameInitializer 在场景加载时注入 SaveData _saveData; // 监听能力解锁事件,实时更新(玩家在已进入的区域解锁能力时即时生效) void OnEnable() => _onAbilityUnlocked.OnEventRaised += OnAbilityUnlocked; void OnDisable() => _onAbilityUnlocked.OnEventRaised -= OnAbilityUnlocked; void OnAbilityUnlocked(string abilityId) { if (abilityId != _requiredAbility.ToString()) return; _blockingObject.SetActive(false); // P1:播放解锁动画(如荆棘收缩、道路开通特效) } } ``` ### AbilityGate 类型对照表 | 能力 | 门控表现形式 | 场景示例 | |------|------------|---------| | `DoubleJump` | 过高的空中平台,单跳无法到达 | 无法单跳到达的洞穴上层 | | `Dash` | 跨越无平台的水平间隙(距离 > 单跳能到) | 废墟区域宽沟壑 | | `WallJump` | 两侧光滑高墙的竖井,无法借力 | 深渊垂直上升通道 | | `Swim` | P1:充满液体的通道,无法通过 | 深渊底部的地下湖 | --- ## 4. 进程锁(ProgressLock) `ProgressLock` 是单向/永久性阻挡,需满足特定条件(如击败特定 Boss 或持有特定道具)才能解锁: ```csharp public class ProgressLock : MonoBehaviour { [Header("解锁条件")] [SerializeField] string _requiredBossId; // 空 = 不检查Boss [SerializeField] string _requiredItemId; // 空 = 不检查道具(P1) [Header("物理表现")] [SerializeField] GameObject _lockedVisuals; // 锁住状态的视觉(如大门、封印石) [SerializeField] GameObject _unlockedVisuals; // 开启状态的视觉(可选,或直接禁用) [SerializeField] Collider2D _blockCollider; [Header("存档")] [SerializeField] string _lockId; // 唯一 ID,存档中记录开启状态 void Start() { bool isUnlocked = CheckUnlocked(); ApplyState(isUnlocked); } bool CheckUnlocked() { var save = SaveManager.Instance.Data; if (!string.IsNullOrEmpty(_requiredBossId) && !save.defeatedBosses.Contains(_requiredBossId)) return false; if (!string.IsNullOrEmpty(_requiredItemId) && !save.ownedItems.Contains(_requiredItemId)) return false; return save.unlockedProgressLocks.Contains(_lockId); } void ApplyState(bool unlocked) { _blockCollider.enabled = !unlocked; _lockedVisuals.SetActive(!unlocked); if (_unlockedVisuals != null) _unlockedVisuals.SetActive(unlocked); } } ``` ### 典型用途 | 场景 | lockId 示例 | 解锁条件 | |------|------------|---------| | Forest → Cave 的大门 | `Lock_Forest_ToCave` | 击败蛛网守卫(bossId=`Boss_SpiderGuard`)| | 废墟入口封印石 | `Lock_Ruins_Entry` | 持有冲刺符文(itemId=`Item_DashRune`)| | 深渊入口遗迹门 | `Lock_Abyss_Entry` | 击败废墟遗骑士 | --- ## 5. Boss 进程管理 Boss 击败信息集中存储在 `SaveData.defeatedBosses` 字符串集合中,由各 `ProgressLock` 和 `RegionDefinitionSO` 查询。 ### BossProgressTracker(轻量辅助组件) 挂载在 Boss 房间的 `BossTrigger` 同一对象上: ```csharp public class BossProgressTracker : MonoBehaviour { [SerializeField] string _bossId; // 如 "Boss_SpiderGuard" [SerializeField] string[] _unlocksProgressLockIds; // 击败后解锁哪些 ProgressLock void OnEnable() => _onBossDefeated.OnEventRaised += OnBossDefeated; void OnDisable() => _onBossDefeated.OnEventRaised -= OnBossDefeated; void OnBossDefeated(string bossId) { if (bossId != _bossId) return; // 1. 通过事件频道通知 SaveSystem 记录(零耦合) _onBossDefeatedForSave.Raise(new BossDefeatedPayload { bossId = bossId, progressLockIds = _unlocksProgressLockIds, }); // 2. 广播区域解锁事件(地图 UI 更新) _onRegionUnlocked.Raise(bossId); } } ``` --- ## 6. HP 容器升级流程 HP 容器(Heart Container)是永久 MaxHP 增量物件,拾取后触发完整 UI 演出: ```csharp public class HPContainerPickup : MonoBehaviour { [SerializeField] string _collectibleId; // 存档用唯一 ID void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; // 通过注入的 SaveData 引用检查,而非 SaveManager.Instance if (_saveData != null && _saveData.collectedItems.Contains(_collectibleId)) return; StartCoroutine(PickupSequence(other.transform)); } SaveData _saveData; // 由 GameInitializer 在 Awake 时注入 IEnumerator PickupSequence(Transform player) { // 1. 禁用玩家输入(进入短暂无敌+输入锁定状态) _inputReader.EnableGameplayInput(false); // 2. 隐藏本物件 gameObject.SetActive(false); // 3. 播放获取特效(中央放大的心形光效,全屏暗化) // Feel MMF_Player: Flash + Scale + Particles // 4. 等待特效完成(0.8s) yield return new WaitForSeconds(0.8f); // 5. MaxHP + 2(每个容器增加 2 HP) // 零耦合:通过事件频道通知 SaveSystem,携带新 MaxHP 值 _onMaxHPContainerPickedUp.Raise(new HPContainerPayload { collectibleId = _collectibleId }); // SaveSystem 收到事件后执行:data.maxHP += 2; data.currentHP = data.maxHP; data.collectedItems.Add(id); Save(); // 6. 广播给 UI 系统更新显示(SaveSystem 修改完数据后再广播真实值,这里仅预估触发动画) _onMaxHPChanged.Raise(_saveData != null ? _saveData.maxHP : 0); _onHPChanged.Raise(_saveData != null ? _saveData.maxHP : 0); // 8. 恢复输入 yield return new WaitForSeconds(0.5f); _inputReader.EnableGameplayInput(true); } } ``` **UI 演出细节**: - 全屏轻微暗化(Canvas_Overlay 半透明黑色遮罩淡入淡出) - HPContainer 新增的心形图标从中央飞入到 HUD 左上角 - Soul 槽短暂闪烁 - 音效:SFX_HPContainer_Pickup(神圣感上升音效) --- ## 7. 收集品进度追踪 ```csharp // SaveData 扩展字段(见 §8) // collectionProgress 记录各类收集品的拾取状态 public static class ProgressCalculator { // 完成度百分比(用于主菜单存档槽显示 / P2 成就) public static float GetCompletionPercent(SaveData data, RegionDefinitionSO[] allRegions) { int total = /* 所有可收集物件总数,硬编码在 GameConstantsSO 中 */; int collected = data.collectedItems.Count + data.defeatedBosses.Count * 5 // Boss 击败权重更高 + data.visitedRooms.Count; // P1:房间探索率 return Mathf.Clamp01((float)collected / total) * 100f; } } ``` --- ## 8. SaveData 字段扩展 在 [08_WorldSystem.md §5](./08_WorldSystem.md) 的 SaveData JSON Schema 基础上,进程系统新增字段: ```json { "player": { "maxHP": 6, "unlockedAbilities": ["Dash", "DoubleJump"], "ownedItems": ["Item_DashRune"] }, "progression": { "defeatedBosses": ["Boss_SpiderGuard"], "unlockedProgressLocks": ["Lock_Forest_ToCave"], "unlockedFastTravelIds": ["FT_Forest_01", "FT_Cave_01"], "visitedRooms": ["Room_Forest_01", "Room_Forest_02"], "collectedItems": ["HC_Forest_01", "HC_Cave_01"] } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `maxHP` | `int` | 当前最大 HP(初始值 6,每个容器 +2)| | `unlockedAbilities` | `HashSet` | 已解锁的能力 ID 集合 | | `defeatedBosses` | `HashSet` | 已击败 Boss 的 bossId 集合 | | `unlockedProgressLocks` | `HashSet` | 已解锁的 ProgressLock.lockId 集合 | | `unlockedFastTravelIds` | `HashSet` | 已激活的 Bench 传送点 ID 集合 | | `visitedRooms` | `HashSet` | P1:已访问房间的场景名,用于地图显示 | | `collectedItems` | `HashSet` | 所有可收集物件的唯一 ID(HP容器/能力道具等)| > **性能规范**:所有需要 `.Contains()` 查询的集合字段均使用 `HashSet`(O(1) 查询),禁止使用 `string[]` 或 `List`(O(n) 遍历)。JSON 序列化使用 `Newtonsoft.Json`(`JsonConvert.DeserializeObject`),Newtonsoft 原生支持 `HashSet` 的序列化/反序列化,无需额外转换。 ### C# SaveData 结构体(部分) ```csharp [Serializable] public class SaveData { // 玩家状态 public int maxHP = 6; public int geoCount = 0; public int soulCount = 0; // 进程集合 — 全部 HashSet 保证 O(1) Contains public HashSet unlockedAbilities = new(); public HashSet defeatedBosses = new(); public HashSet unlockedProgressLocks = new(); public HashSet unlockedFastTravelIds = new(); public HashSet visitedRooms = new(); public HashSet collectedItems = new(); public HashSet exploredRooms = new(); // 地图探索(16_MapSystem) // 辅助方法 public bool HasAbility(AbilityType ability) => unlockedAbilities.Contains(ability.ToString()); public bool HasDefeatedBoss(string bossId) => defeatedBosses.Contains(bossId); } ``` --- ## 9. 区域解锁与地图联动 当 `OnRegionUnlocked` 事件触发时,地图系统(P1)更新对应区域的显示状态: ``` 区域解锁流程: BossProgressTracker.OnBossDefeated() → SaveData.defeatedBosses.Add(bossId) → OnRegionUnlocked.Raise(bossId) ├─ 相邻 ProgressLock: 检测条件满足 → ApplyState(unlocked=true)(门开启) ├─ P1 MapManager: 将下一区域标记为"可探索"(地图上显示区域轮廓) └─ P1 UIManager: 短暂显示"新区域解锁"提示(屏幕上方滑入 ToastNotification) ``` --- ## 10. 事件频道 新增频道(`Assets/ScriptableObjects/Events/Progression/`): | 资产名 | 类型 | 触发时机 | |--------|------|---------| | `OnAbilityUnlocked.asset` | `StringEventChannelSO` | 玩家获得新能力,传递 abilityId | | `OnBossDefeated.asset` | `StringEventChannelSO` | Boss 击败,传递 bossId(由 BossBase 触发)| | `OnRegionUnlocked.asset` | `StringEventChannelSO` | Boss 击败后,传递解锁的区域 ID | | `OnMaxHPChanged.asset` | `IntEventChannelSO` | MaxHP 增加(HP容器拾取后),HPContainer UI 响应 | | `OnProgressLockOpened.asset` | `StringEventChannelSO` | ProgressLock 开启,传递 lockId(P1:地图动效)| > `OnAbilityUnlocked.asset` 与 [08_WorldSystem.md §8](./08_WorldSystem.md) 的 `AbilityUnlock` 组件共用同一频道。 --- ## 11. 编辑器友好设计 - `AbilityGate` Gizmo:在 Scene View 显示对应能力图标(叠加在阻挡碰撞体上方),红色=阻挡中,绿色=已解锁 - `ProgressLock` Custom Inspector:显示当前 lockId、解锁条件、运行时是否满足(绿色勾选 / 红色叉) - `RegionDefinitionSO` Custom Inspector:列出该区域所有房间场景,并标注是否存在(路径是否有效) - EditorWindow `ProgressionChecker`:扫描全部场景,列出所有 AbilityGate / ProgressLock,检测是否有孤立(lockId 未被任何 BossProgressTracker 引用)的进程锁(UI Toolkit `ListView` + `MultiColumnListView`,`CreateGUI()` 实现) --- ## 12. 序列越级防护矩阵(Sequence Break Prevention) > 配合 [49_AntiSoftlockSystem.md](./49_AntiSoftlockSystem.md) 使用。本节定义各区域的越级风险等级与应对策略。 **越级(Sequence Break)**定义:玩家在未获得预期能力的情况下,提前进入设计意图为后续区域的内容。 ### 12.1 各区域越级风险矩阵 | 越级路径 | 所需绕过的 AbilityGate | 风险等级 | 处理策略 | |---------|----------------------|---------|---------| | Forest → Cave(跳过 FlorestBoss)| `AbilityGate_Cave_Entrance` (无要求) | 🟢 低 | 允许(Cave 入口无能力要求)| | Forest → Ruins(跳过 Cave)| `AbilityGate_Ruins_MainGate` (需 Dash) | 🟡 中 | Dash 门严格,绕行需精确跳跃(允许但罕见)| | Cave → Abyss(跳过 Ruins)| `AbilityGate_Abyss_Bridge` (需 DoubleJump) | 🟡 中 | 允许序列越级(速通路线);HardAbilityGate:显示越级警告 | | 任意区域 → Core(最终区域)| `AbilityGate_Core_Gate` (需 Swim+Dash+DoubleJump) | 🔴 高 | 三能力全部严格验证,不允许物理绕过 | | 获得 Swim 前进入液体深区 | `AbilityGate_DeepWater` (需 Swim) | 🟡 中 | 无 Swim = 液体中失血,玩家自然死亡重生(不软锁)| | 获得 WallJump 前进入攀墙区 | `AbilityGate_VerticalShaft` (需 WallJump) | 🟢 低 | 可从底部进入,无能力时 SoftlockDetector 兜底 | ### 12.2 HardAbilityGate(强制门)规范 **普通 AbilityGate** vs **HardAbilityGate** 的区别: | 属性 | AbilityGate | HardAbilityGate | |------|------------|----------------| | 物理阻挡 | 碰撞体阻挡 | 碰撞体阻挡 + Trigger 警告 | | 绕过提示 | 无 | 显示"此路需要 [能力] 才能安全通过" | | 序列越级标记 | 无 | `sequenceBreakRisk = true`(供 QA 分析用)| | 应用场景 | 普通进程门 | 核心区域入口、不可逆操作前 | ```csharp // HardAbilityGate 仅在 Core 区入口和高风险路径使用 // 其余区域用普通 AbilityGate,保留速通路线可能性 [SerializeField] bool sequenceBreakRisk = false; [SerializeField] string warningLocKey; // 若 sequenceBreakRisk = true,显示此警告 ``` ### 12.3 序列越级分析工具 `EscapeGuaranteeValidator`(见 49_AntiSoftlockSystem §6)在执行 BFS 分析时,同时标注所有可能的序列越级路径,输出报告供关卡设计师审查: ``` [越级路径报告] ✓ Forest → Ruins (via precision jump above AbilityGate_Ruins_SideGate) 风险: 低 | 已知速通路线 | SoftlockDetector: 覆盖 ⚠ Cave_B3 → Abyss_Top (via wall clip at X=234, Y=89) 风险: 中 | 玩家可能卡在 Abyss 无 DoubleJump | SoftlockDetector: 覆盖 ✗ Ruins → Core (无已知越级路径) HardAbilityGate 三重验证 ```