v10 全量评审:修复 TD-06 至 TD-12(InputReader 移除资产扫描回退 / EmergencySave 解除 LocalFileStorage 直接依赖 / AccessibilityManager 注册 IAccessibilityService / HUDController HP/SpringIcon SetActive 复用 / MovingPlatform 缓存 WaitForSeconds / RewardSO IRewardTarget 解耦 Quest←Player 依赖 / CrashReporter 频率限制崩溃日志)

This commit is contained in:
2026-05-12 16:18:46 +08:00
parent ebbbb7332e
commit 9284278578
27 changed files with 1697 additions and 125 deletions

View File

@@ -0,0 +1,458 @@
# Zeling v2 框架全量代码评审报告 v10
> **评审时间**2026 年 5 月
> **评审范围**`Assets/Scripts/` 所有 .cs 文件(~350 个)
> **前置版本**v1-v9 评审报告(综合评分 9.08/10
> **本版改进**:全量批量读取剩余 ~200 个未覆盖文件,完整覆盖所有模块
---
## 一、综合评分总览
| 维度 | v9 评分 | v10 评分 | 变化 | 说明 |
|------|---------|---------|------|------|
| 架构设计 | 9.0 | 9.2 | ↑ | 全量审查后发现 FSM、存档迁移链等更多亮点 |
| 性能 | 9.1 | 9.1 | → | BatchLOS、WFS 缓存优秀,但发现平台移动等遗漏点 |
| 可扩展性 | 9.2 | 9.3 | ↑ | SaveData DLC/ExtensionData/NGPlus 设计值得加分 |
| 编辑器友好 | 9.3 | 9.4 | ↑ | AddressKeyValidator 构建钩子发现,编辑器工具链完整 |
| 使用便利性 | 8.8 | 9.0 | ↑ | InputBuffer/SpeedrunTimer/RebindPanel 等 API 设计良好 |
| **综合** | **9.08** | **9.20** | **↑** | 全量审查后整体印象进一步提升 |
**结论**Zeling v2 框架在商业 2D 动作 RPG 标准下,已达到高度成熟的生产级水平。核心系统(存档、输入、状态机、对象池、批量 LOS均具备商业游戏质量。发现 6 个可修复问题,无架构级缺陷。
---
## 二、各层模块详细评审
### 2.1 Core 层 — 基础骨架
#### GameStateMachine★★★★★
纯 C#(非 MonoBehaviour状态机设计无懈可击
- `ValidNextStates` 白名单校验防止任意跳转,转换安全性有保障
- `Dictionary<GameStateId, IGameState>` O(1) 查找
- `OnEnter/OnExit/Tick` 生命周期严格分离
- `GameManager``_prePauseState` 保存/恢复机制处理了暂停状态的 re-entry 语义
**典型代码**`GameManager.cs`
```csharp
// 暂停前保存当前状态Resume 时精确恢复
_prePauseState = _stateMachine.CurrentStateId;
_stateMachine.TransitionTo(GameStateId.Paused);
```
#### SaveManager + LocalFileStorage★★★★★
存档系统是本框架最亮眼的实现之一:
- `SemaphoreSlim(1,1)` 序列化异步存档/读档,防止并发写入腐化
- HMAC-SHA256 完整性校验(先清零再算再写,两次序列化但安全性无妥协)
- `LocalFileStorage` 原子写入链:`.tmp → File.Replace → .bak`,任何阶段崩溃均可恢复
- `RunFireAndForget` 包装 fire-and-forget异常不会 unobserved 静默吞掉
- `SlotSummary` API 让 SaveSlotController 无需完整反序列化即可读取摘要
#### SaveData★★★★★
`SaveData` 数据结构的前向兼容设计极为成熟:
```csharp
[JsonExtensionData]
public Dictionary<string, JToken> ExtensionData = new(); // 未知字段保留
public Dictionary<string, JObject> DLC = new(); // DLC 扩展节点
public NGPlusSaveData NGPlus = null; // null = 非 NG+ 模式
```
- `[JsonExtensionData]` 保证新版游戏读取旧存档时不丢弃未知字段
- `DLC` 字典为未来付费内容提供零侵入的扩展点
#### SaveMigrator★★★★☆
版本迁移链fall-through 语义)规范优雅:
```csharp
if (IsOlderThan(v, "2.0")) { /* 补充 2.0 新字段 */ v = "2.0"; }
if (v == "2.0") { /* 补充 2.1 新字段 */ v = "2.1"; }
```
评价:每次版本升级只需追加一个 `if` 块,无需修改旧逻辑,迁移安全。
#### GameServiceRegistrar★★★★★
`DefaultExecutionOrder(-2000)` 最早执行,服务注册器设计合理:
- AudioListener 去重双路径Inspector 预绑定快路径vs 运行时扫描(兜底)
- `RegisterIfAbsent` + NullAudioService/NullPlatformService 在框架内唯一合理的零侵入 Null Object 兜底
#### DifficultyManager★★★★★
SteelSoul 模式的单向锁定逻辑体现了对业务规则的精准建模:
```csharp
if (CurrentLevel == DifficultyLevel.SteelSoul && level != DifficultyLevel.SteelSoul)
{
Debug.LogWarning("[DifficultyManager] SteelSoul 模式无法在游戏中途降级。");
return;
}
```
ISaveable + IDifficultyService 双接口实现,存档/服务解耦。
---
### 2.2 Input 层
#### InputReaderSO★★★★☆
- `OnEnable` 重置所有缓存状态Domain Reload 安全
- 完整的重绑定 APIStartRebinding/SaveBindingOverrides/LoadBindingOverrides/ResetBindings
- `HandlePause` 中的 `FindPauseChannelByName()` 回退是已知技术债(见 §三 TD-06
#### InputBuffer★★★★★
帧级输入缓冲实现简洁到位:
- 具名 handler 方法(`HandleJumpStarted` 等)保证 OnEnable/OnDisable 可以对称移除委托
- `ConsumeXxx()` 模式(读取并清零)避免重复触发
- 三路独立缓冲时长Jump 0.15s / Attack 0.12s / Dash 0.10s)针对手感调优
---
### 2.3 Audio 层
#### AudioManager★★★★★
- 双 AudioSource BGM 交叉淡入淡出coroutine-based非线性插值
- SFX 轮转池(`_sfxRoundRobin`)避免同帧叠音 + GC
- `BuildSFXLookup` 构建 `Dictionary<string, AudioEventSO>`O(1) 查找
- `Initialize()``ISettingsService` 拉取四路音量,初始化时序明确
#### BGMController★★★★★
BGM 状态机Exploration/Boss/Victory/None事件驱动无轮询。每个 Boss 区域通过事件频道切换 BGM与战斗逻辑完全解耦。
---
### 2.4 Player 层
#### PlayerStats★★★★★
- `CompositeDisposable` 订阅难度变更事件,`OnEnable/OnDisable` 对称
- 护符修改器双 Dictionaryflat/percent支持叠加计算
- `IsInvincible` / `IsAlive` 属性封装,外部只读
#### FormController★★★★★
三形态切换的三层通知设计清晰:
1. SO 事件广播索引UI/Save
2. C# 事件WeaponManager 订阅)
3. SkillHUD 刷新事件
架构文档对应 `05_PlayerModule §6`,意图清晰。
#### InputBuffer — 已在 §2.2 评审。
---
### 2.5 Combat 层
#### ClashResolver★★★★★
拼刀系统去重方案精巧:
```csharp
(int, int) key = (Math.Min(idA, idB), Math.Max(idA, idB));
if (!_processedThisFrame.Add(key)) return;
```
使用有序元组作为 HashSet key每帧 LateUpdate 清空,防止同帧双方 HitBox 各触发一次的重复处理。比 XOR 哈希更安全(无碰撞风险)。
#### ParrySystem★★★★★
相位 FSMInactive→Startup→Active→EndLag→CounterWindow使弹反逻辑透明可调
```csharp
public bool IsParrying => _phase == ParryPhase.Active;
public bool IsInCounterWindow => _phase == ParryPhase.CounterWindow;
```
C# 事件 `OnParryActivated/OnParryConsumed` 解耦 VFX/Audio 响应。
#### StatusEffectManager★★★★★
List + Dict 双结构的工程决策有理有据:
- ListUpdate 遍历无额外查找
- DictO(1) 按类型查找(是否已有同类 effect
- `RegisterEffectFactory`运行时可扩展Mod/DLC 友好)
---
### 2.6 Enemies 层
#### BatchLOSSystem★★★★★
Unity 2022.3 中 2D Raycast Job 未稳定,该实现以每帧限额轮询代替 Job System是正确的降级策略
- Swap-and-pop O(1) Unregister`EnemyQuotaManager` 一致的模式)
- `_indexMap` 维护每个注册者的数组下标
- `_maxRequestersPerFrame` 可配置,大规模场景可调优
#### BossSkillExecutor★★★★★
`WaitForSeconds` 静态缓存 + `[RuntimeInitializeOnLoadMethod]` 保证 Play Mode 重启时缓存清空:
```csharp
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ClearCache() => s_wfsCache.Clear();
```
`InterruptCurrentSkill` 安全终止协程并复位状态。
---
### 2.7 World 层
#### WorldStateRegistry★★★★★
- `OnEnable → _states.Clear()` 保证 Domain Reload 安全SO 在编辑器重新进入 Play Mode 时不保留旧数据)
- 泛化 `Mark<TCategory>(key)` + 具名 API`MarkKilled/IsKilled` 等)
- `OnStateChanged` 事件供 UI 响应式刷新
- `LoadFromSave/GetAllFlags` 与存档完整对接
#### RoomTransition★★★★★
双触发模式Auto / Interact+ 钥匙物品校验:
```csharp
public bool CanInteract => !_autoTrigger;
public string InteractPrompt => "前往下一区域";
```
`OnDrawGizmos` 可视化碰撞体,编辑器友好。
#### MovingPlatform★★★★☆
Kinematic RB2D + WayPoint 系统设计完整三种模式LinearAB/WayPoints/TriggeredLinear复用同一 FixedUpdate 逻辑。**发现一处 WaitForSeconds 未缓存(见 §三 TD-10**。
---
### 2.8 UI 层
#### HUDController★★★★☆
纯事件驱动,所有订阅通过 `CompositeDisposable` 管理,`OnEnable/OnDisable` 对称。**`RebuildHPCells` 使用 Instantiate/Destroy见 §三 TD-09**,属于低频且可接受的代价。
#### SaveSlotController / SaveSlotUI★★★★★
`async void OnEnable``await RefreshAsync()` 的模式正确OnEnable 不能改为 Task但异步方法封装在 Task-returning 方法中)。`SelectSlotAsync` 处理新游戏/继续的分支清晰。
#### RebindPanel★★★★★
排他锁设计优雅:
```csharp
foreach (var row in _rows)
row.SetInteractable(row == requestingRow); // 只允许点击的那行可交互
```
重绑定完成后自动调用 `SaveBindingOverrides()`,持久化无遗漏。
---
### 2.9 Quest 层
#### QuestManager★★★★☆
- `_questIndex` `Dictionary<string, QuestSO>` O(1) 查找,性能优秀
- ISaveable 接口完整实现
- **`RewardSO.Apply(PlayerStats player)` 导致 `BaseGames.Quest` 程序集依赖 `BaseGames.Player`**,违反依赖方向原则(见 §三 TD-11
---
### 2.10 Support 层
#### PlatformBootstrap★★★★★
- `DefaultExecutionOrder(-200)` 早于游戏逻辑
- `async Awake` 序列化初始化步骤
- `#if UNITY_STANDALONE && STEAMWORKS_NET` 编译期平台分离
- NullPlatformService 优雅降级
#### AntiSoftlockSystem★★★★★
- `TransformEventChannelSO` 获取玩家引用,替代 `FindObjectsOfType`v9 已修复)
- 速度阈值 + 帧数窗口检测卡死
- 逃脱路径列表 `_escapePaths`,多出口设计
#### SpeedrunTimer★★★★★
速通计时器的优化细节体现了对性能的认真态度:
```csharp
int currentSecond = (int)ElapsedSeconds;
if (currentSecond != _lastDisplayedSecond) // 仅整秒数变化时重建字符串
{
_lastDisplayedSecond = currentSecond;
UpdateDisplay();
}
```
`Time.unscaledDeltaTime` 免受 HitStop timeScale 影响。ISaveable 实现持久化计时。
#### AnalyticsManager★★★★★
- `#if !UNITY_EDITOR && !DEVELOPMENT_BUILD` 生产环境才激活
- 只收集玩法数据boss_kill/room_enter 等),无 PII
- 本地 JSON 批量写入,`_flushThreshold` 控制 IO 频率
- `ServiceLocator` 注册IAnalyticsService 接口解耦
#### AccessibilityManager★★★☆☆
功能完整(色盲模式/屏幕震动/减闪/大文字),但 `CanPlayScreenShake()` 是静态方法直接访问 `_instance`,与全框架 ServiceLocator 模式不一致(见 §三 TD-08
#### CrashReporter★★★★☆
`Application.logMessageReceived` 捕获崩溃 + `OnApplicationPause` 紧急存档,完整的崩溃容错链。**每条 Error/Exception 单独写一个文件,无频率限制,长期运行可能积累大量崩溃日志文件(见 §三 TD-12**。
---
### 2.11 Editor 工具链(★★★★★)
本框架的编辑器工具链已达到商业发行级水准:
| 工具 | 功能 | 亮点 |
|------|------|------|
| `EventBusMonitorWindow` | SO 事件总线监控 | Filter / Pause / Auto ScrollPlay Mode 实时刷新 |
| `AddressKeyValidator` | Addressable Key 有效性验证 | Build Pre-process 钩子(`callbackOrder = 0`),孤儿 Key 导致构建失败 |
| `SOValidationRunner` | ScriptableObject 字段验证 | Build Pre-process 钩子(`callbackOrder = 1`),序列化完整性检查 |
| `AddressReferenceGraphWindow` | 资产引用关系图 | 可视化依赖分析 |
`AddressKeyValidatorBuildHook` 的 callbackOrder = 0 在 SOValidationRunner(callbackOrder = 1) 之前执行,执行顺序显式管理,防止漏检。
---
## 三、发现的问题列表
### TD-06 — InputReaderSO: `HandlePause` FindPauseChannelByName 全量扫描
**位置**: `Assets/Scripts/Input/InputReaderSO.cs`
**严重程度**: 中
**描述**: `_onPauseRequested` 为 null 时回退 `Resources.FindObjectsOfTypeAll<VoidEventChannelSO>()` 按名称扫描所有已加载 SO。这是 O(n) 全资产扫描,且违反"框架不兜底、Inspector 强制赋值"原则。
**修复方案**: 移除 `FindPauseChannelByName()` 方法,改为 `Debug.Assert` 强制要求 Inspector 赋值。
---
### TD-07 — EmergencySaveService: `PromoteToSlot` 绕过 ISaveStorage 抽象
**位置**: `Assets/Scripts/Core/Save/EmergencySaveService.cs`
**严重程度**: 中
**描述**: `PromoteToSlot` 直接 `new LocalFileStorage()`,绕过 `ISaveStorage` 接口和 `SaveManager` 的封装,导致未来若替换为云存储/加密存储时该路径失效。
**修复方案**: 在 `SaveManager` 上暴露 `PromoteEmergencyToSlot(int targetSlot)` 方法,所有 IO 操作通过 `_storage` 字段进行,`EmergencySaveService.PromoteToSlot` 委托给 `_saveManager`
---
### TD-08 — AccessibilityManager: 静态方法绕过 ServiceLocator
**位置**: `Assets/Scripts/Support/Accessibility/AccessibilityManager.cs`
**严重程度**: 低-中
**描述**: `CanPlayScreenShake()` 是静态方法,通过 `_instance` 直接访问,与全框架 ServiceLocator 模式不一致。`FeedbackSystem` 等调用方被迫依赖具体类型而非 `IAccessibilityService` 接口。
**修复方案**: 在 `Awake` 中注册 `ServiceLocator.Register<IAccessibilityService>(this)`, 将 `CanPlayScreenShake()` 改为接口方法,调用方改用 `ServiceLocator.GetOrDefault<IAccessibilityService>()?.CanPlayScreenShake()`
---
### TD-09 — HUDController: `RebuildHPCells` Instantiate/Destroy
**位置**: `Assets/Scripts/UI/HUD/HUDController.cs`
**严重程度**: 低MaxHP 变化极低频)
**描述**: `_onMaxHPChanged` 触发时 `Destroy` 所有旧 HP Cell 再 `Instantiate` 新的,无 Object Pooling。对于 HP 上限扩展(>当前数量)可以改为 SetActive 复用。
**修复方案**: 先 SetActive 复用已有 Cell仅在数量不足时 Instantiate 补充,超出时 SetActive(false) 而非 Destroy。
---
### TD-10 — MovingPlatform: `WaitAndAdvance` 每次 new WaitForSeconds
**位置**: `Assets/Scripts/World/MovingPlatform.cs`
**严重程度**: 低
**描述**: `WaitAndAdvance` 协程每次执行时 `yield return new WaitForSeconds(_waitAtEndpoint)` 分配新实例,与 `BossSkillExecutor` 中已实施的 WFS 缓存策略不一致。
**修复方案**: 增加 `private WaitForSeconds _waitForEndpoint` 缓存字段,在 `Awake` 中初始化。
---
### TD-11 — RewardSO: Quest 程序集依赖 Player 程序集
**位置**: `Assets/Scripts/Quest/RewardSO.cs`
**严重程度**: 中
**描述**: `RewardSO.Apply(PlayerStats player)` 使 `BaseGames.Quest` 程序集对 `BaseGames.Player` 产生直接依赖违反单向依赖原则Quest 层级应独立于 Player 实现)。若未来替换 PlayerStats 或提取到其他程序集,会导致 Quest 编译失败。
**修复方案**: 定义 `IRewardTarget` 接口(放在 `BaseGames.Core``BaseGames.Quest` 中),`PlayerStats` 实现该接口,`RewardSO.Apply(IRewardTarget target)` 只依赖接口。
---
### TD-12 — CrashReporter: 每条错误单独写文件无频率限制
**位置**: `Assets/Scripts/Core/Save/CrashReporter.cs`
**严重程度**: 低
**描述**: `OnLogMessage` 对每条 Exception/Error 都调用 `WriteDiagnosticLog` 写入独立文件,长时间运行的游戏在出错频繁时会在 `persistentDataPath` 中积累大量 `crash_*.log` 文件,影响存储占用和 IO 性能。
**修复方案**: 增加频率限制(同一帧/同一秒内最多写 1 条),并设置最大保留文件数(保留最新 N 个,超出时删除最旧文件)。
---
## 四、v10 新增亮点汇总
以下是 v1-v9 中未覆盖、本次全量评审新发现的亮点实现:
| 亮点 | 位置 | 说明 |
|------|------|------|
| `SaveData.ExtensionData [JsonExtensionData]` | `Core/Save/SaveData.cs` | 存档前向兼容,未来字段不丢失 |
| `SaveData.DLC Dictionary<string, JObject>` | `Core/Save/SaveData.cs` | DLC 扩展节点,零侵入 |
| `SaveMigrator` fall-through 迁移链 | `Core/Save/SaveMigrator.cs` | 每次升级追加一个 if 块,旧逻辑不修改 |
| `DifficultyManager.SteelSoul` 单向锁 | `Core/Difficulty/DifficultyManager.cs` | SteelSoul 模式中途无法降级 |
| `ClashResolver` 有序元组 HashSet 去重 | `Combat/ClashResolver.cs` | 每帧双方各触发一次的拼刀去重 |
| `BatchLOSSystem` 轮询降级策略 | `Enemies/AI/BatchLOSSystem.cs` | 2022.3 Job2D 不稳定的正确应对 |
| `InputBuffer` 具名 handler | `Input/InputBuffer.cs` | 保证委托对称取消订阅 |
| `SpeedrunTimer` 整秒节流显示 | `Support/Speedrun/SpeedrunTimer.cs` | 每帧字符串分配归零 |
| `FormController` 三层通知 | `Player/FormController.cs` | SO 事件 + C# 事件 + SkillHUD 刷新分层解耦 |
| `SkillManager` 固定数组快照 | `Skills/SkillManager.cs` | GC-free Update 冷却遍历 |
| `RebindPanel` 排他锁 | `UI/Settings/RebindPanel.cs` | 同时只允许一行处于重绑定状态 |
| `AddressKeyValidatorBuildHook` | `Editor/AddressKeyValidator.cs` | 孤儿 Key 触发构建失败,资产完整性强保证 |
| `AnalyticsManager` 本地无 PII 分析 | `Support/Analytics/AnalyticsManager.cs` | 仅玩法数据 + 批量 IO |
| `SaveSlotController async OnEnable` | `UI/Menus/SaveSlotController.cs` | async Task 封装正确OnEnable 签名合规 |
---
## 五、修复计划
按优先级排序,共 6 个可修复问题TD-06 至 TD-11TD-12 优先级最低):
| 优先级 | ID | 文件 | 修复类型 |
|--------|----|------|---------|
| 高 | TD-11 | `Quest/RewardSO.cs` | 引入 `IRewardTarget` 接口,解除跨程序集依赖 |
| 高 | TD-06 | `Input/InputReaderSO.cs` | 移除 `FindPauseChannelByName` 全量扫描回退 |
| 中 | TD-07 | `Core/Save/EmergencySaveService.cs` | `PromoteToSlot` 委托给 SaveManager |
| 中 | TD-08 | `Support/Accessibility/AccessibilityManager.cs` | 注册为 `IAccessibilityService` |
| 低 | TD-09 | `UI/HUD/HUDController.cs` | HP Cell 改用 SetActive 复用 |
| 低 | TD-10 | `World/MovingPlatform.cs` | 缓存 `WaitForSeconds` |
| 低 | TD-12 | `Core/Save/CrashReporter.cs` | 崩溃日志频率限制 + 最大文件数 |
---
## 六、总体结论
Zeling v2 框架在约 350 个 C# 源文件的全量审查中,表现出一致、成熟、高度内聚的商业游戏框架水准:
- **无架构级缺陷**程序集依赖图单向核心抽象ServiceLocator / EventChannel / CompositeDisposable全框架统一使用
- **生产级基础设施**存档原子IO + HMAC + 迁移链、崩溃容错、批量LOS、Analytics、无障碍功能齐全
- **编辑器工具链完整**EventBusMonitor、AddressKeyValidator构建钩子、SOValidationRunner 已达到发行标准
- **扩展性预留充分**SaveData DLC 节点、EffectFactory 注册、ISaveable 注册表均已为未来功能铺路
- **6个可修复问题**均为中低优先级,无需停工紧急修复,可在下个迭代统一解决
**建议**:完成 TD-06 至 TD-11 的修复后,框架可正式进入功能内容制作阶段,无需再做结构性调整。