# 49 · 反软锁死系统(Anti-Softlock System) > **命名空间** `BaseGames.Progression` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.World`(SaveManager、SceneLoader)· `BaseGames.Player`(能力查询) > **关联** 14_ProgressionSystem(能力门/进程锁)· 08_WorldSystem(房间切换)· 31_SaveDataSchema(存档) --- ## 目录 1. [系统总览](#1-系统总览) 2. [软锁死分类](#2-软锁死分类) 3. [房间可达性矩阵](#3-房间可达性矩阵) 4. [Sequence Break 防护](#4-sequence-break-防护) 5. [运行时软锁死检测器](#5-运行时软锁死检测器) 6. [紧急传送机制](#6-紧急传送机制) 7. [软锁死恢复 UI](#7-软锁死恢复-ui) 8. [Escape Guarantee 验证工具](#8-escape-guarantee-验证工具) 9. [SaveData 集成](#9-savedata-集成) 10. [测试矩阵](#10-测试矩阵) 11. [事件频道](#11-事件频道) 12. [编辑器友好设计](#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` 标注其**逃离条件**: ```csharp /// /// 每个房间必须挂载此 SO,记录"最低能力集合即可离开此房间" /// 设计时由关卡设计师填写,编辑器工具自动验证可达性 /// [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`(能力检查)+ `ProgressLock`(Boss 进程检查)。即使玩家绕过能力检查,进程锁仍会物理阻挡。 ``` 高难度区域入口: [AbilityGate: 双跳] ──→ [ProgressLock: 要求已击败Forest_Boss] ──→ 进入区域 ``` **策略 B:AbilityGate 增强版——SkillCapCheck** 对于能用技巧绕过的能力门,增加**技能力检查**(跳跃输入次数限制 + 实际位移验证)。 ```csharp /// /// 增强型能力门:除了检查能力标志,还检测玩家当前是否"真的能做到" /// 用于防止玩家用精准时机绕过只检查标志的 AbilityGate /// 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); } } ``` **策略 C:Sequence Break 日志** 当玩家进入标记了 `sequenceBreakRisk = true` 的区域时,记录遥测事件。用于后续分析玩家行为(见 55_AnalyticsTelemetrySystem)。 --- ## 5. 运行时软锁死检测器 `SoftlockDetector` 挂载在 Persistent 场景,全局监听玩家状态: ```csharp 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 ```csharp namespace BaseGames.Progression { /// /// 紧急传送服务。由 SoftlockDetector 触发或玩家主动在设置菜单中激活。 /// 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()?.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 流程 ```yaml # .github/workflows/design-validation.yml(示意) - name: Escape Guarantee Check run: unity -batchmode -runEditorTests -testFilter "EscapeGuaranteeTests" # 若有 ❌ 房间,测试失败,阻止 PR 合并 ``` --- ## 9. SaveData 集成 在 `SaveData` 中新增字段,支持紧急传送的状态恢复: ```csharp public class WorldSaveData { // ...(现有字段) // 紧急传送记录(调试用,不影响游戏逻辑) [JsonProperty("escapeTeleportCount")] public int EscapeTeleportCount { get; set; } = 0; // 最近激活的存档点 ID(SoftlockDetector 需要此数据确定传送目的地) [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*