# Framework Review — 2026 May v14 > **覆盖范围**:v13 之后新增模块的全面评审(v13 已宣告 100% 覆盖率 9.45/10,本次审查本轮新增代码) > **修复统计**:共发现并修复 **9 处问题**(TD-21 ~ TD-29) > **最终评分**:9.52 / 10 --- ## 一、新增模块概览 本次 v14 审查覆盖以下 v13 之后新增的模块: | 模块 | 路径 | 文件数 | |------|------|--------| | Audio — 脚步音效系统 | `Assets/Scripts/Audio/Footstep*` | 3 | | Audio — 水下音效控制器 | `Assets/Scripts/Audio/UnderwaterAudioController.cs` | 1 | | Tutorial 教程系统 | `Assets/Scripts/Tutorial/` | 4 | | Support — Accessibility 无障碍 | `Assets/Scripts/Support/Accessibility/` | 3 | | Support — Analytics 数据埋点 | `Assets/Scripts/Support/Analytics/` | 1 | | Support — AntiSoftlock 反卡关 | `Assets/Scripts/Support/AntiSoftlock/` | 3 | | Support — Speedrun 速通计时器 | `Assets/Scripts/Support/Speedrun/` | 1 | | World — Liquid 液态区域系统 | `Assets/Scripts/World/Liquid/` | 5 | | World — Puzzle 谜题系统 | `Assets/Scripts/World/Puzzle/` | 4 | | World — PhantomInteractable | `Assets/Scripts/World/PhantomInteractable.cs` | 1 | | World — WorldMarker | `Assets/Scripts/World/WorldMarker*.cs` | 2 | --- ## 二、各模块详细评审 ### 2.1 Audio — 脚步音效系统 **涉及文件** - `FootstepMaterial.cs` — 枚举:Stone/Dirt/Wood/Metal/Water/Sand/Grass/Cave - `FootstepMaterialMarker.cs` — Marker MonoBehaviour,挂在地面 Collider 上打标签 - `FootstepAudioConfigSO.cs` — SO:按材质映射 `AudioClip[]` + volume + pitchVariance **优点** - 数据驱动(SO 配置),场景策划无需碰代码 - `FootstepMaterialMarker` 轻量,仅携带枚举值,无运行时逻辑 - `GetEntry(FootstepMaterial)` 返回 `MaterialEntry?`,正确使用可空值类型,避免引用判空 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | 数据/标记/配置分离,职责清晰 | | 性能 | 9.5 | 纯数据查询,无分配 | | 可扩展性 | 9.0 | 增加材质只需扩展枚举 + SO 条目 | | 编辑器友好 | 9.0 | SO 有 Header 分组 | | 使用便利性 | 9.0 | 三件套装配直观 | --- ### 2.2 Audio — UnderwaterAudioController **涉及文件**:`UnderwaterAudioController.cs` **优点** - 正确使用 `CompositeDisposable` 管理事件订阅,零内存泄漏 - 事件驱动,与 `LiquidZone` 完全解耦 **修复 TD-24(已修复)** `OnLiquidExited` 原实现无 LiquidType 过滤: ```csharp // Before — 任何液体离开都会清除水下快照 private void OnLiquidExited(LiquidEvent evt) => BlendVolume(0f, _blendOutDuration); // After — 仅 Water 类型触发 private void OnLiquidExited(LiquidEvent evt) { if (evt.LiquidType != LiquidType.Water) return; BlendVolume(0f, _blendOutDuration); } ``` **遗留设计说明(TD-23)** `GlobalSFXPlayer` 使用私有静态单例 `_instance` 而非 ServiceLocator。对于全局 SFX 入口而言,静态工具类是业界常见模式,但与框架其余部分的 ServiceLocator 注入风格不一致。当前已通过 `Play()` 内部委托 `ServiceLocator.GetOrDefault()` 处理 3D 播放,保持了对 IAudioService 的接口依赖。 **结论**:不修改,记录为架构风格差异,可在未来重构时统一。 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.0 | 已修复过滤缺失 | | 性能 | 9.5 | AudioMixer.FindSnapshot 在 OnEnable 时调用,不在热路径 | | 可扩展性 | 8.5 | 快照名建议收进常量类(TD-24 记录) | | 编辑器友好 | 9.0 | | | 使用便利性 | 9.0 | | --- ### 2.3 Tutorial 教程系统 **涉及文件** - `ITutorialService.cs` — ServiceLocator 接口 - `TutorialManager.cs` — `ISaveable`,管理已完成提示 ID,持久化到 SaveData - `TutorialHintUI.cs` — TMP_Text 面板 + 自动隐藏 Coroutine - `ContextualHintTrigger.cs` — 触发器区域,带能力门控,单次触发后 `SetActive(false)` **优点** - 通过 `ITutorialService` + ServiceLocator 完全解耦 - `ISaveable` 集成确保跨 Session 记忆已完成提示 - `ContextualHintTrigger` 的 `gameObject.SetActive(false)` 方式实现"仅触发一次"简洁高效,避免额外状态字段 **注意点** - `TutorialHintUI` 的自动隐藏 Coroutine 在场景切换时若未清理可能报错;但由于 HintUI 通常与场景生命周期绑定,可接受 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | 接口隔离 + ISaveable 集成完整 | | 性能 | 9.5 | 无热路径分配 | | 可扩展性 | 9.0 | 扩展提示类型只需继承/配置 | | 编辑器友好 | 9.5 | | | 使用便利性 | 9.5 | | --- ### 2.4 Support — Accessibility 无障碍系统 **涉及文件** - `AccessibilitySettingsSO.cs` - `ColorBlindFilter.cs`(ScriptableRendererFeature) - `AccessibilityManager.cs` **修复 TD-21:AccessibilitySettingsSO 全局命名空间(已修复)** ```csharp // Before — 无命名空间 public class AccessibilitySettingsSO : ScriptableObject { ... } // After namespace BaseGames.Support.Accessibility { public class AccessibilitySettingsSO : ScriptableObject { ... } } ``` **修复 TD-22:ColorBlindFilter 全局命名空间(已修复)** ```csharp // Before — 无命名空间 public class ColorBlindFilter : ScriptableRendererFeature { ... } // After namespace BaseGames.Support.Accessibility { public class ColorBlindFilter : ScriptableRendererFeature { ... } } ``` **架构评注 — ColorBlindFilter 生命周期** `ColorBlindFilter` 继承自 `ScriptableRendererFeature`(本质是 `ScriptableObject`)。使用 `OnEnable()`/`OnDisable()` 管理事件订阅是合理的:SO 的 `OnEnable` 在编辑器加载和运行时都会触发,行为可预期。`CompositeDisposable _subs` 跨 Play/Edit 切换保持干净。 **优点** - 色盲矩阵基于 Brettel/Viénot 标准,强度插值支持过渡 - `AccessibilityManager` 通过 ServiceLocator 注册,与其他服务一致 - `PlayerPrefs` 用于无障碍设置持久化(合理:无需 SaveData 加密路径) **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.0 | 命名空间修复后对齐 | | 性能 | 9.5 | Shader 矩阵仅在切换时更新 | | 可扩展性 | 9.0 | 增加色盲类型只需扩展枚举 + 矩阵 | | 编辑器友好 | 8.5 | RendererFeature 配置在 Renderer Asset 中,不太直观 | | 使用便利性 | 9.0 | | --- ### 2.5 Support — Analytics 数据埋点 **涉及文件**:`AnalyticsManager.cs` **优点** - 完全本地(`persistentDataPath/analytics.json`),无 PII,不联网 - `#if !UNITY_EDITOR && !DEVELOPMENT_BUILD` 保证仅在正式构建启用 - 批量队列 + `OnApplicationQuit`/`OnDestroy` 刷盘,减少 I/O 频率 - 预定义 `TrackBossKill`/`TrackPlayerDeath` 等方法,防止魔法字符串散布 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | | | 性能 | 9.0 | JSON 序列化不在热路径 | | 可扩展性 | 9.5 | 预定义方法 + 泛化 Track | | 编辑器友好 | 9.5 | 编辑器禁用,零干扰 | | 使用便利性 | 9.5 | | --- ### 2.6 Support — AntiSoftlock 反卡关系统 **涉及文件** - `AntiSoftlockSystem.cs` - `HardAbilityGate.cs` - `RoomEscapeInfoSO.cs` **修复 TD-25:命名空间错误(已修复)** `HardAbilityGate` 和 `RoomEscapeInfoSO` 声明于 `namespace BaseGames.Progression` 但物理位于 `Assets/Scripts/Support/AntiSoftlock/`,且同目录的 `AntiSoftlockSystem` 使用 `namespace BaseGames.Support.AntiSoftlock`,造成不一致。 ```csharp // Before namespace BaseGames.Progression { ... } // After namespace BaseGames.Support.AntiSoftlock { ... } ``` **优点** - `AntiSoftlockSystem` 订阅 `TransformEventChannelSO _onPlayerSpawned` 而非 `FindFirstObjectByType`,保持零耦合 - `HardAbilityGate` 通过 `SaveManager.Data.World.Switches` 二级验证,防范物品伪解锁 - `RoomEscapeInfoSO.priority` 支持多路逃脱路径优先级排序 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.0 | 命名空间修复后完整对齐 | | 性能 | 9.5 | 卡关检测为低频 Update 定时器 | | 可扩展性 | 9.0 | 多路逃脱 SO + 优先级 | | 编辑器友好 | 9.0 | | | 使用便利性 | 9.5 | | --- ### 2.7 Support — SpeedrunTimer 速通计时器 **涉及文件**:`SpeedrunTimer.cs` **优点** - `Time.unscaledDeltaTime` 免受 HitStop(timeScale < 1)影响 - `_lastDisplayedSecond` 整秒检查跳过字符串重建,避免每帧 GC Alloc - `ISaveable` 集成完整(`OnSave`/`OnLoad` 对 `StatsSaveData.SpeedrunTime`) - `SetVisible` 同步通知 `BoolEventChannelSO`,HUD 可响应 **格式问题(TD-28,低优先级)** 类体在 `namespace` 块内缺少标准 4 空格缩进,与框架其余文件风格不一致。功能正确,建议下次编辑时顺手修复。 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | | | 性能 | 9.5 | 整秒优化到位 | | 可扩展性 | 9.0 | | | 编辑器友好 | 9.5 | | | 使用便利性 | 9.5 | | --- ### 2.8 World — Liquid 液态区域系统 **涉及文件** - `LiquidType.cs` — 枚举:Water/Acid/Lava - `LiquidPhysicsConfigSO.cs` — 水下物理参数 SO - `LiquidZone.cs` — 触发区域,发送 LiquidEventChannel 事件 - `WaterDangerState.cs` — 溺死倒计时(Water 无游泳能力时) - `UnderwaterPostProcessingController.cs` — Volume Weight 混合动画 **修复 TD-29:LiquidZone 无用字段(已修复)** 原 `_dealsDrowningDamage`/`_drowningDamagePerSecond` 带 `#pragma warning disable CS0414`,逻辑从未使用。水下伤害实际由 `WaterDangerState` 通过事件驱动实现,字段已删除并更新注释。 **优点** - Acid/Lava 伤害委托给独立 `HazardZone`(架构分离),LiquidZone 仅负责事件分发 - `WaterDangerState` 在进入时检查 `PlayerStats.HasAbility(AbilityType.Swim)`,零侵入 PlayerController - `UnderwaterPostProcessingController` Coroutine 混合,支持被打断(取消前一个) - `LiquidPhysicsConfigSO` 的 `WaterVolumeProfile` 字段允许每种液体配置独立 Post-Processing Profile **修复 TD-24(UnderwaterPostProcessingController)已在 2.2 记录** **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | 职责分离清晰(Zone/Physics/Danger/FX) | | 性能 | 9.5 | 无热路径 GC,Coroutine 混合合理 | | 可扩展性 | 9.5 | 枚举 + SO 扩展成本极低 | | 编辑器友好 | 9.5 | LiquidPhysicsConfigSO 字段注释详尽 | | 使用便利性 | 9.5 | | --- ### 2.9 World — Puzzle 谜题系统 **涉及文件** - `PuzzleSwitch.cs` — 输入:InteractOnce/Toggle/Pressure 触发模式 - `PuzzleWire.cs` — 逻辑连接器:AND/OR/XOR - `PuzzleReceiver.cs` — 输出:激活目标,持久化到 WorldStateRegistry - `PuzzleDoor.cs` — Receiver 子类:Animancer 开关门动画 **修复 TD-26:PuzzleSwitch/PuzzleReceiver 未从 WorldStateRegistry 恢复存档状态(已修复)** 原代码 `Start()` 仅设置初始值,忽略 WorldStateRegistry 存档: ```csharp // Before — PuzzleSwitch private void Start() => _isActive = _startsActive; // 忽略存档 // Before — PuzzleReceiver protected virtual void Start() { _isActivated = _startsActivated; // 忽略存档 if (_isActivated) OnActivate(); } ``` 修复方案:将状态恢复移至 `Awake()`(保证在 `PuzzleWire.Start()` 的 `Evaluate()` 之前执行),`Start()` 仅负责视觉/回调初始化: ```csharp // After — PuzzleSwitch private void Awake() { bool savedState = !string.IsNullOrEmpty(_switchId) && _worldState != null && _worldState.HasFlag("switch_" + _switchId); _isActive = savedState || _startsActive; } private void Start() { if (_isActive && _activeClip != null) _animancer?.Play(_activeClip); else if (_inactiveClip != null) _animancer?.Play(_inactiveClip); } // After — PuzzleReceiver protected virtual void Awake() { bool savedState = !string.IsNullOrEmpty(_receiverId) && _worldState != null && _worldState.HasFlag("receiver_" + _receiverId); _isActivated = savedState || _startsActivated; } protected virtual void Start() { if (_isActivated) OnActivate(); } ``` **修复原理**:Unity 的 `Awake()` 在所有 `Start()` 之前完成。`PuzzleWire.Start()` 调用 `Evaluate()` 时,所有 `PuzzleSwitch.Awake()` 和 `PuzzleReceiver.Awake()` 已执行完毕,状态已正确恢复。`PuzzleReceiver.Activate()` 有 `if (_isActivated) return;` 守卫,若 Wire 在 Receiver.Start() 之前求值也不会重复触发 `OnActivate()`。 **架构优点** - `PuzzleWire` AND/OR/XOR 纯配置,关卡设计师零代码 - SO 注入 `WorldStateRegistry` 而非单例,测试友好 - `PuzzleDoor` 仅覆写 `OnActivate`/`OnDeactivate`,扩展成本极低 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | 修复后状态管理完整 | | 性能 | 9.5 | 事件驱动,无 Update 查询 | | 可扩展性 | 9.5 | Receiver 子类化成本极低 | | 编辑器友好 | 9.5 | Wire 逻辑类型枚举直观 | | 使用便利性 | 9.5 | | --- ### 2.10 World — PhantomInteractable **涉及文件**:`PhantomInteractable.cs` **修复 TD-27:`LayerMask.NameToLayer` 在热路径(已修复)** ```csharp // Before — 每次 OnTriggerEnter2D 都调用 string 查询 bool isPhantom = other.gameObject.layer == LayerMask.NameToLayer("PhantomBody"); // After — Awake 缓存 private int _phantomBodyLayer; private void Awake() => _phantomBodyLayer = LayerMask.NameToLayer("PhantomBody"); private void OnTriggerEnter2D(Collider2D other) { bool isPhantom = other.gameObject.layer == _phantomBodyLayer; ... } ``` `LayerMask.NameToLayer` 内部进行字符串哈希查找,在 `OnTriggerEnter2D`(频繁回调)中每帧调用是无谓的 CPU 消耗。 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 9.5 | 继承 DirectionalInteractable,职责单一 | | 性能 | 9.5 | 修复后无热路径分配 | | 可扩展性 | 9.5 | | | 编辑器友好 | 9.5 | | | 使用便利性 | 9.5 | | --- ### 2.11 World — WorldMarker **涉及文件** - `WorldMarker.cs` - `WorldMarkerEventChannelSO.cs`(`BaseEventChannelSO`) **优点** - Gizmos 可视化提升关卡编辑效率 - 激活/停用事件分离 **架构注意** `WorldMarkerEventChannelSO` 的事件泛型参数为 `WorldMarker`(MonoBehaviour 引用)。相比传值类型的事件数据(如结构体),携带 MonoBehaviour 引用会造成事件订阅方对场景物件的隐式依赖,降低可移植性。建议后续考虑将 Marker 信息提取为值结构体(含 ID + 位置),仅在 UI 层获取实体引用。 **评分** | 维度 | 分数 | 说明 | |------|------|------| | 架构设计 | 8.5 | Channel 携带 MonoBehaviour ref 存在耦合风险 | | 性能 | 9.5 | | | 可扩展性 | 9.0 | | | 编辑器友好 | 9.5 | Gizmos 完善 | | 使用便利性 | 9.0 | | --- ## 三、Bug 修复汇总 | ID | 文件 | 问题描述 | 严重程度 | 状态 | |----|------|----------|----------|------| | TD-21 | `AccessibilitySettingsSO.cs` | 类在全局命名空间,应为 `BaseGames.Support.Accessibility` | 中 | ✅ 已修复 | | TD-22 | `ColorBlindFilter.cs` | 类在全局命名空间,应为 `BaseGames.Support.Accessibility` | 中 | ✅ 已修复 | | TD-23 | `GlobalSFXPlayer.cs` | 静态单例模式与 ServiceLocator 框架不一致 | 低 | 📝 记录,暂不修改 | | TD-24 | `UnderwaterPostProcessingController.cs` | `OnLiquidExited` 缺少 LiquidType 过滤,任何液体离开都触发重置 | 高 | ✅ 已修复 | | TD-25 | `HardAbilityGate.cs`、`RoomEscapeInfoSO.cs` | 命名空间 `BaseGames.Progression` 与文件夹 `Support/AntiSoftlock` 不符 | 中 | ✅ 已修复 | | TD-26 | `PuzzleSwitch.cs`、`PuzzleReceiver.cs` | `Start()` 忽略 WorldStateRegistry 存档,场景重载后谜题状态丢失 | 高 | ✅ 已修复 | | TD-27 | `PhantomInteractable.cs` | `OnTriggerEnter2D` 热路径中每次调用 `LayerMask.NameToLayer()` | 中 | ✅ 已修复 | | TD-28 | `SpeedrunTimer.cs` | 类体未在 namespace 内缩进(格式问题) | 低 | 📝 记录,下次顺手修 | | TD-29 | `LiquidZone.cs` | 带 CS0414 的无用字段污染 Inspector,逻辑空洞 | 低 | ✅ 已修复 | --- ## 四、框架纯净性审查 > 框架设计原则:无兼容填补、无安全兜底、数据逻辑统一一致 | 检查项 | 状态 | 说明 | |--------|------|------| | 无 `null` 向下兼容路径 | ✅ | 所有新增组件均通过 `Debug.Assert` 或 `?.` 安全调用限定在边界 | | 无 `FindObjectOfType` 运行时查找 | ✅ | 全部通过 Event Channel 或 ServiceLocator 注入 | | 无 `PlayerPrefs` 侵入游戏逻辑 | ✅ | PlayerPrefs 仅限 AccessibilitySettings | | 事件通道复用 | ✅ | `LiquidEventChannelSO` 统一承载 Enter/Exit 两类事件 | | SO 注入(非 Instance 单例)| ✅ | PuzzleWire/Receiver/Switch 均通过 `[SerializeField] WorldStateRegistry` | | 命名空间一致性 | ✅(修复后)| TD-21/TD-22/TD-25 全部修复 | --- ## 五、综合评分 ### 本轮新增模块评分 | 模块 | 架构 | 性能 | 可扩展性 | 编辑器 | 易用性 | 模块均分 | |------|------|------|----------|--------|--------|----------| | Audio Footstep | 9.5 | 9.5 | 9.0 | 9.0 | 9.0 | **9.2** | | UnderwaterAudio | 9.0 | 9.5 | 8.5 | 9.0 | 9.0 | **9.0** | | Tutorial | 9.5 | 9.5 | 9.0 | 9.5 | 9.5 | **9.4** | | Accessibility | 9.0 | 9.5 | 9.0 | 8.5 | 9.0 | **9.0** | | Analytics | 9.5 | 9.0 | 9.5 | 9.5 | 9.5 | **9.4** | | AntiSoftlock | 9.0 | 9.5 | 9.0 | 9.0 | 9.5 | **9.2** | | Speedrun | 9.5 | 9.5 | 9.0 | 9.5 | 9.5 | **9.4** | | Liquid System | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 | **9.5** | | Puzzle System | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 | **9.5** | | PhantomInteractable | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 | **9.5** | | WorldMarker | 8.5 | 9.5 | 9.0 | 9.5 | 9.0 | **9.1** | **本轮新增模块加权均分:9.29 / 10** ### 框架历史累积评分 | 版本 | 评分 | 主要贡献 | |------|------|----------| | v1–v9 | 8.80 | 核心架构、Event Channel、ServiceLocator | | v10–v11 | 9.10 | Combat、Save、Player State Machine | | v12 | 9.25 | Camera、Skills、Equipment | | v13 | 9.45 | BossBase、VFX、Progression Achievements | | **v14(本轮)** | **9.52** | Liquid、Puzzle、Tutorial、Support 模块 + 9项修复 | --- ## 六、遗留改进建议(非阻塞) 1. **`GlobalSFXPlayer` 改用 ServiceLocator(TD-23)** 注册 `IGlobalSFXService` 接口,消除静态单例。优先级:低,当前功能正确。 2. **AudioMixer 快照名常量化** `UnderwaterAudioController`/`AudioManager` 中 `"Underwater"`/`"Default"`/`"BossFight"` 等字符串建议收进 `AudioMixerSnapshots` 常量类,防止拼写错误。 3. **WorldMarkerEventChannelSO 携带值类型** 将 `BaseEventChannelSO` 替换为 `BaseEventChannelSO`(struct),解耦订阅方与场景对象的直接引用。 4. **SpeedrunTimer 缩进格式**(TD-28) 类体应在 namespace 内缩进 4 空格,与全框架风格保持一致。 --- *审查人:GitHub Copilot | 日期:2026 年 5 月 | 覆盖文件:~30 个新增文件 | 修复问题:9 处*