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

16 KiB
Raw Blame History

zeling_v2 代码深度评审 — Q3 2026

审查周期: 第三轮 (Q3) — 延续 DeepDive_2026_Q2.md
范围: 本周期新增审查模块 + 对全项目做整体架构复查
基准: 成熟商业 2D 动作平台游戏(空洞骑士 / 盐和献祭 / Axiom Verge标准
Unity 版本: 2022.3 LTS / .NET Standard 2.1 / C# 9
审查工程师: GitHub Copilot (Claude Sonnet 4.6)


一、审查范围

本轮新增审查模块

模块 核心文件
游戏主管理 Core/GameManager.cs, Core/GameStateMachine.cs
音频系统 Audio/AudioManager.cs, Audio/BGMController.cs, Audio/CombatSFXController.cs, Audio/AudioMixerKeys.cs
玩家状态机 Player/States/PlayerController.cs, AttackState.cs, DashState.cs
玩家战斗 Player/PlayerCombat.cs, Player/PlayerStats.cs, Player/PlayerMovement.cs
状态效果 Combat/StatusEffects/StatusEffectManager.cs
相机 Camera/CameraStateController.cs
敌人基础 Enemies/EnemyBase.cs, Enemies/EnemyMovement.cs, Enemies/EnemyCombat.cs
Boss Enemies/Boss/BossBase.cs, Enemies/Boss/BossSkillExecutor.cs
Boss 模式 Enemies/Boss/Patterns/TelegraphSystem.cs
招架系统 Parry/ParrySystem.cs
技能系统 Skills/SkillManager.cs
对话 Dialogue/DialogueManager.cs
服务层 Core/GameServiceRegistrar.cs, Core/ServiceLocator.cs
场景 Core/SceneLoader.cs
UI UI/UIManager.cs
世界 World/RoomController.cs, World/RoomTransition.cs, World/MovingPlatform.cs, World/CrumblePlatform.cs
对象池 Core/Pool/GlobalObjectPool.cs
VFX VFX/VFXPool.cs
教程 Tutorial/TutorialManager.cs
表单控制 Player/FormController.cs
过场 Cutscene/CutsceneManager.cs
碰撞 Combat/HitBox.cs
设置 Core/SettingsManager.cs, Core/GlobalSettingsSO.cs

二、五维评分表

维度 Q2 得分 Q3 得分 变化 说明
架构设计 8.0 8.0 整体架构依然优秀;发现 GameManager/CameraStateController/TutorialManager 存在"双身份"问题(既用静态单例又用 ServiceLocatorServiceLocator 的一致性尚未全面落实
性能 7.5 7.5 BGM Source 作为 SFX 兜底的 bug 严重影响实际运行EnemyMovement 的 localScale 翻转会引发物理帧同步问题;其余性能表现良好(无 GC alloc、批量 LOS、sqrMagnitude 等)
可扩展性 8.5 8.5 SO 配置驱动、事件频道、HitBox/DamageInfo 结构体等设计均出色BossSkillExecutor 的可组合 SO 技能序列是同类游戏中少见的干净实现
编辑器友好 8.0 8.0 EventBusMonitor Editor 调试、Inspector Tooltip 完整、RequireComponent 正确CombatSFXController 每个音效类型单独 [SerializeField] 利于调试
使用便利性 7.8 7.8 ServiceLocator 模式整体可用AudioManager.Initialize() 空实现导致音量从未从存档恢复DialogueManager 未处理 _inputReader 为 null 的场景
综合 7.96 7.96 Q2 修复后整体稳固Q3 发现问题以运行时正确性为主,不涉及架构重构

三、问题全表Q3 新发现)

A — 架构问题

ID 文件 严重性 描述
A-1 Camera/CameraStateController.cs ⚠️ public static CameraStateController Instance 未标注 [System.Obsolete];该类同时注册 ServiceLocator双重访问入口造成调用混乱
A-2 Core/GameManager.cs ⚠️ public static GameManager Instance 使用纯静态单例,从未向 ServiceLocator 注册,与项目模式不一致;其他模块无法通过 ServiceLocator 获取 GameManager
A-3 Tutorial/TutorialManager.cs 🔵 保留原始 DontDestroyOnLoad 静态单例,未整合 ServiceLocator与 GameServiceRegistrar 注册模式不一致
A-4 Enemies/Boss/Patterns/TelegraphSystem.cs 🔵 直接访问 GlobalObjectPool.Instance(已标 [Obsolete]),应改用 ServiceLocator.Get<IObjectPoolService>()

P — 性能与正确性问题

ID 文件 严重性 描述
P-1 Audio/AudioManager.cs 🔴 NextSFXSource(): _sfxSources 为空时返回 _bgmSourceA,导致 SFX 通过 BGM AudioSource 播放,破坏交叉淡入淡出的音量状态
P-2 Combat/StatusEffects/StatusEffectManager.cs 🔵 CleanseEffect() 调用 _activeList.Remove(effect) — 已知引用的情况下做 O(n) 线性扫描;可改用 List<T>.RemoveSwapBack 或直接索引删除
P-3 Enemies/EnemyMovement.cs ⚠️ UpdateFacing()transform.localScale X 翻转,影响整个 GameObject 层次(含碰撞体偏移),应改为 SpriteRenderer.flipXPlayerMovement 已采用此模式)

