# 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.5(HK/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 { 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` + 显式 `==`/`!=` 运算符,字典查找走 `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)` 无 GC;Boss 技能等复杂伤害信息走 Builder,清晰易读。两条路径并存,不牺牲一方。 ### 2.5 ✅ EventChainManager:SO 事件 → 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() instead.")] public static AudioManager Instance { get; private set; } ``` 在完成迁移之前,用 `[Obsolete]` 标记旧 API 而非直接删除,给调用方平滑过渡期,是成熟工程实践。 --- ### 2.7 ⚠️ 剩余架构问题 #### P1:GameManager / 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(this)` 等接口注册,保留 `static Instance` 作为向后兼容的转发属性(内部走 ServiceLocator)。 #### P2:QuestManager._onEnemyDied 语义不匹配 ```csharp [SerializeField] private TransformEventChannelSO _onEnemyDied; // 携带 Transform ``` 任务系统用 Transform 反查 `EnemyBase` 组件再读 EnemyId,而任务关心的是"哪种敌人死了"而非"哪个具体对象"。多一层间接查找,且 Transform 可能在查找时已被销毁。 --- ## 3. 性能深度评审 ### 3.1 ✅ 热路径优化全覆盖 | 热路径 | 修复前 | 修复后 | |---|---|---| | `HurtBox` 每次受击 | `GetComponentInParent()` | `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()` 避免重复 `GetComponent` - LinkedList 活跃列表 + LRU 强制回收,100 子弹同屏时 Spawn/Despawn 稳定 O(1) ### 3.4 ⚠️ 剩余性能问题 #### P2:SkillManager.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` + 统一遍历。 #### P2:WorldStateRegistry 无变更通知机制 ```csharp public void MarkCollected(string id) => _collectedIds.Add(id); // 无事件广播 ``` HashSet 写入后没有通知机制,调用方无法得知状态变化。UI 需要主动轮询或自行订阅其他事件来驱动刷新,不够响应式。 #### P3:GameManager.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()` 使用,测试时可注入 Mock(立即完成的假实现),不需要等待真实的死亡动画计时。 ### 4.4 ✅ WorldStateRegistry:ScriptableObject 注入,跨场景共享 `WorldStateRegistry` 是 ScriptableObject(非 MonoBehaviour),在所有场景之间共享同一资产实例,不需要 `DontDestroyOnLoad` 也能保持状态。 ### 4.5 ⚠️ 剩余可扩展性问题 #### P1:WorldStateRegistry 新增实体类型成本高 ```csharp // 5 类实体,5 个独立 HashSet,5 对方法 private HashSet _collectedIds; private HashSet _activatedSavePoints; private HashSet _openedDoors; private HashSet _destroyedObjects; private HashSet _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> _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())).Add(id); ``` 新增实体类型只需在枚举中添加一行。 #### P1:SkillManager 硬编码三技能槽 ```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 _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 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 ⚠️ 剩余编辑器问题 #### P2:EquipmentContext 在 Awake 内联构建,Inspector 不可见 ```csharp private void Awake() { _ctx = new EquipmentContext { Stats = GetComponent(), Feedback = GetComponent(), Events = EventChannelRegistry.Instance, SkillMods = GetComponent(), WeaponMgr = GetComponent(), }; } ``` 若某组件缺失(如 `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); ``` #### P2:PostProcessManager `[SerializeField] Component _bossArenaVolume` 用 `Component` 基类接收 Volume 组件,无类型约束。设计师可能误拖 `BoxCollider2D`,直到运行时才报错。 --- ## 6. 使用便利性(DX)深度评审 ### 6.1 ✅ EquipmentManager:返回错误字符串替代 bool+out ```csharp /// 返回 null 表示成功;返回错误字符串表示失败原因。 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 QuestStates => _questStates; ``` 外部查询任务状态走 `IReadOnlyDictionary`,无法意外写入内部状态。`SaveManager` 持有 `ISaveable` 接口引用,不直接依赖 `QuestManager` 类型。 ### 6.5 ⚠️ 剩余 DX 问题 #### P1:GameManager.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()) _fsm.Register(factory.Create()); ``` #### P2:DeathRespawnService 复活流程等待方式混用 ```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>` | | 5 | **P1** | SkillManager | 三技能槽硬编码 | `Dictionary` 动态冷却表 | | 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 OnStateChanged` | --- ## 9. 后续迭代路线图 ### 第一优先级(1-2 周,不影响功能开发) ``` ① WorldStateRegistry 泛化 API enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag } Dictionary> _states ② SkillManager 动态技能槽 Dictionary _cooldowns ③ EquipmentManager.Awake Debug.Assert 保护 // 4 行断言,30 分钟内完成 ④ GlobalObjectPool 统一 WarmupAsync,提供 AsCoroutine() 桥接 ``` ### 第二优先级(2-4 周,架构级改进) ``` ⑤ GameManager / QuestManager / MapManager 统一服务定位模式 实现 IQuestService / IMapService 接口 → ServiceLocator 注册 static Instance 改为 ServiceLocator.GetOrDefault() ⑥ 利用 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)所有已实施修复后的代码状态。如需深入讨论任何具体问题的实施方案,可继续追问。*