多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,549 @@
# zeling_v2 全面代码评审
> **评审日期**2026-05-11
> **评审范围**`Assets/Scripts/` 全部 C# 代码(约 25 个模块100+ 个文件)
> **评审标准**:成熟商业独立游戏水准(参照 Hollow Knight / Dead Cells / Celeste 量级)
> **总体评分****8.2 / 10**
---
## 目录
1. [总体评价](#1-总体评价)
2. [架构设计](#2-架构设计)
3. [性能](#3-性能)
4. [可扩展性](#4-可扩展性)
5. [编辑器友好性](#5-编辑器友好性)
6. [使用便利性与开发者体验](#6-使用便利性与开发者体验)
7. [分模块详细评估](#7-分模块详细评估)
8. [横切关注点](#8-横切关注点)
9. [风险与残留问题](#9-风险与残留问题)
10. [改进建议优先级清单](#10-改进建议优先级清单)
---
## 1. 总体评价
### 优势总结
这是一套**远高于大多数 Unity 独立游戏原型**水准的代码库。核心架构决策正确,模式选择合理,绝大多数关键热路径已经过性能优化。以下几点值得特别称赞:
- **程序集分层清晰**25 个 `.asmdef` 按功能边界划分,循环引用为零。
- **SO Event Channel 模式**:解耦彻底,任何两个系统之间都不存在直接 `GetComponent` 跨模块调用。
- **数据驱动设计**:几乎所有可调参数都在 ScriptableObject 中,热更改无须重编。
- **伤害流水线清晰**HurtBox 8 步流水线无敌帧→弹反→霸体→护盾→防御→TakeDamage→广播→DoT逻辑完整边界清楚。
- **异步存档**`SaveManager` 采用 `async/await` + `Newtonsoft.Json` + HMAC-SHA256 校验,版本迁移链完整。
### 主要短板
- **测试覆盖缺失**:无任何单元测试文件,关键算法(伤害计算、存档迁移、技能冷却)全靠运行时验证。
- **单例滥用**`GameManager``SaveManager``QuestManager` 三者同时持有静态 `Instance` 属性,与 `ServiceLocator` 体系形成**双轨注册**,职责模糊。
- **线程安全缺口**`SaveManager` 的异步 I/O 路径与主线程直接共享 `_current` 状态,无 Lock 保护。
- **输入架构部分未完成**`InputReaderSO` 是 SO 但依赖 `InputActionAsset` 并在 `OnEnable` 重新绑定,在领域驱动设计视角下存在潜在的状态管理混乱。
---
## 2. 架构设计
### 2.1 分层与依赖方向
```
┌─────────────────────────────────────────────────────┐
│ UI / VFX / Feedback / Editor (叶节点,只依赖下方) │
├─────────────────────────────────────────────────────┤
│ Player.States / Enemies.AI / Quest / World / Skills │
├─────────────────────────────────────────────────────┤
│ Player / Enemies / Combat / Camera / Equipment │
├─────────────────────────────────────────────────────┤
│ Core (ServiceLocator / GameStateMachine / Events) │
│ Core.Save / Core.Pool / Core.Events │
└─────────────────────────────────────────────────────┘
```
**评分9/10**
依赖方向严格向下,没有发现上层模块被下层引用的情况。`BaseGames.Combat.StatusEffects` 独立程序集、`BaseGames.Enemies.Navigation` 抽象为 `IPathAgent` 接口——这种**反向依赖消除**的处理非常专业。
**缺陷**
- `QuestManager``CameraStateController` 仍保留 `static Instance`,与 `ServiceLocator` 方案并存。若要统一,`ICameraService` 已经完成了接口化,`QuestManager` 还未接入。
- `GameServiceRegistrar``ExecutionOrder -2000`)和 `GameManager``-1000`)都向 `ServiceLocator` 注册部分重叠的服务(`IDeathRespawnService``ISceneService`),后注册者会无声地覆盖前者。
### 2.2 状态机
`GameStateMachine` 对状态合法性有**白名单校验**`ValidNextStates`),这是商业游戏中防止非法状态跳转的标准实践。`PlayerController` 的手写状态字典方案(非继承 MonoBehaviour也避免了 `Animancer.FSM``PlayerController` 之间的耦合。
**评分8.5/10**
缺陷Player 状态机不校验合法转换(任何状态都可以调 `TransitionTo` 跳到任何状态),在大型团队中会产生维护风险。
### 2.3 事件系统
`BaseEventChannelSO<T>` + `VoidBaseEventChannelSO` 的 SO 事件频道是当前 Unity 最佳实践之一。`EventSubscription` disposable 句柄也有效消除了忘记退订的内存泄漏。
**评分9.5/10**
唯一可改进点:`BaseEventChannelSO` 在编辑器下调用 `GetInvocationList()`(会产生 GC 分配),这发生在 `Raise()` 热路径上。
改进方式:用 `#if UNITY_EDITOR` 守卫下的计数器字段(而非调用 `GetInvocationList`)替代。
### 2.4 依赖注入
`ServiceLocator` 实现简洁、测试友好(`OverrideForTest` / `Reset` 仅在 `UNITY_EDITOR` 下可见)。`GetOrDefault` 支持可选服务,避免空引用异常。
**评分8/10**
缺陷:
- 没有生命周期管理(`Dispose`)——如果服务是 `MonoBehaviour`,场景卸载后 `_services` 字典中仍持有对已销毁对象的引用。建议在 `OnDestroy` 中调用 `ServiceLocator.Unregister<T>()`
- 无法区分"设计上不存在"的服务和"还未注册"的服务,两者调用 `Get<T>()` 都会抛异常,错误信息不够精确。
---
## 3. 性能
### 3.1 热路径 GC 分配
| 位置 | 实现 | GC 状态 |
|------|------|---------|
| `HitBox.OnTriggerEnter2D` | `DamageInfo.From(so)` struct 工厂 | ✅ 零堆分配 |
| `SkillManager.Update` | `_activeSkills[]` 快照数组 + Dictionary | ✅ 零堆分配 |
| `BatchLOSSystem.FixedUpdate` | 顺序 Raycast2D节流分帧 | ✅ 可接受 |
| `StatusEffectManager.Update` | 逆序遍历 `_activeList`List<T> | ✅ 零分配 |
| `WorldStateRegistry.Mark` | HashSet.Add首次创建 HashSet | ⚠️ 首次调用有小分配,之后零分配 |
| `BaseEventChannelSO.Raise`(编辑器) | `GetInvocationList()` | ❌ 每次 Raise 分配 `Delegate[]` |
| `SaveManager.SaveAsync` | 两次 `JsonConvert.SerializeObject` | ⚠️ 存档期间大量 string 分配,可接受(低频) |
| `InputBuffer.Update` | 3 个 `Mathf.Max` float | ✅ 零分配 |
| `PlayerMovement.FixedUpdate` | `new Vector2` | ✅ struct无堆分配 |
| `GlobalObjectPool` Spawn | `Queue<PooledObject>` Dequeue | ✅ 零分配(池命中时) |
**整体性能评分8.5/10**
### 3.2 物理检测
`PlayerMovement.CheckGrounded()` 使用 `Physics2D.OverlapBox`,每 FixedUpdate 执行一次,这是标准做法。
`BatchLOSSystem` 将 LOS 射线检测分帧节流(每帧最多 `_maxRequestersPerFrame` 次),避免敌人增多时爆帧——这是经典的 **Temporal Spreading** 模式,专业。
**缺陷**`HitBox._hitCooldownTimers`Dictionary 结构)在 `OnDisable` 时调用 `Clear()`——如果每帧有大量命中Dictionary 操作会有轻微 GC 压力。考虑用 `int[]` 按帧号记录命中替代。
### 3.3 对象池
`GlobalObjectPool` 实现完整:
- Addressables 异步预热(统一走 `WarmupSingleAsync`Coroutine 桥接)
- `Queue<PooledObject>` 空闲池
- `LinkedList<PooledObject>` 活跃对象 LRU
- `MaxCount` 强制容量上限
**评分8/10**
**缺陷**:池满时(`MaxCount > 0` 且活跃数达上限)直接返回 null 而不是回收最老对象。在密集弹幕场景下会出现弹丸"消失"。建议改为回收 LRU 节点后复用。
### 3.4 动画系统
采用 **Animancer**双层Layer 0 Base + Layer 1 Overlay避免了 Unity Animator 的状态机 GC 和字符串参数哈希问题。`AttackState` 通过 Animancer 归一化时间事件驱动 HitBox 激活,比 `Animation Event` 更精确可控。
---
## 4. 可扩展性
### 4.1 数据驱动度
**评分9.5/10**
几乎所有参数均通过 ScriptableObject 暴露:
| SO 类型 | 用途 |
|---------|------|
| `PlayerMovementConfigSO` | 移动、加速度、跳跃力 |
| `PlayerAnimationConfigSO` | 动画片段引用、HitBox 时间点 |
| `DamageSourceSO` | 基础伤害、类型、标记、特效 |
| `ProjectileConfigSO` | 速度、生存时间、池 Key |
| `FormSkillSO` | 技能类型、消耗、冷却 |
| `CharmSO` | 护符 Notch 成本、效果链 |
| `QuestSO / QuestObjectiveSO` | 任务目标、条件、奖励 |
| `ChallengeRoomSO` | 敌人波次、计时 |
| `LootTableSO` | 掉落概率表 |
| `EnemyStatsSO` | HP、防御、伤害 |
连击段数(`AttackState` 读取 `AnimCfg.GroundAttacks.Length`)、技能冷却(`FormSkillSO`)、伤害标记(`DamageFlags` enum均完全数据驱动**策划无需改代码即可调整绝大多数玩法参数**。
### 4.2 Combat 可扩展性
`DamageFlags``[Flags] enum`,新增伤害属性只需增加枚举值,不需要修改任何现有代码——开闭原则实践良好。
`CharmEffect``OnEquip(EquipmentContext ctx)` / `OnUnequip` 抽象基类模式使新护符完全独立于 `EquipmentManager` 逻辑——这是 **Strategy 模式**的正确用法。
`StatusEffect` 基类 + 子类(`FireEffect``PoisonEffect``StaggerEffect`)结构同样符合开闭原则。
**评分9/10**
### 4.3 敌人 AI 可扩展性
Behavior Designer 节点封装为独立 `BD_*` 类,与 `EnemyBase` 通过虚方法接口(`BeginAttack``MoveTo`)交互——不依赖具体实现。
`IPathAgent` 接口对导航完全解耦可以在不修改敌人逻辑的情况下替换导航后端PathBerserker2d / NavMesh / 自定义)。
**评分8.5/10**
**缺陷**`EnemyBase` 使用简单的 `EnemyStateType` enum而非真正的 FSM复杂 Boss 行为会在 `TakeDamage` / `Die` 方法里堆积 `if/switch`。建议 Boss 使用独立 FSM 或行为树驱动。
### 4.4 存档可扩展性
`SaveMigrator` 的链式 `goto case` 迁移模式是正确的版本迁移方案。`SaveData``[JsonExtensionData]` 字段允许 DLC 和未知字段的向前兼容。`NGPlusSaveData``null` 表示非 NG+ 模式,语义清晰。
**评分9/10**
---
## 5. 编辑器友好性
### 5.1 Inspector 暴露
**评分8.5/10**
- 所有 MonoBehaviour 字段均有 `[Header]` 分组Inspector 布局清晰。
- `[RequireComponent]` 用于强制依赖(`HitBox``HurtBox``Projectile` 等),避免配置遗漏。
- `[DefaultExecutionOrder]` 显式标注执行顺序(`-2000` / `-1000` / `-900` / `-200` / `-100`),避免 Awake 初始化竞态。
- `[CreateAssetMenu]` 覆盖所有 SO 类型,策划可直接右键菜单创建。
**缺陷**
- `PlayerAnimationConfigSO` 中的 `GroundAttackHitBoxEnterTimes[]``GroundAttackHitBoxExitTimes[]` 是并行数组,容易在 Inspector 中出现长度不匹配的问题。建议改用嵌套 struct
```csharp
[Serializable]
public struct AttackTimings { [Range(0,1)] public float Enter, Exit; }
public AttackTimings[] GroundAttackTimings;
```
- `GlobalObjectPool.PoolConfig.MaxCount = 0` 表示无上限,但 Inspector 中没有 `[Tooltip]` 说明,容易被误填为"不允许生成任何对象"。
- 部分 `[SerializeField] private string _id` 缺少 `[Tooltip]`,大型团队中会增加沟通成本。
### 5.2 调试工具
`EventBusMonitor``#if UNITY_EDITOR`)记录事件名称、负载、监听者数量和帧号——这是**专业调试基础设施**,在大型团队中能极大加速问题定位。
`Debug.Assert` 保护关键依赖(`EquipmentManager.Awake`),在 Editor 运行时会立即报告配置问题。
**评分8/10**
**缺陷**
- `EventBusMonitor` 数据只在内存中,没有 EditorWindow 面板可视化。若增加一个实时 Event Flow 面板,调试效率会大幅提升。
- 没有统一的 `OnValidate` 验证框架,目前靠运行时 `Debug.LogWarning` 发现配置问题,而非编辑器实时提示。
### 5.3 Gizmos
未见 `OnDrawGizmos` 实现(除标准 Unity 内置)。商业游戏中通常会为 `HurtBox`、`HitBox`、`LOS Ray`、`Room Bounds` 添加 Gizmos 可视化,便于关卡设计师直观调试判定盒和视野范围。
---
## 6. 使用便利性与开发者体验
### 6.1 API 设计
**评分8.5/10**
- `EquipmentManager.TryEquipCharm` 返回 `string?`null = 成功,非 null = 错误原因)是简洁的结果模式,优于 `bool` + `out string error`。
- `WorldStateRegistry` 泛化 API`IsMarked` / `Mark`+ 具名向后兼容 API`IsCollected` / `MarkCollected`)的两层设计既保持灵活又兼顾可读性。
- `DamageInfo.Builder` + `DamageInfo.From(so)` 双路径高频热路径用零分配工厂复杂场景Boss 特殊伤害)用 Builder——设计考虑到位。
- `ServiceLocator.GetOrDefault<T>()` 的可选服务查询比 `try-catch` 方式性能更好API 意图也更清晰。
**缺陷**
- `PlayerController` 公开属性列表过长15+ 个)。作为协调器这是可以理解的,但若进一步将上下文分组(`PlayerMovementContext`、`PlayerCombatContext`),状态类的代码会更简洁。
- `SaveManager.Data` 属性标注了 `[Obsolete]` 但仍为 public在大型团队中会产生混淆建议改为 `internal` 并添加 `EditorBrowsable(Never)`。
### 6.2 代码可读性
**评分9/10**
- 方法体普遍短小(< 30 行),单一职责明确。
- 注释质量高:关键类和方法均有 `<summary>` XML 文档注释,并附有架构编号引用(如"架构 06_CombatModule §5")。
- 命名规范统一:`_camelCase` 私有字段,`PascalCase` 公共属性,`TryXxx` 用于可失败操作,`HandleXxx` 用于事件处理。
**缺陷**
- 中文注释和英文代码混排在部分文件中存在(`InputReaderSO` 全英文,`PlayerController` 全中文),团队规范不统一。
- `AttackState.PlayAttackClip` 中内联了防御性三元运算符兜底逻辑(`?? 0.3f`),建议将默认值提取到常量字段以明确语义:
```csharp
private const float DefaultHitBoxEnterTime = 0.3f;
private const float DefaultHitBoxExitTime = 0.6f;
```
### 6.3 错误处理
**评分7.5/10**
- `ServiceLocator.Get<T>()` 抛出 `InvalidOperationException`,错误信息包含类型名和配置提示,比 `NullReferenceException` 友好得多。
- `SaveManager.LoadAsync` 捕获 `JsonException` 并返回 `false`,不会因存档损坏崩溃。
- `InputReaderSO` 在找不到 ActionMap 时只发出 `Debug.LogError`,仍然正常运行(降级处理)。
**缺陷**
- `QuickSave` / `QuickLoad` 调用 `RunFireAndForget`,异步异常会被静默吞掉(只有日志)。在存档失败场景下,玩家不会得到任何 UI 反馈。
- `GlobalObjectPool.Spawn` 在池空且 MaxCount 已达时返回 `null`,调用方(`ProjectileManager.Spawn`)需要自行处理 null但代码中未见统一的 null guard。
---
## 7. 分模块详细评估
### 7.1 Core 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `ServiceLocator` | 9/10 | 轻量、类型安全、测试友好,生命周期管理待完善 |
| `GameStateMachine` | 8.5/10 | 合法性校验完整,缺少状态转换日志 |
| `GameManager` | 7.5/10 | 单例+ServiceLocator双轨服务注册与GameServiceRegistrar重叠 |
| `GameServiceRegistrar` | 8/10 | ExecutionOrder -2000 正确AudioListener 修复逻辑健壮 |
| `DeathRespawnService` | 8.5/10 | 局部 lambda 订阅模式正确,避免了全局 bool 轮询 |
### 7.2 Combat 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `HurtBox` | 9/10 | 8 步流水线完整,接口注入优雅,注释清晰 |
| `HitBox` | 8.5/10 | 命中冷却计时器设计合理ClashResolver 接口清晰 |
| `DamageInfo` | 9/10 | struct + Builder + 零分配工厂三路并存,文档注释清晰 |
| `ClashResolver` | 8/10 | 拼刀检测独立化,职责单一 |
| `StatusEffectManager` | 8.5/10 | 双结构List+Dict设计专业MaterialPropertyBlock 不污染共享材质 |
| `Projectile` | 8/10 | 模板方法模式正确ReturnToPool 路径清晰 |
### 7.3 Player 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `PlayerController` | 8.5/10 | 协调器职责明确,公开属性略多,状态机无转换白名单 |
| `PlayerMovement` | 9/10 | FixedUpdate 物理、Coyote Time、单向平台均正确实现 |
| `PlayerStats` | 8.5/10 | ISaveable + IRestoreOnSave 双接口设计合理,难度缩放逻辑正确 |
| `FormController` | 9/10 | 三事件链SO事件/C#事件/SkillHUD刷新分层清晰 |
| `InputReaderSO` | 7.5/10 | OnEnable 重置+重绑定解决了编辑器重置问题,但状态管理复杂 |
| `InputBuffer` | 9/10 | 极简实现,命名 handler 避免了匿名 lambda 退订问题 |
| `SkillManager` | 8.5/10 | `Dictionary<FormSkillSO,float>` 动态冷却优雅,零 GC Update |
### 7.4 Enemies 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `EnemyBase` | 8/10 | IDamageable + ILOSRequester 实现完整BD 虚方法接口设计好 |
| `BatchLOSSystem` | 9/10 | 分帧节流是正确的性能优化,升级 Job System 路径清晰 |
| `BD_* 节点` | 8.5/10 | 每个节点职责单一,通过 EnemyBase 接口解耦,可测试性好 |
| `EnemyQuotaManager` | 8/10 | 激活敌人配额系统,防止屏幕外敌人堆积 |
| `LootResolver` | 8/10 | LootTableSO 数据驱动,概率计算逻辑清晰 |
### 7.5 Save 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `SaveManager` | 8/10 | async/await + HMAC-SHA256 + 具名访问器设计正确,**线程安全待补** |
| `SaveData` | 9/10 | `[JsonExtensionData]` 前向兼容NGPlus null 语义明确 |
| `SaveMigrator` | 9/10 | 链式 `goto case` 版本迁移是正确模式 |
| `LocalFileStorage` | 7/10 | 接口隔离便于测试,**缺少原子写入**(先写临时文件再重命名) |
| `EmergencySaveService` | 8/10 | 崩溃时存档兜底,设计考虑周全 |
### 7.6 World 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `WorldStateRegistry` | 9/10 | `Dictionary<Category,HashSet>` + `OnStateChanged` 响应式设计 |
| `RoomController` | 8.5/10 | `ServiceLocator.GetOrDefault<ICameraService>` 解耦正确 |
| `Collectible` | 8/10 | WorldStateRegistry 注入,幂等拾取逻辑 |
| `SavePoint` | 8/10 | 触发存档逻辑清晰,激活状态持久化 |
### 7.7 Camera 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `CameraStateController` | 9/10 | `ICameraService` 实现 + ServiceLocator 注册,仍保留 `static Instance` 双保险 |
| `CameraBlendProfileSO` | 8.5/10 | 混合配置数据驱动,每个房间可独立配置 |
### 7.8 UI 模块
| 组件 | 评分 | 说明 |
|------|------|------|
| `UIManager` | 8/10 | `Stack<GameObject>` Panel 管理模式,事件驱动状态响应 |
| `PlayerFeedback` | 8.5/10 | IFeedbackPlayer 接口解耦 Feel 框架NamedPresets 字典查找便捷 |
| `ToastManager` | 8/10 | 简洁的通知队列实现 |
---
## 8. 横切关注点
### 8.1 命名空间一致性
`BaseGames.*` 命名空间与 `.asmdef` 文件名严格对应,没有出现跨命名空间的类型混用。
**评分9.5/10**
### 8.2 注释文档质量
XML `<summary>` 覆盖率高,关键方法(`HurtBox.ReceiveDamage`、`GlobalObjectPool.Spawn`)有详细的步骤注释。部分 TODO 注释("Phase 2加载存档场景TODO")说明了实现阶段,有助于团队理解完成度。
**评分8.5/10**
### 8.3 线程安全
`SaveManager` 的 `_current`、`_saveables`、`_currentSlot` 等字段在主线程和 `async Task` 中共享读写,**没有任何同步原语保护**。在 Unity 主线程模型下绝大多数 `await` 续体会回到主线程,但 `LocalFileStorage.WriteAsync` 若使用 `Task.Run` 会真正在线程池执行,此时主线程的 `foreach (var s in _saveables)` 若并发修改则有竞态。
**评分6.5/10**(主要风险点)
建议:对 `SaveAsync` 加 `SemaphoreSlim` 保证同一时刻只有一个存档操作,`_saveables` 改用线程安全集合或在 `SaveAsync` 开始时做快照(`_saveables.ToList()`)。
### 8.4 内存管理
- ScriptableObject 在 Play 期间修改的字段(如 `WorldStateRegistry._states`)在 Editor 中**不会自动重置**,需要在 `OnEnable` 或脚本中显式 `Reset()`。当前实现是 `LoadFromSave` 中调用 `_states.Clear()`,但如果测试时直接 Play 而不加载存档,旧数据会残留——这是 Unity Editor 开发的常见陷阱。
- `EventChannelRegistry` 继承 `MonoBehaviour`(挂在场景物体上),但 SO 事件频道自身持有 `event Action`——场景卸载后若订阅者未退订会造成悬空引用。
### 8.5 错误恢复
`CrashReporter` + `EmergencySaveService` 的存在说明设计者考虑了崩溃恢复场景,这在商业游戏中是必须的。
**评分8/10**
---
## 9. 风险与残留问题
### P0严重可能导致数据丢失或崩溃
| # | 问题 | 文件 | 描述 |
|---|------|------|------|
| 1 | 存档竞态风险 | `SaveManager.cs` | `_current` 在异步路径和主线程同时访问,无同步保护 |
| 2 | 存档非原子写入 | `LocalFileStorage.cs` | 写入中断会产生损坏文件,需先写临时文件再重命名 |
| 3 | 池满时 null 返回未处理 | `GlobalObjectPool.cs` | `Spawn` 返回 null部分调用方未做 null guard |
### P1影响功能稳定性
| # | 问题 | 文件 | 描述 |
|---|------|------|------|
| 4 | 服务双轨注册 | `GameManager.cs` + `GameServiceRegistrar.cs` | `IDeathRespawnService` 被注册两次,后者覆盖前者,行为取决于执行顺序 |
| 5 | SO 运行时状态不重置 | `WorldStateRegistry.cs` | Editor Play 直接进游戏不加载存档时,上次 Play 的状态可能残留 |
| 6 | Player 状态机无白名单 | `PlayerController.cs` | 任意状态可直接跳转到任意状态,大型开发中易引入非法状态 |
### P2质量改进
| # | 问题 | 描述 |
|---|------|------|
| 7 | `AttackState` 并行数组 | `GroundAttackHitBoxEnterTimes[]` 与 `ExitTimes[]` 长度需手动保持一致,易错 |
| 8 | `EventBusMonitor` GetInvocationList GC | 编辑器模式每次 Raise 分配 `Delegate[]` |
| 9 | `QuestManager` 未接入 ServiceLocator | 仍依赖 `static Instance`,与架构方向不一致 |
| 10 | 无单元测试 | 存档迁移、伤害计算、技能冷却等关键逻辑无自动化验证 |
| 11 | 无 `OnDrawGizmos` | 运行时 HitBox / HurtBox / LOS 边界不可视,关卡调试成本高 |
---
## 10. 改进建议优先级清单
### P0 — 必须修复
```csharp
// 1. LocalFileStorage原子写入先写 .tmp 再重命名覆盖)
private async Task WriteAtomicAsync(int slot, string json)
{
string path = GetPath(slot);
string tmp = path + ".tmp";
await File.WriteAllTextAsync(tmp, json);
File.Move(tmp, path, overwrite: true); // 原子替换,崩溃不会损坏旧存档
}
// 2. SaveManager防并发保护
private readonly SemaphoreSlim _saveLock = new SemaphoreSlim(1, 1);
public async Task SaveAsync(int slot = -1)
{
await _saveLock.WaitAsync();
try
{
var snapshot = _saveables.ToList(); // 主线程快照,防并发修改
// ... 原有逻辑,使用 snapshot 而非 _saveables ...
}
finally { _saveLock.Release(); }
}
// 3. GlobalObjectPool池满时回收 LRU 而非返回 null
if (maxCount > 0 && _alive[key].Count >= maxCount)
{
var oldest = _alive[key].Last.Value;
_alive[key].RemoveLast();
oldest.gameObject.SetActive(false);
_pools[key].Enqueue(oldest);
}
```
### P1 — 强烈建议
```csharp
// 4. 消除服务双轨注册:
// 删除 GameManager.RegisterServices() 整个方法,
// 仅保留 GameServiceRegistrarExecutionOrder -2000作为服务注册入口。
// 5. WorldStateRegistryOnEnable 重置,防 Editor Play 状态残留
private void OnEnable()
{
_states.Clear();
}
// 6. Player 状态机转换白名单(最小化改动)
// 在 PlayerController.TransitionTo 中增加调试断言:
#if UNITY_EDITOR
if (_currentState != null && _debugValidateTransitions)
Debug.Assert(IsValidTransition(_currentState.GetType(), targetType),
$"[PlayerController] 非预期转换: {_currentState.GetType().Name} → {targetType.Name}");
#endif
```
### P2 — 建议改进
```csharp
// 7. AttackState 并行数组 → 嵌套 struct
[System.Serializable]
public struct AttackTimings
{
[Range(0f, 1f)] public float HitBoxEnter;
[Range(0f, 1f)] public float HitBoxExit;
}
// PlayerAnimationConfigSO 改为:
public AttackTimings[] GroundAttackTimings;
// 8. EventBusMonitor用整数计数器替代 GetInvocationList
// 在 BaseEventChannelSO 中:
#if UNITY_EDITOR
private int _subscriberCount;
public int SubscriberCount => _subscriberCount;
#endif
// Subscribe 时 _subscriberCount++,取消时 --O(1),无 GC
// 9. OnDrawGizmosHurtBox 示例)
#if UNITY_EDITOR
private void OnDrawGizmos()
{
var col = GetComponent<Collider2D>();
if (col == null) return;
Gizmos.color = _isActive ? new Color(1, 0, 0, 0.5f) : new Color(1, 0, 0, 0.1f);
Gizmos.DrawWireCube(col.bounds.center, col.bounds.size);
}
#endif
```
### 架构长期优化
```
10. 引入测试框架
- 新增 asmdefBaseGames.TestsUNITY_INCLUDE_TESTS 平台限定)
- 优先覆盖SaveMigrator、DamageInfo.From、WorldStateRegistry、SkillManager 冷却
- 使用 ServiceLocator.OverrideForTest 替换真实服务
11. QuestManager 接口化
- 提取 IQuestManager 接口
- 在 GameServiceRegistrar 中注册
- 删除 QuestManager.Instance保留 private 单例防多次注册)
12. EditorWindow 可视化工具
- EventBusMonitor → 运行时 Event Flow 实时面板
- WorldStateRegistry → 状态浏览器(按类别分组显示已标记 ID
- GlobalObjectPool → 各池容量/活跃数实时显示
- 以上均可在 23 天内实现,对调试效率提升极大
```
---
## 附录:评分汇总
| 维度 | 评分 | 权重 | 加权分 |
|------|------|------|--------|
| 架构设计 | 8.7 | 25% | 2.18 |
| 性能 | 8.5 | 20% | 1.70 |
| 可扩展性 | 9.0 | 20% | 1.80 |
| 编辑器友好性 | 8.0 | 15% | 1.20 |
| 使用便利性 | 8.2 | 10% | 0.82 |
| 安全性/健壮性 | 6.5 | 10% | 0.65 |
| **综合** | | | **8.35 → 8.2** |
> **安全性/健壮性**是唯一拉低整体评分的维度,核心问题是 P0 的存档线程安全和文件原子写入。修复全部 P0+P1 问题后,预期评分可达 **8.8 / 10**,达到优质商业独立游戏代码标准。