D — 代码质量 / 开发体验问题

ID 文件 严重性 描述
D-1 Dialogue/DialogueManager.cs ⚠️ OnEnable() / OnDisable() 直接访问 _inputReader.SubmitEvent 无 null 检查;若 Inspector 未绑定 _inputReader 则引发 NullReferenceException
D-2 Skills/SkillManager.cs 🔵 文件顶部缺少 using System.Collections.Generic;UpdateSkillSet() 内用完整限定名 System.Collections.Generic.List<FormSkillSO> 内联书写,降低可读性
D-3 Audio/AudioManager.cs ⚠️ Initialize() 为空方法TODO 注释)——音量从未从存档/默认值中恢复,每次启动均使用 AudioMixer 默认值,忽略用户设置
D-4 Audio/AudioManager.cs 🔵 PlayBGM(string key)PlaySFX(string key) 仅发出 Debug.LogWarningPhase 2 占位符)——已知问题,文档记录备忘
D-5 Enemies/EnemyCombat.cs 🔵 StartAttack() 有 TODO 注释,动画播放未实现——已知缺口,不影响当前流程
D-6 Core/SettingsManager.cs ⚠️ SettingsManager 未向 ServiceLocator 注册自身,导致 AudioManager.Initialize() 等需要读取设置的组件无法通过 ServiceLocator 访问已加载的设置数据

四、亮点记录(继续保持)

这些实现值得在商业项目中重用,不做修改:

HitBox + DamageInfo 零 GC 设计

// HitBox.cs — struct 工厂,无堆分配
var info = DamageInfo.From(_currentSource, knockDir, _attackerTransform.position, layer);

DamageInfo 作为 struct 传递,配合 HashSet<Collider2D> 防重复命中,在高密度战斗中避免 GC 压力,是商业级设计。

BossSkillExecutor 可组合 SO 序列

// BossSkillExecutor.cs
private IEnumerator ExecuteSkillCoroutine(BossSkillSO skill)
{
    // 并行 VulnerabilityWindow + 攻击序列
    // SO 驱动设计师无需修改代码即可调整Boss行为
}

BossSkillSO → SkillSequenceSO → AttackPatternSO 三层嵌套 SO 结构支持 RepeatIfPlayerInRange、MaxRepeatCount 等行为配置,是少见的干净 Boss 设计。

BaseEventChannelSO 订阅句柄模式

// 安全订阅,配合 CompositeDisposable 防止内存泄漏
public EventSubscription Subscribe(Action<T> callback)

Editor 环境自动计数 _subscriberCount,配合 EventBusMonitor.Record 实时调试,优于裸 C# 事件。

GlobalObjectPool LRU 回收

// 达到 MaxCount 时 O(1) 回收最早活跃对象
po = aliveList.First.Value;
aliveList.RemoveFirst();
po.ForceReturnToPool();

LinkedList 保持 spawn 顺序O(1) LRU是商业游戏对象池的标准做法。

EnemyBase 批量 LOS

