Files
zeling_v2/Docs/Design/14_ProgressionSystem.md
2026-05-08 11:04:00 +08:00

18 KiB
Raw Permalink Blame History

14 · 进程系统区域组织·能力门·Boss 进程)

命名空间 BaseGames.Progression
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.WorldSaveSystem· BaseGames.Player(能力查询)


目录

  1. 系统总览
  2. 区域Region划分
  3. 能力门AbilityGate
  4. 进程锁ProgressLock
  5. Boss 进程管理
  6. HP 容器升级流程
  7. 收集品进度追踪
  8. SaveData 字段扩展
  9. 区域解锁与地图联动
  10. 事件频道
  11. 编辑器友好设计

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 字符串集合中,由各 ProgressLockRegionDefinitionSO 查询。

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> 所有可收集物件的唯一 IDHP容器/能力道具等)

性能规范:所有需要 .Contains() 查询的集合字段均使用 HashSet<string>O(1) 查询),禁止使用 string[]List<string>O(n) 遍历。JSON 序列化使用 Newtonsoft.JsonJsonConvert.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 开启,传递 lockIdP1地图动效

OnAbilityUnlocked.asset08_WorldSystem.md §8AbilityUnlock 组件共用同一频道。


11. 编辑器友好设计

  • AbilityGate Gizmo在 Scene View 显示对应能力图标(叠加在阻挡碰撞体上方),红色=阻挡中,绿色=已解锁
  • ProgressLock Custom Inspector显示当前 lockId、解锁条件、运行时是否满足绿色勾选 / 红色叉)
  • RegionDefinitionSO Custom Inspector列出该区域所有房间场景并标注是否存在路径是否有效
  • EditorWindow ProgressionChecker:扫描全部场景,列出所有 AbilityGate / ProgressLock检测是否有孤立lockId 未被任何 BossProgressTracker 引用的进程锁UI Toolkit ListView + MultiColumnListViewCreateGUI() 实现)

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 三重验证