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

26 KiB
Raw Blame History

zeling_v2 完整代码综合评审(含修复后状态)

评审日期2026-05-11
评审范围Assets/Scripts/ 全部 25 个程序集,约 200+ 源文件
参照基准《空洞骑士》《Celeste》《Neon Abyss》《Dead Cells》《Hades》等成熟商业 2D 动作游戏
修复轮次本文档反映第三轮优化10 项 P0-P2 改动 + 8 项本轮改动)后的当前代码状态


目录

  1. 评分总览
  2. 架构设计深度评审
  3. 性能深度评审
  4. 可扩展性深度评审
  5. 编辑器友好性深度评审
  6. 使用便利性DX深度评审
  7. 子系统逐一点评
  8. 残留问题清单
  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消除装箱

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合法性验证 + 错误上报

if (_current != null && !_current.ValidNextStates.Contains(nextId))
{
    error = $"[GameStateMachine] 非法转换 {_current.Id} → {nextId}";
    return false;
}

状态图的合法转换表由每个 IGameState 自持,违规转换不静默失败,而是返回 false + error 字符串调用方GameManager打 Warning。这是商业级防御性设计。

2.4 DamageInfo 双路径工厂

// 热路径:零堆分配,直接从 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# 事件的桥接层

// SO 事件(跨场景持久)→ 中继 C# 事件(给 ChainCondition 订阅)
Subscribe(_onBossDefeated, id => { OnBossDefeated?.Invoke(id); EvaluateAll(); });

将 ScriptableObject 事件频道与纯 C# 条件判断系统优雅解耦。叙事链条件不需要知道 SO 的存在EventChainManager 成为纯净的"事件路由器"。

2.6 AudioManager旧单例迁移模式规范

// 标记已废弃的旧访问方式
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
public static AudioManager Instance { get; private set; }

在完成迁移之前,用 [Obsolete] 标记旧 API 而非直接删除,给调用方平滑过渡期,是成熟工程实践。


2.7 ⚠️ 剩余架构问题

P1GameManager / QuestManager / MapManager 仍是 static Instance

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 语义不匹配

[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

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 三个浮点减法(可忽略,但有更优方案)

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 无变更通知机制

public void MarkCollected(string id) => _collectedIds.Add(id);   // 无事件广播

HashSet 写入后没有通知机制调用方无法得知状态变化。UI 需要主动轮询或自行订阅其他事件来驱动刷新,不够响应式。

P3GameManager.RegisterStates 每帧 _fsm.Tick(deltaTime)

private void Update() => _fsm.Tick(Time.deltaTime);

状态机 Tick 调用是 O(1)(一次虚方法调用),但若当前状态(如 GameplayState)本身做了复杂逻辑,则需进一步评估。整体可接受。


4. 可扩展性深度评审

4.1 QuestSO 分支叙事系统结构完整

[Header("完成后续任务(分支)")]
public QuestBranch[] branches;

[Serializable]
public class QuestBranch
{
    public string  conditionQuestId;   // 条件任务 Completed → 走本分支(空 = 默认)
    public QuestSO nextQuest;
    public string  npcDialogueKey;
}

任务树可以纯通过 SO 资产的引用关系构建,无需修改代码添加新分支。条件任务完成 → 解锁后续任务的逻辑在 QuestManager.CompleteQuest() 中自动处理。

4.2 EquipmentManager护符效果接口化

public interface ICharmEffect
{
    void OnEquip(EquipmentContext ctx);
    void OnUnequip(EquipmentContext ctx);
}

每个护符效果是独立的 ScriptableObject(或类),实现 ICharmEffectEquipmentContext 携带 Stats / Feedback / SkillMods / WeaponMgr 等必要引用,效果脚本不需要 MonoBehaviour完全可测试。

4.3 DeathRespawnService 接口化,可替换实现

public interface IDeathRespawnService
{
    IEnumerator StartDeathSequenceCoroutine();
    IEnumerator StartRespawnCoroutine();
    IEnumerator StartGameOverCoroutine();
}

GameManager 通过 ServiceLocator.Get<IDeathRespawnService>() 使用,测试时可注入 Mock立即完成的假实现不需要等待真实的死亡动画计时。

4.4 WorldStateRegistryScriptableObject 注入,跨场景共享

WorldStateRegistry 是 ScriptableObject非 MonoBehaviour在所有场景之间共享同一资产实例不需要 DontDestroyOnLoad 也能保持状态。

4.5 ⚠️ 剩余可扩展性问题

P1WorldStateRegistry 新增实体类型成本高

// 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 处改动

建议

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 硬编码三技能槽

private FormSkillSO _soulSkill;
private FormSkillSO _spirit1;
private FormSkillSO _spirit2;
private float _soulCooldown;
private float _spirit1Cooldown;
private float _spirit2Cooldown;

槽位数量在编译时固定为 3无法通过配置扩展。若后期形态需要 4 个或 2 个技能,需修改 SkillManager 代码。

建议

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构建前自动数据校验

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 编辑器专用日志

#if UNITY_EDITOR
public static event Action<string, string> OnChainExecutedInEditor;
#endif

叙事链执行时向编辑器窗口推送日志chainId + 执行结果),不产生运行时开销。#if UNITY_EDITOR 包裹严格,不泄漏到 Release 构建。

5.4 WorldMarker 可视化 Gizmos

Gizmos.color = _markerType switch
{
    WorldMarkerType.Objective       => Color.yellow,
    WorldMarkerType.NPC             => Color.cyan,
    WorldMarkerType.PointOfInterest => Color.green,
    ...
};

场景视图直接看到标记类型与覆盖范围,场景设计师无需 Play 即可预览关卡布局。

5.5 [RequireComponent] 同节点依赖自动保障

本轮修复后,PlayerController 已标注:

[RequireComponent(typeof(InputBuffer))]
[RequireComponent(typeof(PlayerMovement))]
[RequireComponent(typeof(PlayerStats))]
[RequireComponent(typeof(AnimancerComponent))]

在 Inspector 中添加 PlayerController 时 Unity 自动附加所有必须组件,防止遗漏。

5.6 ⚠️ 剩余编辑器问题

P2EquipmentContext 在 Awake 内联构建Inspector 不可见

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

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

/// <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

// 基类处理通用初始化池引用、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 异步/协程异常全部包裹

// 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 状态只读视图

public IReadOnlyDictionary<string, QuestStateEnum> QuestStates => _questStates;

外部查询任务状态走 IReadOnlyDictionary,无法意外写入内部状态。SaveManager 持有 ISaveable 接口引用,不直接依赖 QuestManager 类型。

6.5 ⚠️ 剩余 DX 问题

P1GameManager.RegisterStates 硬编码 9 个状态

private void RegisterStates()
{
    _fsm.Register(new InitializingState());
    _fsm.Register(new MainMenuState());
    // ... 9 个
}

每次添加新游戏状态都需要修改 GameManager.cs。游戏状态类实现了 IGameState,可通过反射或工厂模式自动注册(IGameStateFactory 接口已存在):

// IGameStateFactory.cs 已定义,但 GameManager 未使用
foreach (var factory in GetComponents<IGameStateFactory>())
    _fsm.Register(factory.Create());

P2DeathRespawnService 复活流程等待方式混用

// 在 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 _onEnemyDiedTransformEventChannelSO 携带 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所有已实施修复后的代码状态。如需深入讨论任何具体问题的实施方案可继续追问。