18 KiB
14 · 进程系统(区域组织·能力门·Boss 进程)
命名空间
BaseGames.Progression
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events·BaseGames.World(SaveSystem)·BaseGames.Player(能力查询)
目录
- 系统总览
- 区域(Region)划分
- 能力门(AbilityGate)
- 进程锁(ProgressLock)
- Boss 进程管理
- HP 容器升级流程
- 收集品进度追踪
- SaveData 字段扩展
- 区域解锁与地图联动
- 事件频道
- 编辑器友好设计
1. 系统总览
进程系统解决"类银河恶魔城游戏如何组织超大地图进程"的核心问题:
进程系统职责:
├─ 区域划分 → RegionDefinitionSO 定义区域元数据
├─ 能力门 → AbilityGate 阻挡没有对应能力的玩家
├─ 进程锁 → ProgressLock 阻挡未满足条件的通路
├─ Boss 进程 → Boss 击败解锁通往新区域的大门
├─ HP 容器升级 → 拾取后 MaxHP++ 并触发 UI 演出
└─ 收集品追踪 → 完成度百分比(成就/结局条件)
2. 区域(Region)划分
区域枚举
游戏世界分为以下区域,按开放顺序排列:
| 区域 ID | 中文名 | 特点 | Boss | 开放条件 |
|---|---|---|---|---|
Forest |
扎根森林 | 初始区域,基础敌人 | 蛛网守卫 | 无(起始区域) |
Cave |
腐蚀洞穴 | 黑暗区域,大量陷阱 | 蚀骨蠕虫 | 击败蛛网守卫 |
Ruins |
坍塌废墟 | 需要冲刺能力 | 废墟遗骑士 | 获得冲刺能力 |
Abyss |
深渊裂隙 | 需要双跳,垂直地图 | 深渊之喉 | 击败废墟遗骑士 |
Core |
核心熔炉 | 终局区域 | 最终Boss | 击败深渊之喉 |
RegionDefinitionSO
每个区域一个 SO 资产,集中管理区域元数据:
[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 空间门控的核心组件:
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 或持有特定道具)才能解锁:
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 同一对象上:
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 演出:
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. 收集品进度追踪
// 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 的 SaveData JSON Schema 基础上,进程系统新增字段:
{
"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 结构体(部分)
[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 的AbilityUnlock组件共用同一频道。
11. 编辑器友好设计
AbilityGateGizmo:在 Scene View 显示对应能力图标(叠加在阻挡碰撞体上方),红色=阻挡中,绿色=已解锁ProgressLockCustom Inspector:显示当前 lockId、解锁条件、运行时是否满足(绿色勾选 / 红色叉)RegionDefinitionSOCustom Inspector:列出该区域所有房间场景,并标注是否存在(路径是否有效)- EditorWindow
ProgressionChecker:扫描全部场景,列出所有 AbilityGate / ProgressLock,检测是否有孤立(lockId 未被任何 BossProgressTracker 引用)的进程锁(UI ToolkitListView+MultiColumnListView,CreateGUI()实现)
12. 序列越级防护矩阵(Sequence Break Prevention)
配合 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 分析用) |
| 应用场景 | 普通进程门 | 核心区域入口、不可逆操作前 |
// 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 三重验证