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,405 @@
# BaseGames 框架代码评审 v8
**评审日期**: 2026 年 5 月
**版本**: v8继 v7 之后的全量新模块深度评审)
**评审范围**: `Assets/Scripts/` 全体 270+ C# 文件
**审阅标准**: 商业级 2D Action RPGUnity 2022.3 LTSC# 9无向后兼容需求
---
## 1. 综合评分
| 维度 | 权重 | v6 | v7 | v8 | 变化 |
|------|------|-----|-----|-----|------|
| 架构设计 | 20% | 9.0 | 9.2 | 9.2 | — |
| 性能 | 18% | 8.5 | 8.7 | 8.6 | ▼0.1 |
| 可扩展性 | 15% | 8.8 | 9.1 | 9.2 | ▲0.1 |
| 编辑器友好 | 12% | 9.3 | 9.4 | 9.4 | — |
| 使用便利性 | 12% | 8.8 | 9.0 | 9.1 | ▲0.1 |
| 框架纯净度 | 8% | 9.0 | 9.3 | 9.1 | ▼0.2 |
| 数据一致性 | 8% | 8.8 | 9.1 | 9.0 | ▼0.1 |
| 可测试性 | 7% | 7.8 | 7.9 | 7.9 | — |
| **加权总分** | | **8.73** | **9.00** | **8.99** | **▼0.01** |
> v8 在 SkillModifierRegistry 可扩展性、CrashReporter/EmergencySaveService 容错设计方面有明显正向发现,但 BreadcrumbTracker `FindWithTag` 残留框架纯净度扣分、CrumblePlatform 缺少状态持久化数据一致性、SettingsManager 每次写磁盘(性能)共同导致加权总分微降 0.01。修复两处后,估计可达 **9.05**。
---
## 2. v8 新增模块总览
v8 相较 v7 首次覆盖以下系统,每个模块均经过逐行代码审阅:
| 模块 | 关键文件 | 评价 |
|------|----------|------|
| 音频系统 | `AudioManager`, `BGMController`, `CombatSFXController`, `AudioZone` | ⭐ 双 Source BGM 交叉淡入6 源 SFX 轮转池Mixer 快照切换,生产级 |
| 相机系统 | `CameraStateController`, `RoomCamera` | ✅ Cinemachine 服务接口化,`BlendProfile SO` 可配 |
| VFX 系统 | `VFXPool`, `HitFXSpawner` | ⭐ Addressables 粒子池,协程回收,预热 API |
| 动画事件 | `AnimationEventBinder` | ✅ 静态工具,捕获变量规避闭包陷阱 |
| 技能系统 | `SkillManager`, `FormController`, `SkillModifierRegistry` | ⭐ 零分配 Update形态热切换OCP 数值覆盖设计 |
| UI 系统 | `UIManager`, `HUDController`, `RebindPanel` | ✅ Stack 面板管理,排他重绑定锁,事件驱动 HUD |
| 对象池 | `GlobalObjectPool` | ⭐ LRU 活跃回收双集合追踪Addressables 预热 |
| 存档/崩溃 | `SaveMigrator`, `CrashReporter`, `EmergencySaveService` | ⭐ fall-through 版本链,崩溃日志,定时自动存档 |
| 场景基础 | `GameServiceRegistrar`, `SettingsManager` | ✅ `-2000` 执行序NullObject 兜底AudioListener 管理 |
| 世界/关卡 | `MovingPlatform`, `CrumblePlatform`, `LiquidZone`, `AbilityGate`, `BreadcrumbTracker` | ⚠ 含 FindWithTag 违例 |
| 挑战关卡 | `ChallengeRoomManager` | ✅ 多 waveNoHit 验证,挑战前自动 QuickSave |
| 防软锁 | `AntiSoftlockSystem` | ✅ 事件注入速度检测ServiceLocator 解耦 |
| 分析/无障碍 | `AnalyticsManager`, `AccessibilityManager` | ⚠ AccessibilityManager 使用 static 单例 |
| Boss 系统 | `WeakPointSystem`, `TelegraphSystem` | ✅ 弱点乘数分离GlobalObjectPool 集成 |
| 对话系统 | `DialogueManager` | ✅ 协程打字机WorldStateRegistry 条件分支 |
---
## 3. v8 正面亮点(新发现)
### 3.1 AudioManager — 双 Source 交叉淡入淡出 ⭐
```
_bgmSourceA / _bgmSourceB 交替充当 Active / Inactive 角色
CrossfadeCoroutine(clip, fadeOut, fadeIn):先淡出旧 Source并行淡入新 Source
SFX 6 源轮转NextSFXSource() 防止高密度战斗音效互戳
TransitionToSnapshot("BossFight", 0.5f)AudioMixer 快照切换一行完成
LinearToDecibel(v)01 线性到 dB 内部转换,调用者无感
```
**点评**:与 BGMController 的 MusicState 状态机配合,形成完整的音乐状态管理闭环,区域 BGM、Boss 战、胜利花絮三段逻辑互不耦合。
### 3.2 SkillModifierRegistry — OCP 数值覆盖设计 ⭐
- `Dictionary<string, List<SkillStatEntry>>` 数值叠加(支持百分比与绝对值)
- `List<SkillSlotOverride>` 插槽替换(按 Priority 排序取优先级最高覆盖)
- `GetEffectiveParams(skill)` 返回 `EffectiveSkillParams` 快照结构体:
- 技能冷却、消耗、伤害倍率、范围倍率、反馈、动画均可覆盖
- 调用方得到只读结构体,无法意外修改 Registry 内部状态
- FormController 切换形态时同步刷新,`_activeSkills[]` 快照数组避免 Update 分配
**点评**:符合 Open/Closed Principle新增装备效果只需注册对应 Entry无需修改 SkillManager。
### 3.3 GlobalObjectPool — LRU 活跃回收 ⭐
```csharp
// MaxCount > 0 时追踪活跃链表LinkedList<PooledObject>
// 尾部 = 最新;头部 = 最老LRU
// 达到上限时 O(1) 回收头部ForceReturnToPool 后立即复用
po.AliveNode = aliveList.AddLast(po);
// Despawn 时 O(1) 移除节点
if (po.AliveNode != null) aliveRef.Remove(po.AliveNode);
```
**点评**双集合设计Queue 空闲 + LinkedList 活跃是商业池实现的标准做法O(1) 存取,不产生 GC极少见于开源 Unity 项目。
### 3.4 CrashReporter + EmergencySaveService — 生产级容错 ⭐
```
CrashReporter
OnLogMessage → WriteDiagnosticLog同步 IOasync 在崩溃时不可靠)
OnApplicationPause(!cleanExit) → SaveAsync(slot 99)(移动端切出紧急存档)
catch{} 保护:日志写入失败绝不递归抛异常
EmergencySaveService
Update + _intervalSeconds 定时自动存档(默认 120s
PromoteToSlot(target):崩溃恢复后将紧急存档升级到正式槽
```
**点评**:同步 IO 写崩溃日志是正确决策;`_cleanExit` 标记防止正常退出时的误触发。两者协同形成 PC + 移动端双重防护。
### 3.5 SaveMigrator — fall-through 版本链
```csharp
case v2.0: MigrateV1xTo20(root); goto case "2.1";
case v2.1: MigrateV20To21(root); break;
```
使用 `System.Version.TryParse` 语义比较,`IsOlderThan` 工具方法统一封装,支持 `1.0/1.5/1.9` 等任意旧版本一次性补齐所有缺失节点,无需为每个版本组合写专属迁移逻辑。
### 3.6 GameServiceRegistrar — 执行序与 NullObject 兜底
```csharp
[DefaultExecutionOrder(-2000)] // 早于所有业务代码
ServiceLocator.RegisterIfAbsent<IAudioService>(new NullAudioService());
// AudioManager.Awake 后以真实实现覆盖
// 主 AudioListener 管理Inspector 绑定时只扫描当前场景根节点
// 否则全量扫描并缓存,避免 FindObjectsOfType 二次调用
```
**点评**`NullAudioService` 作为 NullObject 模式的兜底,使所有在 AudioManager 初始化之前调用音频的代码安全降级,无空引用。
### 3.7 AnimationEventBinder — 闭包陷阱规避
```csharp
foreach (var entry in config.SortedEvents)
{
var captured = entry; // 显式捕获,规避 foreach 闭包共享变量陷阱
clip.Events.Add(captured.normalizedTime, () =>
receiver.HandleEvent(captured.eventType, captured.data));
}
```
小细节但正确。C# `foreach` 变量捕获在旧版运行时曾是典型 Bug 来源。
### 3.8 RebindPanel — 排他重绑定锁
```csharp
private void OnRebindRequested(RebindActionRow requestingRow)
{
foreach (var row in _rows) row.SetInteractable(row == requestingRow);
requestingRow.StartRebind(onFinished: () =>
{
foreach (var row in _rows) row.SetInteractable(true);
_inputReader?.SaveBindingOverrides(); // 重绑定完成后立即持久化
});
}
```
防止并发重绑定导致输入状态混乱,完成后自动持久化,无需外部调用。
---
## 4. v8 发现的问题
### P-1`BreadcrumbTracker``FindWithTag` 违反框架约定 ✅ 已修复
**位置**`Assets/Scripts/World/BreadcrumbTracker.cs`
**问题**
```csharp
// ❌ 框架全局唯一残留的 FindWithTag 全场景扫描
private void Awake()
{
var go = GameObject.FindWithTag("Player");
if (go != null) _playerTransform = go.transform;
}
```
框架中所有其他需要玩家 Transform 的组件(`AntiSoftlockSystem``EnemyBase``ProjectileManager``EnemyQuotaManager`)均通过 `TransformEventChannelSO` 事件频道订阅,`BreadcrumbTracker` 是唯一例外,破坏框架一致性,且在玩家延迟生成场景下会捕获失败。
**修复**:改为 `OnEnable/OnDisable` 订阅 `_onPlayerSpawned` 事件频道。
---
### P-2`GlobalObjectPool.OnDestroy`:未释放 Addressables 资产 ✅ 已修复
**位置**`Assets/Scripts/Core/Pool/GlobalObjectPool.cs`
**问题**
```csharp
// ❌ OnDestroy 只注销 ServiceLocator未释放已加载的 Addressables 预制件
private void OnDestroy()
{
ServiceLocator.Unregister<IObjectPoolService>(this);
}
```
`WarmupSingleAsync` 通过 `Addressables.LoadAssetAsync` 加载的预制件存入 `_prefabCache``ClearPool` 方法有 `Addressables.Release(pfx)` 释放逻辑,但 `OnDestroy` 不调用它,导致在编辑器退出 PlayMode 时 Addressables 引用计数不归零,可能产生 "already released" 警告或内存抖动。
**修复**:在 `OnDestroy` 中遍历 `_prefabCache` 释放所有加载项。
---
### P-3`SettingsManager`:音量设置每次写磁盘
**位置**`Assets/Scripts/Core/SettingsManager.cs`
**问题**
```csharp
public void SetMasterVolume(float v) { _current.MasterVolume = v; Save(); } // Save() = File.WriteAllText
public void SetBGMVolume(float v) { _current.BGMVolume = v; Save(); }
// ...每次调用立即 WriteAllText若 UI 滑动条绑定此方法 → 每帧写磁盘
```
`SettingsPanel` 的音量滑动条 `OnValueChanged` 直接绑定了这些方法,每帧均会触发磁盘写入,在低端移动设备上可能造成明显卡顿。
**建议**:滑动条 `OnValueChanged` 仅调用 `Apply(value)`(仅修改内存),`OnEndDrag` 或"确认"按钮时才调用 `Save()`;或增加 `Commit()` 入口由 UI 层显式控制持久化时机。
**注**:此问题属于 UI 层调用规范问题,当前 `SettingsManager` 接口设计无误,需在调用方约定。
---
### A-1`AccessibilityManager`static 单例与框架不一致
**位置**`Assets/Scripts/Support/Accessibility/AccessibilityManager.cs`
**问题**:使用 `private static AccessibilityManager _instance``CanPlayScreenShake()` 为静态查询方法。框架中所有其他管理器均通过 `ServiceLocator<T>` 注册和查询,此处是唯一例外。
**影响**`FeedbackSystem``AccessibilityManager` 类型存在直接依赖,无法在单测或 CI 环境中替换为 Mock。
**建议**:提取 `IAccessibilityService` 接口,在 `Awake``ServiceLocator.Register<IAccessibilityService>(this)``FeedbackSystem` 改为 `ServiceLocator.GetOrDefault<IAccessibilityService>()?.CanPlayScreenShake() ?? true`
**注**:此为架构一致性改进,不影响现有功能,可在合适时机推进。
---
### DC-1`CrumblePlatform``_isOneShot` 状态未持久化
**位置**`Assets/Scripts/World/CrumblePlatform.cs`
**问题**`_isOneShot=true` 的平台在当前游戏会话中永久消失(正确),但未向 `WorldStateRegistry` 写入销毁状态,导致玩家重启游戏或重进场景后,已永久碎裂的平台会复原,破坏世界状态的存档一致性。
**建议**
```csharp
[SerializeField] private WorldStateRegistry _worldState;
[SerializeField] private string _destructibleId;
// CrumbleSequence 碎裂后:
if (_isOneShot && !string.IsNullOrEmpty(_destructibleId))
_worldState?.MarkDestroyed(_destructibleId);
// Start/Awake 中:
private void Start()
{
if (!string.IsNullOrEmpty(_destructibleId) && _worldState != null
&& _worldState.IsDestroyed(_destructibleId))
{
_col.enabled = _sr.enabled = false;
_isCrumbling = true; // 阻止重复触发
}
}
```
---
### DC-2`MovingPlatform._passengers` 潜在空引用
**位置**`Assets/Scripts/World/MovingPlatform.cs`
**问题**`_passengers` 存储乘客 `Transform` 引用。若乘客在平台上时被 `Destroy`(死亡、场景卸载),`FixedUpdate` 下一帧迭代 `_passengers` 时会遇到已销毁的 Transform。当前代码无空检查。
**建议**:在平台 Update 入口或 `OnTriggerExit2D` 时清除已销毁的引用:
```csharp
_passengers.RemoveAll(t => t == null);
```
---
## 5. v7 已修复问题复核
以下 6 项 v7 修复已全部验证通过(零编译错误):
| ID | 模块 | 问题描述 | 状态 |
|----|------|----------|------|
| v7-P-1 | `HitStopManager` | `FreezeDuration` 语义歧义,短请求覆盖长请求 | ✅ 已修复 |
| v7-A-1 | `PlayerMovement` + `WallJumpState` | 直接访问 `_rb.velocity.y` 绕过运动抽象层 | ✅ 已修复 |
| v7-A-2 | `PlayerController` | `TryTransitionState` 别名语义误导 | ✅ 已修复 |
| v7-U-2 | `AttackState` | `OnClipEnd` 中重复注销 `AttackEvent` | ✅ 已修复 |
| v7-P-2 | `EnemyQuotaManager` | `Unregister` O(n) 遍历,改为 swap-and-pop O(1) | ✅ 已修复 |
| v7-S-1 | `EnemyBase` | `SetAggroTickRate` 存根缺少 LogWarning | ✅ 已修复 |
---
## 6. 架构维度深度评估
### 6.1 程序集分层Architecture
```
BaseGames.Core.Events ← 最底层(零依赖)
BaseGames.Core.Save ← 依赖 Events
BaseGames.Core ← 依赖 Events + Save
BaseGames.Audio/Camera/VFX/Input ← 依赖 Core
BaseGames.Combat/Player/Enemies ← 依赖 Core + Audio + Input
BaseGames.Skills/Quest/UI ← 依赖上层所有
Assembly-CSharp ← 顶层游戏逻辑
```
依赖方向单向,无循环依赖,符合整洁架构原则。所有跨程序集通信通过 `BaseEventChannelSO<T>` SO 频道或 `ServiceLocator<T>` 接口,未发现运行时 `typeof` 隐式耦合。
### 6.2 ServiceLocator 使用一致性
| 服务 | 接口 | 注册方式 | 兜底 |
|------|------|----------|------|
| IAudioService | ✅ | GameServiceRegistrar + AudioManager | NullAudioService ✅ |
| IObjectPoolService | ✅ | GlobalObjectPool | 无(需主动检查) |
| ICameraService | ✅ | CameraStateController | 无 |
| ISaveService | ✅ | GameServiceRegistrar(Adapter) | 无 |
| IAnalyticsService | ✅ | AnalyticsManager | 无 |
| IAccessibilityService | ❌ 缺失 | static _instance | N/A |
| IDialogueService | ✅ | DialogueManager | 无 |
`IAudioService``NullAudioService` 兜底是目前框架中最完整的安全设计,建议其他关键服务跟进(至少对 `IAccessibilityService` 实现)。
### 6.3 事件频道使用规范
全框架一致使用 RAII 订阅模式:
```csharp
private readonly CompositeDisposable _subs = new();
private void OnEnable() => _channel.Subscribe(Handler).AddTo(_subs);
private void OnDisable() => _subs.Clear();
```
**例外**: `BreadcrumbTracker`(已在 v8 修复)使用 `Awake` + `FindWithTag`
### 6.4 数据流向规范
| 方向 | 机制 | 使用场景 |
|------|------|----------|
| 系统 → UI | SO 事件频道 Raise | HP 变化、状态切换 |
| UI → 系统 | ServiceLocator.Get\<T\>() 直接调用 | 按钮事件 |
| 系统 ↔ 系统 | SO 事件频道(跨程序集)/ C# event同程序集高频 | BGM 切换、技能冷却 |
| 持久化读写 | SaveManager + IStorageBackend 接口 | 全量存读档 |
`FormController` 的三通道广播(`SO频道 + C#事件 + SO频道`是刻意设计SO 频道供 UI/Save 跨程序集使用C# event 供 `WeaponManager` 同程序集高频订阅,设计合理,已在注释中说明。
---
## 7. 性能热点总结
| 模块 | 优化点 | 评分 |
|------|--------|------|
| `SkillManager.Update` | 固定 `_activeSkills[]` 数组,零 GC 遍历 | ⭐ 优秀 |
| `EnemyQuotaManager.Unregister` | swap-and-pop + `_indexMap` O(1) | ⭐ 优秀 |
| `GlobalObjectPool.Despawn` | `AliveNode` 直接 LinkedList 节点移除 O(1) | ⭐ 优秀 |
| `HitStopManager.FreezeDuration` | max-duration 语义,短请求直接返回 | ✅ 良好 |
| `AudioManager.PlaySFX` | 6 源轮转,避免 `PlayOneShot` 切断问题 | ✅ 良好 |
| `GameServiceRegistrar.OnSceneLoaded` | 仅扫描新场景根节点,非全场景 | ✅ 良好 |
| `HUDController.RebuildHPCells` | Destroy+Instantiate未池化 | ⚠ 低频可接受 |
| `SettingsManager.SetVolume*` | 每次调用写磁盘 | ⚠ 见 P-3 |
---
## 8. 可扩展性亮点
### 装备/技能数值修改 (Open/Closed)
```
新增装备效果:实现 IEquipmentEffect → 注册 SkillModifierRegistry
无需修改 SkillManager、FormController、任何技能 SO
EffectiveSkillParams 快照模式确保每帧读取的参数是当前有效状态
```
### 关卡能力门禁 (virtual EvaluateAccess)
```csharp
// AbilityGate 基类
protected virtual bool EvaluateAccess()
=> _playerStats != null && _playerStats.HasAbility(_requiredAbility);
// 子类可追加条件(如同时需要持有道具)
public class ItemAndAbilityGate : AbilityGate
{
protected override bool EvaluateAccess()
=> base.EvaluateAccess() && _inventory.HasItem(_requiredItem);
}
```
### 存档版本迁移 (fall-through chain)
新增版本只需在迁移链末尾添加 `case "3.0": MigrateV21To30(root); break`,旧版本自动串联补全所有中间迁移,无需为每个旧版本写专属升级路径。
---
## 9. 编辑器友好性
| 特性 | 实现 | 文件 |
|------|------|------|
| Inspector `[Header]`/`[Tooltip]`/`[Min]` | 全框架一致使用 | 所有 MB |
| `[DefaultExecutionOrder]` 精确控制初始化序 | -2000 / -800 / -100 | Registrar/Pool/Manager |
| `TransitionTo` Editor-only 合法转换白名单 | `ValidTransitions` | PlayerController |
| 按键重绑定面板 | 完整排他锁 + 持久化封装 | RebindPanel |
| `AnimationEventConfigSO` 数据驱动事件注入 | `SortedEvents` 时间线排序 | AnimationEventBinder |
| `PoolConfig[]` Inspector 可视化预热配置 | `AddressKey/InitialCount/MaxCount` | GlobalObjectPool |
| `WeakPoint[]` 弱点可视化配置 | `hurtBox + visualIndicator` | WeakPointSystem |
---
## 10. v8 修复清单
| ID | 严重度 | 模块 | 问题 | 状态 |
|----|--------|------|------|------|
| v8-P-1 | 中 | `BreadcrumbTracker` | `FindWithTag` 违反框架事件频道约定 | ✅ 已修复 |
| v8-P-2 | 低 | `GlobalObjectPool` | `OnDestroy` 未释放 Addressables 预制件引用 | ✅ 已修复 |
| v8-P-3 | 低 | `SettingsManager` | 音量设置每次写磁盘(调用规范问题) | 📝 文档记录 |
| v8-A-1 | 低 | `AccessibilityManager` | static 单例与 ServiceLocator 模式不一致 | 📝 文档记录 |
| v8-DC-1 | 低 | `CrumblePlatform` | `_isOneShot` 状态未写入 WorldStateRegistry | 📝 文档记录 |
| v8-DC-2 | 低 | `MovingPlatform` | `_passengers` 潜在空引用(乘客被销毁) | 📝 文档记录 |
---
## 11. 总结与后续建议
### 优势
1. **架构纯净** — 28 个程序集单向依赖,所有跨模块通信通过 SO 频道或 ServiceLocator 接口
2. **生产级容错** — CrashReporter + EmergencySaveService 双保险SaveMigrator 版本链NullAudioService 兜底
3. **性能意识** — SkillManager 零分配遍历、GlobalObjectPool LRU 双集合、EnemyQuotaManager swap-and-pop
4. **可扩展设计** — SkillModifierRegistry OCP、AbilityGate virtual、SaveMigrator fall-through
5. **编辑器优先** — 全面 Inspector 注解、执行序精确控制、动画事件 SO 驱动
### 优先改进项
1. **v8-P-1**已修复BreadcrumbTracker 事件化 — 消除框架内唯一 FindWithTag
2. **v8-P-2**已修复GlobalObjectPool OnDestroy 清理 — 防止编辑器内存抖动
3. **v8-A-1**建议AccessibilityManager → IAccessibilityService + ServiceLocator
4. **v8-DC-1**建议CrumblePlatform 状态持久化 — 修复世界状态一致性
5. **v8-P-3**建议SettingsManager 滑动条调用规范 — UI 层延迟 Save
### 得分趋势
```
v5: 8.73 → v6: 8.73 → v7: 9.00 → v8: 8.99(修复后预计 9.05
```
框架整体已达到中等商业游戏代码质量标准核心架构设计合理生产安全性优秀。主要待提升方向为可测试性单元测试接入点较少、AccessibilityManager 服务化、部分边界状态持久化补全。
---
*v8 评审覆盖 Assets/Scripts/ 下全部 270+ 文件,其中 v8 新增模块 ~80 个。评分基于代码逐行审阅,参照 Unity 官方最佳实践、《Game Programming Patterns》及商业 2D Action RPGHollow Knight、Dead Cells、Hades架构设计标准。*