多轮审查评估

This commit is contained in:
2026-05-13 09:19:54 +08:00
parent 458f344e83
commit 1b37297585
57 changed files with 3019 additions and 218 deletions

View File

@@ -0,0 +1,505 @@
# 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<IAudioService>()` 处理 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-21AccessibilitySettingsSO 全局命名空间(已修复)**
```csharp
// Before — 无命名空间
public class AccessibilitySettingsSO : ScriptableObject { ... }
// After
namespace BaseGames.Support.Accessibility
{
public class AccessibilitySettingsSO : ScriptableObject { ... }
}
```
**修复 TD-22ColorBlindFilter 全局命名空间(已修复)**
```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` 免受 HitStoptimeScale < 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-29LiquidZone 无用字段(已修复)**
`_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-24UnderwaterPostProcessingController已在 2.2 记录**
**评分**
| 维度 | 分数 | 说明 |
|------|------|------|
| 架构设计 | 9.5 | 职责分离清晰Zone/Physics/Danger/FX |
| 性能 | 9.5 | 无热路径 GCCoroutine 混合合理 |
| 可扩展性 | 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-26PuzzleSwitch/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<WorldMarker>`
**优点**
- 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**
### 框架历史累积评分
| 版本 | 评分 | 主要贡献 |
|------|------|----------|
| v1v9 | 8.80 | 核心架构、Event Channel、ServiceLocator |
| v10v11 | 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` 改用 ServiceLocatorTD-23**
注册 `IGlobalSFXService` 接口,消除静态单例。优先级:低,当前功能正确。
2. **AudioMixer 快照名常量化**
`UnderwaterAudioController`/`AudioManager``"Underwater"`/`"Default"`/`"BossFight"` 等字符串建议收进 `AudioMixerSnapshots` 常量类,防止拼写错误。
3. **WorldMarkerEventChannelSO 携带值类型**
`BaseEventChannelSO<WorldMarker>` 替换为 `BaseEventChannelSO<WorldMarkerInfo>`struct解耦订阅方与场景对象的直接引用。
4. **SpeedrunTimer 缩进格式**TD-28
类体应在 namespace 内缩进 4 空格,与全框架风格保持一致。
---
*审查人GitHub Copilot | 日期2026 年 5 月 | 覆盖文件:~30 个新增文件 | 修复问题9 处*