455 lines
18 KiB
Markdown
455 lines
18 KiB
Markdown
# 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<string>` | 已解锁的能力 ID 集合 |
|
||
| `defeatedBosses` | `HashSet<string>` | 已击败 Boss 的 bossId 集合 |
|
||
| `unlockedProgressLocks` | `HashSet<string>` | 已解锁的 ProgressLock.lockId 集合 |
|
||
| `unlockedFastTravelIds` | `HashSet<string>` | 已激活的 Bench 传送点 ID 集合 |
|
||
| `visitedRooms` | `HashSet<string>` | P1:已访问房间的场景名,用于地图显示 |
|
||
| `collectedItems` | `HashSet<string>` | 所有可收集物件的唯一 ID(HP容器/能力道具等)|
|
||
|
||
> **性能规范**:所有需要 `.Contains()` 查询的集合字段均使用 `HashSet<string>`(O(1) 查询),禁止使用 `string[]` 或 `List<string>`(O(n) 遍历)。JSON 序列化使用 `Newtonsoft.Json`(`JsonConvert.DeserializeObject<SaveData>`),Newtonsoft 原生支持 `HashSet<T>` 的序列化/反序列化,无需额外转换。
|
||
|
||
### C# SaveData 结构体(部分)
|
||
|
||
```csharp
|
||
[Serializable]
|
||
public class SaveData
|
||
{
|
||
// 玩家状态
|
||
public int maxHP = 6;
|
||
public int geoCount = 0;
|
||
public int soulCount = 0;
|
||
|
||
// 进程集合 — 全部 HashSet<string> 保证 O(1) Contains
|
||
public HashSet<string> unlockedAbilities = new();
|
||
public HashSet<string> defeatedBosses = new();
|
||
public HashSet<string> unlockedProgressLocks = new();
|
||
public HashSet<string> unlockedFastTravelIds = new();
|
||
public HashSet<string> visitedRooms = new();
|
||
public HashSet<string> collectedItems = new();
|
||
public HashSet<string> 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 三重验证
|
||
```
|