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

12 KiB
Raw Blame History

DeepDive_2026_Q4 — 代码深度评审 & 重构报告

日期2026-05-12
评审轮次Q4累计第四轮延续 Q1/Q2/Q3
核心主题:单例污染彻底清除 — 全项目静态 Instance 统一迁移至 ServiceLocator


一、本轮评审背景

Q1Q3 已累计 36 项修复,但代码库仍存在两类混用模式:

  1. 部分 Manager 类保留了 [System.Obsolete] 修饰的静态 Instance 字段,实质上既注册 ServiceLocator 又维护 Instance形成双轨并存
  2. 若干调用方仍通过 .Instance 访问,积累了 #pragma warning disable CS0618 噪音。

用户明确要求:作为全新项目,不保留任何废弃成员,直接删除,保持代码纯净。


二、评估维度与评分

维度 Q3 评分 Q4 评分 变化 说明
架构设计 8.0 8.8 +0.8 依赖注入全面统一,彻底消除双轨单例
性能 7.5 7.5 本轮无性能专项改动
可扩展性 8.5 9.0 +0.5 SO 与 ScriptableObject 也改用 ServiceLocator更易测试替换
编辑器友好 8.0 8.0 不涉及 Inspector 改动
使用便利性 7.8 8.5 +0.7 调用方代码一致性强,无需记忆哪些类有 Instance

综合评分8.56 / 10商业标准参考8.0+ 为生产就绪9.5+ 为顶尖)


三、发现问题全表

