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

17 KiB
Raw Permalink Blame History

49 · 反软锁死系统Anti-Softlock System

命名空间 BaseGames.Progression
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.WorldSaveManager、SceneLoader· BaseGames.Player(能力查询)
关联 14_ProgressionSystem能力门/进程锁)· 08_WorldSystem房间切换· 31_SaveDataSchema存档


目录

  1. 系统总览
  2. 软锁死分类
  3. 房间可达性矩阵
  4. Sequence Break 防护
  5. 运行时软锁死检测器
  6. 紧急传送机制
  7. 软锁死恢复 UI
  8. Escape Guarantee 验证工具
  9. SaveData 集成
  10. 测试矩阵
  11. 事件频道
  12. 编辑器友好设计

1. 系统总览

银河恶魔城Metroidvania品类特有的测试挑战玩家可能凭借技巧或意外路线没有必要能力的情况下进入某区域,然后无法离开,导致游戏无法继续推进(软锁死)。

反软锁死系统职责:
  ├─ 房间可达性矩阵    → 设计时静态验证:每个房间的"逃离路径"是否始终存在
  ├─ Sequence Break防护 → 防止能力组合绕过能力门进入预期外区域
  ├─ 运行时检测器      → 检测玩家静止超时 / 卡在特定区域无法脱离
  ├─ 紧急传送机制      → 触发后安全传送玩家到最近存档点(不损失进度)
  └─ 恢复 UI           → 提示玩家当前情况并提供选项(非强制打断游戏流)

核心原则

  • 软锁死恢复不惩罚玩家——传送到上一存档点,不扣除 Geo/物品
  • 系统检测是辅助性的,主要靠设计时静态分析彻底消除软锁死隐患
  • 传送必须有玩家确认,不自动发生(防止误触破坏游戏体验)

2. 软锁死分类

2.1 类型一能力锁死Ability Trap

玩家进入一个区域,该区域有单向出口(如只能向下跳入但无法跳出的深井)。进入后发现无法用当前能力离开。

设计规则

  • 凡有单向入口(只能跌落/仅靠冲刺进入)的区域,出口必须支持无能力通过,或在该区域内提供能力道具
  • AbilityGate 只阻挡进入,不阻挡离开(出口方向无能力门)

2.2 类型二进程锁死Progress Trap

玩家解锁了某 ProgressLock(如击败 Boss 后大门开启),进入新区域后在新区域 Boss 战中死亡过多次Geo 全失,卡在新区域无法回购消耗品。

设计规则

  • 每个区域内有至少一个免费恢复点(灵泉台/免费商品),或进入 Boss 房前强制激活存档点

2.3 类型三状态锁死State Trap

游戏状态出错(如 Boss 战异常中断、剧情触发器失灵)导致玩家被困在一个无门的空间。

