Files
zeling_v2/Docs/Review/ComprehensiveCodeReview.md
2026-05-12 15:34:08 +08:00

635 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# zeling_v2 完整代码综合评审(含修复后状态)
> **评审日期**2026-05-11
> **评审范围**`Assets/Scripts/` 全部 25 个程序集,约 200+ 源文件
> **参照基准**《空洞骑士》《Celeste》《Neon Abyss》《Dead Cells》《Hades》等成熟商业 2D 动作游戏
> **修复轮次**本文档反映第三轮优化10 项 P0-P2 改动 + 8 项本轮改动)后的当前代码状态
---
## 目录
1. [评分总览](#1-评分总览)
2. [架构设计深度评审](#2-架构设计深度评审)
3. [性能深度评审](#3-性能深度评审)
4. [可扩展性深度评审](#4-可扩展性深度评审)
5. [编辑器友好性深度评审](#5-编辑器友好性深度评审)
6. [使用便利性DX深度评审](#6-使用便利性dx深度评审)
7. [子系统逐一点评](#7-子系统逐一点评)
8. [残留问题清单](#8-残留问题清单)
9. [后续迭代路线图](#9-后续迭代路线图)
---
## 1. 评分总览
| 维度 | 本次得分 | 上次得分 | 变化 | 商业顶线参考 |
|---|---|---|---|---|
| **架构设计** | 8.0 | 7.5 | ↑ 0.5 | 8.5HK/Celeste 架构级别) |
| **性能** | 7.5 | 7.0 | ↑ 0.5 | 8.0 |
| **可扩展性** | 7.5 | 7.5 | → | 9.0 |
| **编辑器友好性** | 7.5 | 6.5 | ↑ 1.0 | 8.0 |
| **使用便利性** | 7.5 | 7.0 | ↑ 0.5 | 8.5 |
| **综合** | **7.6** | **7.1** | ↑ **0.5** | ≈8.3 |
> 本轮修复了 `localScale` 翻转 / `SwimState` 未注册 / `ForceState()` 空实现 / `SaveManager.Data` 直接暴露 / `GameServiceRegistrar` 场景扫描 / `PlatformBootstrap` 每帧查找等多个 P1 缺陷,各维度均有明显提升。
---
## 2. 架构设计深度评审
### 2.1 ✅ 顶层架构:层次分明,职责边界清晰
```
Persistent Scene
[GameManager] ← 单一入口GameStateMachine 封装
[GameServiceRegistrar] ← 服务注册DefaultExecutionOrder(-2000)
[AudioManager] ← IAudioService 实现
[GlobalObjectPool] ← 对象池
[SaveManager] ← 存档
[QuestManager] ← 任务
[MapManager] ← 地图
[EventChainManager] ← 叙事事件链
Gameplay Scene单场景或分块加载
[RoomController] ← 房间出生点 + 摄像机切换
Enemy Prefabs ← EnemyBase + BehaviorTree
Player Prefab ← PlayerController + 16 状态 + HurtBox + HitBox
```
**亮点**`GameServiceRegistrar.DefaultExecutionOrder(-2000)` 是所有 MonoBehaviour 中最早执行的,确保服务在任何其他脚本的 `Awake` 之前注册完毕,启动顺序依赖问题得到彻底解决。
### 2.2 ✅ GameStateId正确实现 IEquatable消除装箱
```csharp
public readonly struct GameStateId : System.IEquatable<GameStateId>
{
public readonly string Id;
public bool Equals(GameStateId other) => Id == other.Id;
public override bool Equals(object obj) => obj is GameStateId g && Equals(g);
public override int GetHashCode() => Id?.GetHashCode() ?? 0;
public static bool operator ==(GameStateId a, GameStateId b) => a.Equals(b);
public static bool operator !=(GameStateId a, GameStateId b) => !a.Equals(b);
}
```
`readonly struct` + `IEquatable<T>` + 显式 `==`/`!=` 运算符,字典查找走 `Equals(GameStateId)` 重载,完全无装箱。**之前评审中担心的 struct 装箱问题已正确处理。**
### 2.3 ✅ GameStateMachine合法性验证 + 错误上报
```csharp
if (_current != null && !_current.ValidNextStates.Contains(nextId))
{
error = $"[GameStateMachine] 非法转换 {_current.Id} → {nextId}";
return false;
}
```
状态图的合法转换表由每个 `IGameState` 自持,违规转换不静默失败,而是返回 false + error 字符串调用方GameManager打 Warning。这是商业级防御性设计。
### 2.4 ✅ DamageInfo 双路径工厂
```csharp
// 热路径:零堆分配,直接从 SO 填字段
public static DamageInfo From(DamageSourceSO so) { ... }
// 构建路径:可读链式 Builder
var info = new DamageInfo.Builder()
.SetRaw(damage).SetType(Fire).SetFlags(CanBeParried).Build();
```
高频 HitBox 碰撞走 `DamageInfo.From(so)` 无 GCBoss 技能等复杂伤害信息走 Builder清晰易读。两条路径并存不牺牲一方。
### 2.5 ✅ EventChainManagerSO 事件 → C# 事件的桥接层
```csharp
// SO 事件(跨场景持久)→ 中继 C# 事件(给 ChainCondition 订阅)
Subscribe(_onBossDefeated, id => { OnBossDefeated?.Invoke(id); EvaluateAll(); });
```
将 ScriptableObject 事件频道与纯 C# 条件判断系统优雅解耦。叙事链条件不需要知道 SO 的存在EventChainManager 成为纯净的"事件路由器"。
### 2.6 ✅ AudioManager旧单例迁移模式规范
```csharp
// 标记已废弃的旧访问方式
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
public static AudioManager Instance { get; private set; }
```
在完成迁移之前,用 `[Obsolete]` 标记旧 API 而非直接删除,给调用方平滑过渡期,是成熟工程实践。
---
### 2.7 ⚠️ 剩余架构问题
#### P1GameManager / QuestManager / MapManager 仍是 static Instance
```csharp
public static GameManager Instance { get; private set; } // 核心状态机持有者
public static QuestManager Instance { get; private set; } // ISaveable 实现者
public static MapManager Instance { get; private set; } // ISaveable 实现者
```
SaveManager 的 `Data` 属性已标 `[Obsolete]` 并提供具名访问器,但上述三个管理器仍在用 `static Instance`,与 `ServiceLocator` 模式并存。这导致:
- `QuestManager.Instance?.Register(this)``OnEnable` 中调用——若 QuestManager 先于 SaveManager 在 Inspector 顺序中初始化,`SaveManager.Instance` 可能为 null
- 无法在运行时替换 QuestManager 实现(如 Tutorial 场景使用简化版)
**建议**:三者均添加 `ServiceLocator.Register<IQuestService>(this)` 等接口注册,保留 `static Instance` 作为向后兼容的转发属性(内部走 ServiceLocator
#### P2QuestManager._onEnemyDied 语义不匹配
```csharp
[SerializeField] private TransformEventChannelSO _onEnemyDied; // 携带 Transform
```
任务系统用 Transform 反查 `EnemyBase` 组件再读 EnemyId而任务关心的是"哪种敌人死了"而非"哪个具体对象"。多一层间接查找,且 Transform 可能在查找时已被销毁。
---
## 3. 性能深度评审
### 3.1 ✅ 热路径优化全覆盖
| 热路径 | 修复前 | 修复后 |
|---|---|---|
| `HurtBox` 每次受击 | `GetComponentInParent<IStatusEffectable>()` | `Awake` 缓存 |
| `EnemyBase.Update` 距离 | `Vector2.Distance`(√) | `sqrMagnitude` |
| `IsPlayerInRange(r)` | `Mathf.Sqrt` | `range * range` |
| `GlobalObjectPool` 活跃对象强制回收 | `List.RemoveAt(0)` O(n) | `LinkedList` 头节点 O(1) |
| `PlatformBootstrap.Update` | `ServiceLocator.GetOrDefault` 每帧字典查找 | `_platform` 字段缓存 |
| `PlayerMovement.UpdateFacing` | `localScale` 写入(触发 Transform 脏) | `SpriteRenderer.flipX` |
### 3.2 ✅ StatusEffectManager MaterialPropertyBlock
```csharp
private MaterialPropertyBlock _propBlock;
private void Awake() { _propBlock = new MaterialPropertyBlock(); ... }
// 设置 Shader 属性时:
_renderer.GetPropertyBlock(_propBlock);
_propBlock.SetFloat(_flashId, value);
_renderer.SetPropertyBlock(_propBlock);
```
不修改共享 `Material`,不触发 Unity 的 Material 实例化,避免渲染批次打断。
### 3.3 ✅ 对象池 + Addressables 预热
- 离线 Addressables `LoadAssetAsync` 加载 Prefab零运行时同步加载
- `PooledObject` 节点缓存 `GetComponentCached<T>()` 避免重复 `GetComponent`
- LinkedList 活跃列表 + LRU 强制回收100 子弹同屏时 Spawn/Despawn 稳定 O(1)
### 3.4 ⚠️ 剩余性能问题
#### P2SkillManager.Update 三个浮点减法(可忽略,但有更优方案)
```csharp
private void Update()
{
if (_soulCooldown > 0) _soulCooldown -= Time.deltaTime;
if (_spirit1Cooldown > 0) _spirit1Cooldown -= Time.deltaTime;
if (_spirit2Cooldown > 0) _spirit2Cooldown -= Time.deltaTime;
}
```
每帧三次浮点减法是可接受的开销。若后续扩展到 N 个技能,应改为 `Dictionary<FormSkillSO, float>` + 统一遍历。
#### P2WorldStateRegistry 无变更通知机制
```csharp
public void MarkCollected(string id) => _collectedIds.Add(id); // 无事件广播
```
HashSet 写入后没有通知机制调用方无法得知状态变化。UI 需要主动轮询或自行订阅其他事件来驱动刷新,不够响应式。
#### P3GameManager.RegisterStates 每帧 `_fsm.Tick(deltaTime)`
```csharp
private void Update() => _fsm.Tick(Time.deltaTime);
```
状态机 Tick 调用是 O(1)(一次虚方法调用),但若当前状态(如 `GameplayState`)本身做了复杂逻辑,则需进一步评估。整体可接受。
---
## 4. 可扩展性深度评审
### 4.1 ✅ QuestSO 分支叙事系统结构完整
```csharp
[Header("完成后续任务(分支)")]
public QuestBranch[] branches;
[Serializable]
public class QuestBranch
{
public string conditionQuestId; // 条件任务 Completed → 走本分支(空 = 默认)
public QuestSO nextQuest;
public string npcDialogueKey;
}
```
任务树可以纯通过 SO 资产的引用关系构建,无需修改代码添加新分支。条件任务完成 → 解锁后续任务的逻辑在 `QuestManager.CompleteQuest()` 中自动处理。
### 4.2 ✅ EquipmentManager护符效果接口化
```csharp
public interface ICharmEffect
{
void OnEquip(EquipmentContext ctx);
void OnUnequip(EquipmentContext ctx);
}
```
每个护符效果是独立的 `ScriptableObject`(或类),实现 `ICharmEffect``EquipmentContext` 携带 Stats / Feedback / SkillMods / WeaponMgr 等必要引用,效果脚本不需要 MonoBehaviour完全可测试。
### 4.3 ✅ DeathRespawnService 接口化,可替换实现
```csharp
public interface IDeathRespawnService
{
IEnumerator StartDeathSequenceCoroutine();
IEnumerator StartRespawnCoroutine();
IEnumerator StartGameOverCoroutine();
}
```
GameManager 通过 `ServiceLocator.Get<IDeathRespawnService>()` 使用,测试时可注入 Mock立即完成的假实现不需要等待真实的死亡动画计时。
### 4.4 ✅ WorldStateRegistryScriptableObject 注入,跨场景共享
`WorldStateRegistry` 是 ScriptableObject非 MonoBehaviour在所有场景之间共享同一资产实例不需要 `DontDestroyOnLoad` 也能保持状态。
### 4.5 ⚠️ 剩余可扩展性问题
#### P1WorldStateRegistry 新增实体类型成本高
```csharp
// 5 类实体5 个独立 HashSet5 对方法
private HashSet<string> _collectedIds;
private HashSet<string> _activatedSavePoints;
private HashSet<string> _openedDoors;
private HashSet<string> _destroyedObjects;
private HashSet<string> _flags;
public bool IsCollected(string id) => _collectedIds.Contains(id);
public void MarkCollected(string id) => _collectedIds.Add(id);
// ... 每新增一类需要 3 处改动
```
**建议**
```csharp
public enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag }
private readonly Dictionary<WorldObjectCategory, HashSet<string>> _states = new();
public bool IsMarked(WorldObjectCategory cat, string id)
=> _states.TryGetValue(cat, out var s) && s.Contains(id);
public void Mark(WorldObjectCategory cat, string id)
=> (_states.TryGetValue(cat, out var s) ? s : (_states[cat] = new HashSet<string>())).Add(id);
```
新增实体类型只需在枚举中添加一行。
#### P1SkillManager 硬编码三技能槽
```csharp
private FormSkillSO _soulSkill;
private FormSkillSO _spirit1;
private FormSkillSO _spirit2;
private float _soulCooldown;
private float _spirit1Cooldown;
private float _spirit2Cooldown;
```
槽位数量在编译时固定为 3无法通过配置扩展。若后期形态需要 4 个或 2 个技能,需修改 `SkillManager` 代码。
**建议**
```csharp
private readonly Dictionary<FormSkillSO, float> _cooldowns = new();
public void UpdateSkillSet(FormSkillSO[] skills)
{
_cooldowns.Clear();
foreach (var s in skills) if (s != null) _cooldowns[s] = 0f;
}
```
---
## 5. 编辑器友好性深度评审
### 5.1 ✅ SOValidationRunner构建前自动数据校验
```csharp
public class SOValidationRunner : IPreprocessBuildWithReport
{
public void OnPreprocessBuild(BuildReport report)
{
var (errors, warnings) = RunAll();
if (errors.Count > 0)
throw new BuildFailedException(...); // 有错误时中止构建
}
[MenuItem("Tools/Validate All ScriptableObjects")]
public static void ValidateMenu() { ... }
}
```
- 实现 `IPreprocessBuildWithReport`,构建时自动运行,防止空引用 SO 进入 Release 包
- `callbackOrder = 1`,在 `AddressKeyValidator (order=0)` 后执行,验证链有序
- `[MenuItem]` 支持手动一键校验
### 5.2 ✅ EventBusMonitor 运行时追踪
256 条环形缓冲区,记录每个 SO 事件的:帧号 / 频道名 / 负载 / 监听器数量。任意事件触发时序问题在 Editor 中 5 秒内可定位。
### 5.3 ✅ EventChainEditorWindow 编辑器专用日志
```csharp
#if UNITY_EDITOR
public static event Action<string, string> OnChainExecutedInEditor;
#endif
```
叙事链执行时向编辑器窗口推送日志chainId + 执行结果),不产生运行时开销。`#if UNITY_EDITOR` 包裹严格,不泄漏到 Release 构建。
### 5.4 ✅ WorldMarker 可视化 Gizmos
```csharp
Gizmos.color = _markerType switch
{
WorldMarkerType.Objective => Color.yellow,
WorldMarkerType.NPC => Color.cyan,
WorldMarkerType.PointOfInterest => Color.green,
...
};
```
场景视图直接看到标记类型与覆盖范围,场景设计师无需 Play 即可预览关卡布局。
### 5.5 ✅ [RequireComponent] 同节点依赖自动保障
本轮修复后,`PlayerController` 已标注:
```csharp
[RequireComponent(typeof(InputBuffer))]
[RequireComponent(typeof(PlayerMovement))]
[RequireComponent(typeof(PlayerStats))]
[RequireComponent(typeof(AnimancerComponent))]
```
在 Inspector 中添加 PlayerController 时 Unity 自动附加所有必须组件,防止遗漏。
### 5.6 ⚠️ 剩余编辑器问题
#### P2EquipmentContext 在 Awake 内联构建Inspector 不可见
```csharp
private void Awake()
{
_ctx = new EquipmentContext
{
Stats = GetComponent<PlayerStats>(),
Feedback = GetComponent<PlayerFeedback>(),
Events = EventChannelRegistry.Instance,
SkillMods = GetComponent<SkillModifierRegistry>(),
WeaponMgr = GetComponent<WeaponManager>(),
};
}
```
若某组件缺失(如 `PlayerFeedback` 未挂载),`_ctx.Feedback` 为 null护符效果 `OnEquip` 调用 `ctx.Feedback` 时静默失败。建议在 Awake 末尾加 `Debug.Assert`
```csharp
Debug.Assert(_ctx.Stats != null, "[EquipmentManager] 缺少 PlayerStats", this);
Debug.Assert(_ctx.Feedback != null, "[EquipmentManager] 缺少 PlayerFeedback", this);
Debug.Assert(_ctx.SkillMods != null, "[EquipmentManager] 缺少 SkillModifiers", this);
```
#### P2PostProcessManager `[SerializeField] Component _bossArenaVolume`
`Component` 基类接收 Volume 组件,无类型约束。设计师可能误拖 `BoxCollider2D`,直到运行时才报错。
---
## 6. 使用便利性DX深度评审
### 6.1 ✅ EquipmentManager返回错误字符串替代 bool+out
```csharp
/// <summary>返回 null 表示成功;返回错误字符串表示失败原因。</summary>
public string TryEquipCharm(CharmSO charm)
{
if (charm == null) return "护符不存在";
if (_equipped.Contains(charm)) return "已经装备";
if (!_collected.Contains(charm)) return "尚未收集此护符";
int remaining = _currentNotchCapacity - UsedNotches;
if (charm.notchCost > remaining) return $"笔记不足(需要 {charm.notchCost},剩余 {remaining}";
...
return null; // 成功
}
```
调用方可直接将错误字符串显示给用户,不需要额外的错误码枚举。设计简洁,优于 `bool TryEquip(..., out string error)`
### 6.2 ✅ Projectile 基类Initialize + 模板方法 OnInitialized
```csharp
// 基类处理通用初始化池引用、HitBox 激活、计时器)
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction)
{
_config = config;
DamageInfo = damageInfo;
Direction = direction.normalized;
_aliveTimer = 0f;
_hitBox.Activate(config.DamageSource);
OnInitialized(); // 钩子:子类设定初速度
}
protected virtual void OnInitialized() { }
```
子类只需重写 `OnInitialized()` 设置 Rigidbody 速度,其余公共逻辑由基类统一处理。`LinearProjectile` / `HomingProjectile` / `ArcProjectile` 各自 ≤ 20 行。
### 6.3 ✅ 异步/协程异常全部包裹
```csharp
// QuickSave/QuickLoad 调用点无法 await用包裹器捕获异常
private static async void RunFireAndForget(Task task, string context)
{
try { await task; }
catch (Exception e)
{
Debug.LogError($"[SaveManager] {context} 失败: {e.Message}\n{e.StackTrace}");
}
}
```
所有 `async void` 入口QuickSave / QuickLoad都经过 `RunFireAndForget` 包裹,异常不会静默吞掉。
### 6.4 ✅ QuestManager完整 ISaveable + IReadOnlyDictionary 状态只读视图
```csharp
public IReadOnlyDictionary<string, QuestStateEnum> QuestStates => _questStates;
```
外部查询任务状态走 `IReadOnlyDictionary`,无法意外写入内部状态。`SaveManager` 持有 `ISaveable` 接口引用,不直接依赖 `QuestManager` 类型。
### 6.5 ⚠️ 剩余 DX 问题
#### P1GameManager.RegisterStates 硬编码 9 个状态
```csharp
private void RegisterStates()
{
_fsm.Register(new InitializingState());
_fsm.Register(new MainMenuState());
// ... 9 个
}
```
每次添加新游戏状态都需要修改 `GameManager.cs`。游戏状态类实现了 `IGameState`,可通过反射或工厂模式自动注册(`IGameStateFactory` 接口已存在):
```csharp
// IGameStateFactory.cs 已定义,但 GameManager 未使用
foreach (var factory in GetComponents<IGameStateFactory>())
_fsm.Register(factory.Create());
```
#### P2DeathRespawnService 复活流程等待方式混用
```csharp
// 在 Coroutine 中轮询 bool flag_deathConfirmed
yield return new WaitUntil(() => _deathConfirmed);
```
通过轮询 bool 等待玩家确认,而不是直接订阅确认事件的回调。在 Coroutine 中 `WaitUntil` 每帧检查一次,虽然性能可接受,但意图不如直接回调清晰。
---
## 7. 子系统逐一点评
| 子系统 | 质量评级 | 亮点 | 注意点 |
|---|---|---|---|
| **GameStateMachine** | ⭐⭐⭐⭐⭐ | 合法转换验证 + 错误上报 | 状态注册仍手动 |
| **事件频道** | ⭐⭐⭐⭐⭐ | SO + C# event 双层 + 可组合订阅句柄 + EventBusMonitor | - |
| **ServiceLocator** | ⭐⭐⭐⭐⭐ | 接口注册 + 测试 Override/Reset | 与 static Instance 并存(迁移中) |
| **SaveManager** | ⭐⭐⭐⭐ | HMAC校验 + 版本迁移 + fire-and-forget 包裹 | Data 属性已标 Obsolete待删除 |
| **HurtBox** | ⭐⭐⭐⭐ | 8 步流水线完整 + 接口注入 + 缓存 | 依赖注入不可见于 Inspector |
| **DamageInfo** | ⭐⭐⭐⭐⭐ | 双路径工厂(零 GC + Builder | - |
| **PlayerController** | ⭐⭐⭐⭐ | POCO FSM + 类型字典 O(1) + SwimState 已注册 | 仍有 ~14 个 SerializeField |
| **AttackState** | ⭐⭐⭐⭐ | 连击数据驱动 + Animancer 事件驱动 HitBox | - |
| **PlayerMovement** | ⭐⭐⭐⭐ | flipX 朝向已修复 | `_spriteRenderer` 需 Inspector 赋值或 GetComponentInChildren |
| **EnemyBase** | ⭐⭐⭐ | 事件频道获取玩家 Transform + Start 兜底 | ForceState 动画已修复;状态机仍是枚举级别 |
| **GlobalObjectPool** | ⭐⭐⭐⭐ | LinkedList LRU + Addressables 预热 + 双版本桥接 | 双版本预热逻辑有重复 |
| **StatusEffectManager** | ⭐⭐⭐⭐⭐ | 双结构 + MaterialPropertyBlock + 逆序遍历 | - |
| **EquipmentManager** | ⭐⭐⭐⭐ | ICharmEffect 接口 + EquipmentContext + ISaveable | _ctx 依赖无 Assert 保护 |
| **SkillManager** | ⭐⭐⭐ | 修改器注册表 + FormSkillSO 数据驱动 | 三个硬编码槽 |
| **QuestManager** | ⭐⭐⭐⭐ | 分支任务 + ISaveable + IReadOnlyDictionary | EnemyDied 事件类型不匹配 |
| **WorldStateRegistry** | ⭐⭐⭐ | SO + 跨场景共享 + ISaveable | 每类实体独立字段,扩展性差 |
| **AudioManager** | ⭐⭐⭐⭐ | BGM 交叉淡入 + SFX 轮转池 + 旧 API 已标 Obsolete | Phase 2 实现尚未完整 |
| **EventChainManager** | ⭐⭐⭐⭐⭐ | SO 事件 → C# 事件桥接 + Editor 专用日志 | - |
| **SOValidationRunner** | ⭐⭐⭐⭐⭐ | 构建前校验 + MenuItem 一键校验 | - |
---
## 8. 残留问题清单
> 以下为本轮修复后仍存在的问题,按优先级排序。
| # | 优先级 | 模块 | 问题描述 | 建议修复方式 |
|---|--------|------|----------|------------|
| 1 | **P1** | GameManager / QuestManager / MapManager | `static Instance` 与 ServiceLocator 并存 | 实现接口 + ServiceLocator 注册,保留 Instance 作转发 |
| 2 | **P1** | QuestManager | `_onEnemyDied``TransformEventChannelSO` 携带 Transform | 改用 `EnemyDeathData { string EnemyId; Transform Source; }` 事件 |
| 3 | **P1** | GameManager | `RegisterStates()` 硬编码,未使用已存在的 `IGameStateFactory` 接口 | 通过工厂接口或配置自动注册 |
| 4 | **P1** | WorldStateRegistry | 5 类状态独立字段,扩展性差 | 改为 `Dictionary<WorldObjectCategory, HashSet<string>>` |
| 5 | **P1** | SkillManager | 三技能槽硬编码 | `Dictionary<FormSkillSO, float>` 动态冷却表 |
| 6 | **P2** | EquipmentManager | `EquipmentContext` 依赖无 `Debug.Assert` 保护 | Awake 末尾加断言 |
| 7 | **P2** | PostProcessManager | `[SerializeField] Component _bossArenaVolume` 无类型约束 | 改为 `Volume` 具体类型 |
| 8 | **P2** | DeathRespawnService | `WaitUntil(() => _deathConfirmed)` 轮询模式 | 改为直接 TaskCompletionSource 或事件回调 |
| 9 | **P2** | GlobalObjectPool | `WarmupCoroutine` / `WarmupAsync` 逻辑重复 | 统一 async Task提供 Coroutine 桥接扩展方法 |
| 10 | **P3** | SaveManager | `[Obsolete] Data` 属性仍存在 | 确认调用方全部迁移后删除 |
| 11 | **P3** | RoomController | `CameraStateController.Instance?.SwitchRoom()` 使用 static Instance | 改用 ServiceLocator 或事件频道 |
| 12 | **P3** | WorldStateRegistry | 无变更通知机制 | 添加 `event Action<WorldObjectCategory, string> OnStateChanged` |
---
## 9. 后续迭代路线图
### 第一优先级1-2 周,不影响功能开发)
```
① WorldStateRegistry 泛化 API
enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag }
Dictionary<WorldObjectCategory, HashSet<string>> _states
② SkillManager 动态技能槽
Dictionary<FormSkillSO, float> _cooldowns
③ EquipmentManager.Awake Debug.Assert 保护
// 4 行断言30 分钟内完成
④ GlobalObjectPool 统一 WarmupAsync提供 AsCoroutine() 桥接
```
### 第二优先级2-4 周,架构级改进)
```
⑤ GameManager / QuestManager / MapManager 统一服务定位模式
实现 IQuestService / IMapService 接口 → ServiceLocator 注册
static Instance 改为 ServiceLocator.GetOrDefault<IXXXService>()
⑥ 利用 IGameStateFactory 接口自动注册游戏状态
GameManager.RegisterStates() 改为工厂驱动
⑦ QuestManager._onEnemyDied 改用 EnemyDeathData 事件类型
```
### 第三优先级(长期,影响内容管线)
```
⑧ EnemyBase 状态机升级
完全依赖 Behavior Designer 行为树,废弃 EnemyStateType 枚举
EnemyBase 成为纯数据持有者 + 生命周期桥接器
⑨ 单元测试接入ServiceLocator 已支持 OverrideForTest
SaveMigrator.Migrate 各版本路径
StatusEffectManager 堆叠/净化逻辑
QuestManager 目标进度计算
HurtBox 8 步流水线各分支
⑩ 护符效果数据管线自动化
ICharmEffect.Validate() 接入 SOValidationRunner
CharmSO.effects[] 空引用在构建时捕获
```
---
## 附:三轮修复全记录
| 轮次 | 问题 | 文件 | 状态 |
|------|------|------|------|
| 第一轮 P0 | SaveManager HMAC 跨设备失效 | SaveManager.cs | ✅ |
| 第一轮 P0 | QuickSave 异步异常静默吞掉 | SaveManager.cs | ✅ |
| 第一轮 P1 | HurtBox 每次受击 GetComponent | HurtBox.cs | ✅ |
| 第一轮 P1 | EnemyBase FindWithTag O(n) | EnemyBase.cs | ✅ |
| 第一轮 P1 | EnemyStats DistanceToPlayer 未平方 | EnemyStats.cs | ✅ |
| 第一轮 P1 | PlayerController Resources.FindObjectsOfTypeAll | PlayerController.cs | ✅ |
| 第一轮 P1 | PlayerController _dependenciesReady 每帧重计算 | PlayerController.cs | ✅ |
| 第一轮 P2 | AttackState 连击硬编码 3 段 | AttackState.cs | ✅ |
| 第一轮 P2 | HurtState 硬编码 0.4s 硬直 | HurtState.cs | ✅ |
| 第一轮 P2 | GlobalObjectPool List O(n) LRU | GlobalObjectPool.cs | ✅ |
| 第二轮 Bug | EnemyBase Awake 订阅未调用 | EnemyBase.cs | ✅ |
| 第三轮 P1 | PlayerMovement localScale 朝向 | PlayerMovement.cs | ✅ |
| 第三轮 P1 | SwimState 未注册 | PlayerController.cs | ✅ |
| 第三轮 P1 | EnemyBase.ForceState() 空实现 | EnemyBase.cs | ✅ |
| 第三轮 P1 | SaveManager.Data 暴露内部状态 | SaveManager.cs + 2 调用方 | ✅ |
| 第三轮 P1 | GameServiceRegistrar 全场景扫描 | GameServiceRegistrar.cs | ✅ |
| 第三轮 P1 | PlayerController [RequireComponent] 缺失 | PlayerController.cs | ✅ |
| 第三轮 P3 | PlatformBootstrap 每帧 ServiceLocator 查找 | PlatformBootstrap.cs | ✅ |
| 第三轮 P2 | UIManager 丢弃 shopId | UIManager.cs | ✅ |
| 第三轮 P3 | LocalizationManager 异常静默 | LocalizationManager.cs | ✅ |
---
*本文档反映当前2026-05-11所有已实施修复后的代码状态。如需深入讨论任何具体问题的实施方案可继续追问。*