ID 分类 文件 问题描述 严重度 本轮状态
S-1 架构 AudioManager.cs [Obsolete] Instance 字段 + #pragma 噪音 已删除
S-2 架构 GameManager.cs [Obsolete] Instance 字段 + #pragma 噪音 已删除
S-3 架构 CameraStateController.cs [Obsolete] Instance + 多处 #pragma + 无用 OnDestroy 已删除
S-4 架构 VFXPool.cs [Obsolete] Instance + #pragma 噪音 已删除
S-5 架构 SaveManager.cs [Obsolete] Instance + #pragma 噪音 已删除
S-6 架构 GlobalObjectPool.cs static Instance(无 Obsolete与 ServiceLocator 双轨并存 已删除
S-7 架构 QuestManager.cs static Instance,与 ServiceLocator 双轨并存 已删除
S-8 架构 EventChannelRegistry.cs static Instance,未注册到 ServiceLocator 已删除+注册
S-9 架构 TutorialManager.cs static Instance + OnDestroy 清空,未注册 ServiceLocator 已删除+注册
S-10 架构 MapManager.cs static Instance,与 ServiceLocator 双轨并存 已删除
C-1 耦合 SaveableMonoBehaviour.cs SaveManager.Instance 直接访问(基类影响所有 ISaveable 子类) 已迁移
C-2 耦合 MapPin.cs / MapManager.cs SaveManager.Instance 直接访问 已迁移
C-3 耦合 DifficultyManager.cs SaveManager.Instance 直接访问 已迁移
C-4 耦合 DeathRespawnService.cs SaveManager.Instance 直接访问 已迁移
C-5 耦合 ChallengeRoomTrigger.cs SaveManager.Instance 直接访问 已迁移
C-6 耦合 ChallengeRoomManager.cs SaveManager.Instance x3 直接访问 已迁移
C-7 耦合 EventChainSO.cs (ScriptableObject) SaveManager.Instance 在 SO 中直接访问 已迁移
C-8 耦合 EventChainManager.cs SaveManager.Instance 直接访问 已迁移
C-9 耦合 ProgressLock.cs SaveManager.Instance 直接访问 已迁移
C-10 耦合 HPContainerPickup.cs SaveManager.Instance x3 直接访问 已迁移
C-11 耦合 RangedEnemy.cs GlobalObjectPool.Instance.Spawn(...) 无空检查,存在潜在 NRE 已迁移+空检查
C-12 耦合 AssetReleaseTracker.cs GlobalObjectPool.Instance x3 直接访问 已迁移
C-13 耦合 BD_SpawnProjectile.cs GlobalObjectPool.Instance 直接访问 已迁移
C-14 耦合 BD_SummonMinions.cs GlobalObjectPool.Instance 直接访问 已迁移
C-15 耦合 HitFXSpawner.cs VFXPool.Instance 直接访问 已迁移
C-16 耦合 QuestGiver.cs QuestManager.Instance x4 重复访问 已迁移(缓存局部变量)
C-17 耦合 ContextualHintTrigger.cs TutorialManager.Instance x2 直接访问 已迁移(缓存局部变量)
C-18 耦合 EquipmentManager.cs EventChannelRegistry.Instance 直接访问 已迁移

四、修复详情

S 系列:静态单例清除

修复策略

对每个 Manager 类,统一执行以下三步:

  1. 删除 静态 Instance 属性(含 [System.Obsolete]、注释)
  2. 删除 Awake 中对 Instance 的赋值和 #pragma warning disable/restore CS0618
  3. 改写 重复实例化防护为 ServiceLocator.GetOrDefault<T>() != null 检查
// ❌ 旧:双轨模式,含 Obsolete 噪音
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
public static AudioManager Instance { get; private set; }

private void Awake()
{
#pragma warning disable CS0618
    if (Instance != null) { Destroy(gameObject); return; }
    Instance = this;
#pragma warning restore CS0618
    ServiceLocator.Register<IAudioService>(this);
}

// ✅ 新ServiceLocator 唯一路径
private void Awake()
{
    if (ServiceLocator.GetOrDefault<IAudioService>() != null) { Destroy(gameObject); return; }
    ServiceLocator.Register<IAudioService>(this);
    Initialize();
}

EventChannelRegistry / TutorialManager 新增注册

这两个类原先只维护静态 Instance从未注册 ServiceLocator。本轮补齐

// EventChannelRegistry
private void Awake()
{
    if (BaseGames.Core.ServiceLocator.GetOrDefault<IEventChannelRegistry>() != null)
    { Destroy(gameObject); return; }
    BaseGames.Core.ServiceLocator.Register<IEventChannelRegistry>(this);
    DontDestroyOnLoad(transform.root.gameObject);
}

// TutorialManager
private void Awake()
{
    if (BaseGames.Core.ServiceLocator.GetOrDefault<TutorialManager>() != null)
    { Destroy(gameObject); return; }
    BaseGames.Core.ServiceLocator.Register<TutorialManager>(this);
    DontDestroyOnLoad(gameObject);
}

C 系列:调用方迁移

SaveManager 调用方C-1 ~ C-10

所有调用方统一替换:

// ❌ 旧
SaveManager.Instance?.Register(this);

// ✅ 新
ServiceLocator.GetOrDefault<SaveManager>()?.Register(this);

SaveableMonoBehaviour(影响范围最广的基类)同时补充 using BaseGames.Core; import。

ScriptableObject 中的 ServiceLocatorC-7EventChainSO

ScriptableObject 是纯 C# 对象ServiceLocator 作为 static Dictionary 同样可在其中访问:

// ❌ 旧IsMet 表达式体——双重 Instance 访问)
public override bool IsMet() => SaveManager.Instance != null && SaveManager.Instance.GetFlag(flagId);

// ✅ 新(单次查询,缓存到局部变量)
public override bool IsMet()
{
    var sm = ServiceLocator.GetOrDefault<SaveManager>();
    return sm != null && sm.GetFlag(flagId);
}

RangedEnemy 空指针修复C-11

原代码在池服务未就绪时会抛出 NullReferenceException

// ❌ 旧(无空检查)
var go = GlobalObjectPool.Instance.Spawn(_projectileConfig.PoolKey, spawnPos, Quaternion.identity);

// ✅ 新(明确 null 检查 + 警告日志)
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null)
{
    Debug.LogWarning($"[RangedEnemy] {name}: IObjectPoolService 未就绪,无法生成抛射物。");
    return;
}
var go = pool.Spawn(_projectileConfig.PoolKey, spawnPos, Quaternion.identity);

QuestGiver 重复访问优化C-16

原代码在 3 个方法中各自重新调用 QuestManager.Instance,迁移时统一缓存:

var qm = ServiceLocator.GetOrDefault<IQuestManager>();
if (qm == null) return;
// 只查询一次,后续均使用 qm 局部变量

五、设计原则对比

Q4 后项目依赖注入一致性

模式 Q3 之前 Q4 之后
ServiceLocator 注册 仅部分 Manager 全部 Manager
静态 Instance 访问 10 个类保留 0 个
[Obsolete] / #pragma 噪音 5 处 0 处
ScriptableObject 访问运行时服务 SaveManager.InstanceInstance 模式) ServiceLocator.GetOrDefault<SaveManager>()(统一模式)