解决方案:运行时软锁死检测器 + 紧急传送(见 §5、§6

2.4 类型四Sequence Break 后遗症

玩家绕过能力门提前进入高难度区域,在该区域内卡死(无法回头也无法前进)。

解决方案Sequence Break 防护层(见 §4


3. 房间可达性矩阵

3.1 EscapeInfo每个房间必须标注的数据

每个房间场景在设计时必须用 RoomEscapeInfoSO 标注其逃离条件

/// <summary>
/// 每个房间必须挂载此 SO记录"最低能力集合即可离开此房间"
/// 设计时由关卡设计师填写,编辑器工具自动验证可达性
/// </summary>
[CreateAssetMenu(menuName = "Progression/RoomEscapeInfo")]
public class RoomEscapeInfoSO : ScriptableObject
{
    [Header("房间标识")]
    public string sceneAddress;              // 对应 Addressable 场景地址

    [Header("逃离要求AND 关系,满足其一路线即可)")]
    public EscapeRoute[] escapeRoutes;       // 多条逃离路线(满足任意一条即视为可逃离)

    [Header("单向入口警告")]
    public bool hasOneWayEntry;              // 是否有单向进入点(如跌落入口)
    [TextArea(1, 3)]
    public string designerNotes;            // 设计注意事项
}

[Serializable]
public class EscapeRoute
{
    public string        routeLabel;         // 如 "向左回到 Forest_Main"
    public string        targetSceneAddress; // 逃离到达的目标房间
    public AbilityType[] requiredAbilities;  // 空 = 无需任何能力即可离开
}

3.2 区域逃离规则表(设计约束)

区域 房间 逃离条件 备注
Forest 全部 无能力 初始区域,永远可徒步离开
Cave 普通房间 无能力(单向跌落入口除外) 跌落房间必须提供爬出方式
Cave Boss房间前厅 无能力 Boss 房外必须可回头
Ruins 普通房间 无能力(冲刺可选捷径) 捷径不阻止不会冲刺的玩家离开
Ruins 高处秘密房间 双跳 OR 无能力(通过台阶可下) 只进不出的高台禁止存在
Abyss 普通房间 双跳 深渊区域合理要求双跳离开
Core 全部 已默认获得所有核心能力 终局区域无限制

规则:如果某房间的所有 EscapeRoute 中requiredAbilities 的最低要求超过了玩家进入该房间时理论上能拥有的能力,则视为设计 BUG编辑器工具会标红警告。


4. Sequence Break 防护

4.1 什么是 Sequence Break

玩家利用技巧(如精准跳跃、速通走位)绕过 AbilityGate 进入预期外的区域。这在社区速通中是正常现象,但在普通游玩中可能导致:

  • 进入远超当前实力的区域被反复秒杀
  • 在设计边界外卡死

4.2 防护策略

策略 A双层门控

关键区域入口同时有 AbilityGate(能力检查)+ ProgressLockBoss 进程检查)。即使玩家绕过能力检查,进程锁仍会物理阻挡。

高难度区域入口:
  [AbilityGate: 双跳]  ──→  [ProgressLock: 要求已击败Forest_Boss]  ──→  进入区域

策略 BAbilityGate 增强版——SkillCapCheck

对于能用技巧绕过的能力门,增加技能力检查(跳跃输入次数限制 + 实际位移验证)。

/// <summary>
/// 增强型能力门:除了检查能力标志,还检测玩家当前是否"真的能做到"
/// 用于防止玩家用精准时机绕过只检查标志的 AbilityGate
/// </summary>
public class HardAbilityGate : AbilityGate
{
    [Header("额外物理验证")]
    [SerializeField] bool _requirePhysicalValidation = false;

    // 在关卡测试期间用编辑器工具标记"此门已验证可能被绕过"
    [SerializeField] bool _sequenceBreakRisk = false;

    protected override bool EvaluateAccess()
    {
        if (!base.EvaluateAccess()) return false;
        if (!_requirePhysicalValidation) return true;

        // 对于需要物理验证的门:检查能力实际已激活(非仅标志为 true
        return _playerStats != null && _playerStats.IsAbilityActuallyUnlocked(_requiredAbility);
    }
}

策略 CSequence Break 日志

当玩家进入标记了 sequenceBreakRisk = true 的区域时,记录遥测事件。用于后续分析玩家行为(见 55_AnalyticsTelemetrySystem


5. 运行时软锁死检测器

SoftlockDetector 挂载在 Persistent 场景,全局监听玩家状态:

namespace BaseGames.Progression
{
    public class SoftlockDetector : MonoBehaviour
    {
        [Header("检测参数")]
        [SerializeField] float _stuckTimeThreshold = 45f;  // 45s 无实质移动 = 疑似卡死
        [SerializeField] float _moveThreshold = 0.5f;      // 移动阈值单位Unity unit

        [Header("依赖")]
        [SerializeField] VoidEventChannelSO _onSoftlockSuspected;
        [SerializeField] Transform          _playerTransform;  // 通过 GameInitializer 注入

        Vector2 _lastRecordedPosition;
        float   _stuckTimer;
        bool    _isDetectorActive = true;  // Boss 战/过场动画中临时禁用

        void Update()
        {
            if (!_isDetectorActive || _playerTransform == null) return;

            float moved = Vector2.Distance(_playerTransform.position, _lastRecordedPosition);

            if (moved > _moveThreshold)
            {
                _lastRecordedPosition = _playerTransform.position;
                _stuckTimer = 0f;
            }
            else
            {
                _stuckTimer += Time.deltaTime;
                if (_stuckTimer >= _stuckTimeThreshold)
                {
                    _stuckTimer = 0f;  // 重置,避免反复触发
                    _onSoftlockSuspected.Raise();
                }
            }
        }

        // Boss 战开始时暂停检测(玩家可能长时间在小范围内战斗)
        public void SetDetectorActive(bool active) => _isDetectorActive = active;
    }
}

检测器禁用时机

场景 禁用原因
Boss 战进行中 小范围高强度战斗会误触发
过场动画播放中 玩家无控制权
对话进行中 玩家站立不动属正常
存档点交互动画中 玩家静止属正常
加载画面 无玩家对象

6. 紧急传送机制

6.1 EscapeTeleporter

namespace BaseGames.Progression
{
    /// <summary>
    /// 紧急传送服务。由 SoftlockDetector 触发或玩家主动在设置菜单中激活。
    /// </summary>
    public class EscapeTeleporter : MonoBehaviour
    {
        [SerializeField] SceneLoaderSO          _sceneLoader;
        [SerializeField] VoidEventChannelSO      _onSoftlockSuspected;    // 订阅检测器事件
        [SerializeField] SoftlockRecoveryUISO    _recoveryUIConfig;
        [SerializeField] SaveManager             _saveManager;            // 注入引用

        void OnEnable()  => _onSoftlockSuspected.OnEventRaised += OnSoftlockSuspected;
        void OnDisable() => _onSoftlockSuspected.OnEventRaised -= OnSoftlockSuspected;

        void OnSoftlockSuspected()
        {
            // 打开恢复 UI玩家自主选择不强制传送
            RecoveryUIManager.Instance.Show(_recoveryUIConfig, OnTeleportConfirmed, OnTeleportCancelled);
        }

        void OnTeleportConfirmed()
        {
            // 找到当前角色最近的已激活存档点
            var nearestSavePoint = _saveManager.GetNearestActivatedSavePoint();
            if (nearestSavePoint == null)
            {
                // Fallback传送到区域入口存档点
                nearestSavePoint = _saveManager.GetRegionEntryPoint();
            }

            // 传送:不触发死亡,不消耗 Geo/物品,保留当前 HP
            _sceneLoader.TeleportToSavePoint(nearestSavePoint, preserveItems: true, preserveHP: true);
        }

        void OnTeleportCancelled()
        {
            // 玩家选择继续尝试,重置检测器计时
            FindObjectOfType<SoftlockDetector>()?.ResetTimer();
        }
    }
}

6.2 传送安全规则

规则 说明
不触发死亡 传送不走死亡流程,不生成遗骸,不重置房间
不扣 Geo 正常死亡会在遗骸处保留 Geo紧急传送直接保留
不重置消耗品 灵泉次数、符文耐久等保持当前值
保留 Boss 进程 若 Boss 已处于濒死状态,传送后 Boss 仍保持受伤状态
传送冷却 60s 防止玩家滥用紧急传送作为快速旅行手段

7. 软锁死恢复 UI

SoftlockDetector 触发时,弹出轻量级提示 UI不打断游戏流类似通知横幅

┌─────────────────────────────────────────────────────┐
│  ⚠ 似乎陷入困境了?                                  │
│                                                      │
│  可以传送到最近的存档点(不会损失道具或 Geo         │
│                                                      │
│  [  传送到存档点  ]          [  继续尝试  ]           │
└─────────────────────────────────────────────────────┘
  • 文案不使用"软锁死"等技术词汇
  • 玩家选择"继续尝试"后,检测计时器重置为 20s缩短防止反复打扰
  • 如果玩家连续 3 次选择"继续尝试"后仍不动,则在暂停菜单中永久显示"传送到存档点"按钮直到玩家使用

暂停菜单中的手动传送入口

无论是否触发自动检测,玩家可在 Pause → 设置 → 辅助功能 中找到:

"传送到最近存档点"(含 30s 冷却)


8. Escape Guarantee 验证工具

8.1 编辑器窗口

Assets/Editor/EscapeGuaranteeValidator.cs — UI Toolkit EditorWindow

功能:

  • 读取所有 RoomEscapeInfoSO,构建房间连通图
  • 对每个房间执行逃离可达性 BFS(广度优先搜索):
    • 起始状态:玩家在该房间,拥有进入时理论最低能力集
    • 目标:能到达任意存档点
  • 若某房间无法到达任何存档点 → 标红报错
  • 若某房间逃离路线依赖未确认获取的能力 → 标黄警告
Escape Guarantee Validator
═══════════════════════════════════════════════════════════
  [▶ 运行验证] [导出报告]

  Forest_Main         ✅ 可从 3 条路线逃离(最低要求:无)
  Forest_SecretCave   ✅ 可从 1 条路线逃离(最低要求:无)
  Cave_Entrance       ✅ 可从 2 条路线逃离(最低要求:无)
  Cave_DropShaft_01   ⚠ 仅有 1 条逃离路线(双跳)— 单向入口!
  Ruins_HighPlatform  ❌ 无逃离路线BUG缺少 EscapeRoutes 配置)
═══════════════════════════════════════════════════════════
  总计: 47 个房间 | 45 ✅ | 1 ⚠ | 1 ❌

8.2 集成到 CI 流程

# .github/workflows/design-validation.yml示意
- name: Escape Guarantee Check
  run: unity -batchmode -runEditorTests -testFilter "EscapeGuaranteeTests"
  # 若有 ❌ 房间,测试失败,阻止 PR 合并

9. SaveData 集成

SaveData 中新增字段,支持紧急传送的状态恢复:

public class WorldSaveData
{
    // ...(现有字段)

    // 紧急传送记录(调试用,不影响游戏逻辑)
    [JsonProperty("escapeTeleportCount")]
    public int EscapeTeleportCount { get; set; } = 0;

    // 最近激活的存档点 IDSoftlockDetector 需要此数据确定传送目的地)
    [JsonProperty("lastSavePointId")]
    public string LastSavePointId { get; set; }
}

10. 测试矩阵

测试场景 预期结果 优先级
跌入单向坑无法出 45s 后触发软锁死提示 P0
Boss 战中长时间僵局 检测器禁用,不触发提示 P0
对话中长时间站立 检测器禁用,不触发提示 P0
传送到存档点后物品正常保留 无物品损失 P0
无存档点激活时传送 Fallback 到区域入口点 P1
速通玩家手动关闭软锁死提示 检测重置,下次触发时间延长 P1
绕过 AbilityGate 进入 Cave 记录 SequenceBreak 遥测事件 P2

11. 事件频道

频道 SO 类型 触发时机
OnSoftlockSuspected VoidEventChannelSO SoftlockDetector 检测到超时
OnEscapeTeleportUsed StringEventChannelSO 玩家确认传送(传入目标存档点 ID
OnBossFightStarted VoidEventChannelSO 订阅:暂停检测器
OnBossFightEnded VoidEventChannelSO 订阅:恢复检测器

12. 编辑器友好设计

  • SoftlockDetector Inspector显示当前 _stuckTimer(只读进度条)、检测器开关状态
  • AbilityGate Inspector新增 [SequenceBreakRisk] 勾选框(标红显示,提醒 QA 重点测试)
  • RoomEscapeInfoSO Inspector每条 EscapeRoute 右侧显示绿/黄/红状态标签
  • Escape Guarantee Validator:每次打开 Play Mode 自动后台验证,发现 在控制台输出警告

本文档版本 1.0 · 2026-04 · 关联 14_ProgressionSystem / 08_WorldSystem / 31_SaveDataSchema