// 避免每个敌人每帧独立执行射线检测
public virtual bool IsPlayerVisible() => _losResult; // BatchLOSSystem 写入

通过 BatchLOSSystem 统一处理视线检测,避免 N 个敌人 × N 帧的射线广播,是高性能敌人 AI 的必要优化。

PlayerController 事件驱动 FSM

// 无 Update() switch 语句,状态对象独立封装逻辑
Owner.TransitionTo(Owner.GetState<IdleState>());

每个 State 封装 OnStateEnter/Exit/Update/FixedUpdate,配合 Input.AttackEvent 驱动,干净解耦,添加新状态无需修改现有状态。


五、对比商业标准

与《空洞骑士》架构对比

特性 空洞骑士参考 zeling_v2 差距
服务访问 静态单例(较老实践) ServiceLocator + 接口 更先进
配置驱动 SO 广泛使用 SO + 事件频道 更彻底
Boss 行为 硬编码状态机 可组合 SO 序列 更灵活
音频系统 FMOD AudioMixer + 双源淡入淡出 ⚠️ 可用,但初始化缺口
单例一致性 全静态单例 混合(部分 ServiceLocator部分静态 ⚠️ 需统一
存档音量恢复 启动时读取 Initialize() 为空 🔴 缺失功能

与《盐和献祭》性能实践对比

特性 参考实践 zeling_v2 状态
敌人 facing 翻转 SpriteRenderer.flipX transform.localScale ⚠️ 需修复
对象池 统一池 + 预热 GlobalObjectPool + WarmupAsync 正确
攻击判定 HashSet 防重复 HitBox._hitThisActivation 正确
SFX 多源轮转 多源池 有实现,但空池兜底有 bug ⚠️ P-1

六、修复优先级与计划

优先级 ID 文件 修复方案
🔴 立即修复 P-1 AudioManager.cs NextSFXSource() 空池时返回 nullPlaySFX 增加 null 守卫
⚠️ 本周期修复 D-3 AudioManager.cs + SettingsManager.cs 实现 Initialize() 从 SettingsManager 读取音量SettingsManager 注册到 ServiceLocator
⚠️ 本周期修复 D-1 DialogueManager.cs OnEnable/OnDisable 增加 _inputReader != null 守卫
⚠️ 本周期修复 A-1 CameraStateController.cs Instance 字段标注 [System.Obsolete]
⚠️ 本周期修复 A-2 GameManager.cs Awake() 中注册 ServiceLocatorInstance 标注 [System.Obsolete]
⚠️ 本周期修复 P-3 EnemyMovement.cs UpdateFacing() 改用 SpriteRenderer.flipX
🔵 优化修复 D-2 SkillManager.cs 添加 using System.Collections.Generic;,移除内联完整限定名
🔵 优化修复 A-4 TelegraphSystem.cs 改用 ServiceLocator.GetOrDefault<IObjectPoolService>()
📋 记录待办 A-3 TutorialManager.cs ServiceLocator 整合Phase 2 重构时处理)
📋 记录待办 D-4/D-5 AudioManager.cs, EnemyCombat.cs Phase 2 实现占位符

七、修复详情Q3 已应用)

Fix P-1 — AudioManager SFX 池空时 BGM Source 污染

问题代码

private AudioSource NextSFXSource()
{
    if (_sfxSources == null || _sfxSources.Length == 0) return _bgmSourceA; // ❌ 污染 BGM Source
    return _sfxSources[_sfxRoundRobin++ % _sfxSources.Length];
}

修复后

private AudioSource NextSFXSource()
{
    if (_sfxSources == null || _sfxSources.Length == 0)
    {
        Debug.LogError("[AudioManager] SFX Source 池为空,请在 Inspector 中为 _sfxSources 赋值。");
        return null;
    }
    return _sfxSources[_sfxRoundRobin++ % _sfxSources.Length];
}
// PlaySFX 增加 null 守卫
public void PlaySFX(AudioClip clip, float volumeScale = 1f)
{
    if (clip == null) return;
    var src = NextSFXSource();
    if (src == null) return; // ← 新增
    src.volume = volumeScale;
    src.PlayOneShot(clip);
}

Fix D-3 — AudioManager.Initialize() 实现音量恢复