与商业参考项目对比

项目 单例模式 DI 方案 一致性
Hollow KnightTeam Cherry 多数 GameManager.Instance 无标准 DI
Dead CellsMotion Twin MonoBehaviourSingleton 泛型基类 内部 Locator
本项目 Q4 0 个裸 Instance ServiceLocator 统一

六、本轮修改文件清单

# 文件 改动类型
1 Audio/AudioManager.cs 删除 [Obsolete] Instance + #pragma
2 Core/GameManager.cs 删除 [Obsolete] Instance + #pragma
3 Camera/CameraStateController.cs 删除 [Obsolete] Instance + #pragma + OnDestroy
4 VFX/VFXPool.cs 删除 [Obsolete] Instance + #pragma
5 Core/Save/SaveManager.cs 删除 [Obsolete] Instance + #pragma
6 Core/Pool/GlobalObjectPool.cs 删除裸 Instance
7 Quest/QuestManager.cs 删除裸 Instance
8 Core/Events/EventChannelRegistry.cs 删除裸 Instance,新增 ServiceLocator 注册
9 Tutorial/TutorialManager.cs 删除裸 Instance + OnDestroy,新增 ServiceLocator 注册
10 World/Map/MapManager.cs 删除裸 Instance
11 Core/Save/SaveableMonoBehaviour.cs 迁移至 ServiceLocator + 添加 using
12 World/Map/MapPin.cs 迁移至 ServiceLocator + 添加 using
13 World/Map/MapManager.cs 迁移 OnEnable/OnDisable
14 Core/Difficulty/DifficultyManager.cs 迁移至 ServiceLocator
15 Core/DeathRespawnService.cs 迁移至 ServiceLocator
16 Quest/ChallengeRoomTrigger.cs 迁移至 ServiceLocator + 添加 using
17 Quest/ChallengeRoomManager.cs 迁移至 ServiceLocator + 添加 using
18 EventChain/EventChainSO.cs 迁移至 ServiceLocator + 添加 using
19 EventChain/EventChainManager.cs 迁移至 ServiceLocator
20 Progression/ProgressLock.cs 迁移至 ServiceLocator + 添加 using
21 Progression/HPContainerPickup.cs 迁移至 ServiceLocator + 添加 using
22 Enemies/RangedEnemy.cs 迁移至 ServiceLocator + 添加 null 守卫
23 Core/Assets/AssetReleaseTracker.cs 迁移至 ServiceLocator
24 Enemies/AI/BD_SpawnProjectile.cs 迁移至 ServiceLocator + 添加 using
25 Enemies/AI/BD_SummonMinions.cs 迁移至 ServiceLocator + 添加 using
26 VFX/HitFXSpawner.cs 迁移至 ServiceLocator + 添加 using
27 Quest/QuestGiver.cs 迁移至 ServiceLocator缓存局部变量优化
28 Tutorial/ContextualHintTrigger.cs 迁移至 ServiceLocator + 添加 using
29 Equipment/EquipmentManager.cs 迁移至 ServiceLocator + 添加 using

七、遗留待处理事项Phase 2

ID 文件 问题 优先级
D-4 Audio/AudioManager.cs PlayBGM(string key) / PlaySFX(string key) 桩方法Phase 2 接入 AudioEventSO
D-5 Enemies/EnemyCombat.cs StartAttack() 动画 TODO
P-2 Combat/StatusEffects/StatusEffectManager.cs CleanseEffect O(n) 线性查找
R-1 Core/ServiceLocator.cs Unregister<T>() 方法,场景切换时旧引用无法清理(非 DontDestroyOnLoad 的服务)
R-2 Core/Assets/AssetReleaseTracker.cs 硬编码 PrefabEnemyGrunt / PrefabEnemySkullArch,应改为可配置列表

八、累计修复记录

轮次 文件数 修复项数 主题
Q1DeepDive_2026.md 8 15 命名空间、反射、SaveManager 迁移
Q2DeepDive_2026_Q2.md 10 12 ServiceLocator 推广、死代码、TogglePause、缩进
Q3DeepDive_2026_Q3.md 8 9 BGM源污染、音量恢复、SpriteRenderer翻转、null守卫
Q4本文 29 28S-1S-10, C-1C-18 全项目 Instance 清除
累计 64