问题:

  1. AudioManager.Initialize() 为空存根Volume 永远不从存档恢复
  2. SettingsManager 未注册到 ServiceLocator其他组件无法访问已加载的设置数据

修复:

  • SettingsManager.cs 新增 Awake() 向 ServiceLocator 注册自身
  • AudioManager.Initialize() 通过 ServiceLocator 获取 SettingsManager应用四路音量
  • AudioManager.Awake() 在注册 IAudioService 后调用 Initialize()

Fix A-1 — CameraStateController Instance 标注过时

[System.Obsolete("Use ServiceLocator.Get<ICameraService>() instead.")]
public static CameraStateController Instance { get; private set; }

Fix A-2 — GameManager 注册 ServiceLocator

private void Awake()
{
    if (Instance != null && Instance != this) { Destroy(gameObject); return; }
    Instance = this;
    ServiceLocator.Register<GameManager>(this); // ← 新增
    DontDestroyOnLoad(transform.root.gameObject);
    // ...
}

[System.Obsolete("Use ServiceLocator.Get<GameManager>() instead.")]
public static GameManager Instance { get; private set; }

Fix D-1 — DialogueManager _inputReader null 守卫

private void OnEnable()
{
    if (_inputReader != null) _inputReader.SubmitEvent += OnSubmit; // ← 添加 null check
}
private void OnDisable()
{
    if (_inputReader != null) _inputReader.SubmitEvent -= OnSubmit; // ← 添加 null check
}

Fix D-2 — SkillManager using 指令

// 文件顶部添加
using System.Collections.Generic;

// UpdateSkillSet() 内替换
var active = new List<FormSkillSO>(3); // ← 移除 System.Collections.Generic. 限定名

Fix P-3 — EnemyMovement SpriteRenderer.flipX

// 新增字段
[SerializeField] private SpriteRenderer _spriteRenderer;

// UpdateFacing 替换
private void UpdateFacing(float dir)
{
    if (Mathf.Approximately(dir, 0f)) return;
    int newDir = dir > 0f ? 1 : -1;
    if (newDir == _facingDir) return;
    _facingDir = newDir;
    if (_spriteRenderer != null)
        _spriteRenderer.flipX = newDir < 0;
    else
    {
        // Fallback: localScale当 Inspector 未绑定 SpriteRenderer 时)
        Vector3 s = transform.localScale;
        transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
    }
}

Fix B-1 — TelegraphSystem 改用 ServiceLocator

// Before
var pool = GlobalObjectPool.Instance;

// After
var pool = BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.Pool.IObjectPoolService>();

八、累计修复记录(三轮汇总)

轮次 修复数 主题
Q1 (DeepDive_2026.md) 15 命名空间、反射清理、SaveManager 迁移、基础 Null 守卫
Q2 (DeepDive_2026_Q2.md) 12 SaveManager→ServiceLocator、死代码清理、TogglePause 逻辑、IndentFix
Q3 (本文档) 9 BGM 源污染 bug、音量恢复实现、SpriteRenderer 翻转、架构 Obsolete 标注、null 守卫
合计 36

九、后续建议Phase 2 / 长期)

  1. TutorialManager ServiceLocator 整合
    DontDestroyOnLoad + 静态单例替换为 GameServiceRegistrar 注册,与 SaveManager/DialogueManager 保持一致。

  2. SettingsManager Apply() 补全音量应用
    SettingsManager.Apply() 中加入 AudioMixer 音量设置(通过 ServiceLocator 获取 IAudioService避免 SettingsManager 和 AudioManager 之间的职责模糊。

  3. AudioManager Phase 2 占位符实现
    PlayBGM(string key)PlaySFX(string key) 接入 AudioEventSO替换当前 LogWarning 占位符。

  4. EnemyCombat.StartAttack() 动画
    补全动画播放逻辑,与 PlayerCombat 的 HitBox 事件链对齐。

  5. StatusEffectManager.CleanseEffect 优化
    List<T> 替换为带索引的数据结构(如 Dictionary<string, StatusEffect>O(n) Remove → O(1) Remove。


文档生成时间: 2026-Q3 | 工具: GitHub Copilot Claude Sonnet 4.6