# 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 存在"双身份"问题(既用静态单例又用 ServiceLocator),ServiceLocator 的一致性尚未全面落实 | | **性能** | 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()` | ### 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.RemoveSwapBack` 或直接索引删除 | | P-3 | `Enemies/EnemyMovement.cs` | ⚠️ 中 | `UpdateFacing()` 用 `transform.localScale` X 翻转,影响整个 GameObject 层次(含碰撞体偏移),应改为 `SpriteRenderer.flipX`(PlayerMovement 已采用此模式) | ### 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` 内联书写,降低可读性 | | D-3 | `Audio/AudioManager.cs` | ⚠️ 中 | `Initialize()` 为空方法(TODO 注释)——音量从未从存档/默认值中恢复,每次启动均使用 AudioMixer 默认值,忽略用户设置 | | D-4 | `Audio/AudioManager.cs` | 🔵 低 | `PlayBGM(string key)` 和 `PlaySFX(string key)` 仅发出 `Debug.LogWarning`(Phase 2 占位符)——已知问题,文档记录备忘 | | D-5 | `Enemies/EnemyCombat.cs` | 🔵 低 | `StartAttack()` 有 TODO 注释,动画播放未实现——已知缺口,不影响当前流程 | | D-6 | `Core/SettingsManager.cs` | ⚠️ 中 | `SettingsManager` 未向 ServiceLocator 注册自身,导致 `AudioManager.Initialize()` 等需要读取设置的组件无法通过 ServiceLocator 访问已加载的设置数据 | --- ## 四、亮点记录(继续保持) 这些实现值得在商业项目中重用,不做修改: ### HitBox + DamageInfo 零 GC 设计 ```csharp // HitBox.cs — struct 工厂,无堆分配 var info = DamageInfo.From(_currentSource, knockDir, _attackerTransform.position, layer); ``` `DamageInfo` 作为 struct 传递,配合 `HashSet` 防重复命中,在高密度战斗中避免 GC 压力,是商业级设计。 ### BossSkillExecutor 可组合 SO 序列 ```csharp // BossSkillExecutor.cs private IEnumerator ExecuteSkillCoroutine(BossSkillSO skill) { // 并行 VulnerabilityWindow + 攻击序列 // SO 驱动,设计师无需修改代码即可调整Boss行为 } ``` `BossSkillSO → SkillSequenceSO → AttackPatternSO` 三层嵌套 SO 结构支持 RepeatIfPlayerInRange、MaxRepeatCount 等行为配置,是少见的干净 Boss 设计。 ### BaseEventChannelSO 订阅句柄模式 ```csharp // 安全订阅,配合 CompositeDisposable 防止内存泄漏 public EventSubscription Subscribe(Action callback) ``` Editor 环境自动计数 `_subscriberCount`,配合 `EventBusMonitor.Record` 实时调试,优于裸 C# 事件。 ### GlobalObjectPool LRU 回收 ```csharp // 达到 MaxCount 时 O(1) 回收最早活跃对象 po = aliveList.First.Value; aliveList.RemoveFirst(); po.ForceReturnToPool(); ``` LinkedList 保持 spawn 顺序,O(1) LRU,是商业游戏对象池的标准做法。 ### EnemyBase 批量 LOS ```csharp // 避免每个敌人每帧独立执行射线检测 public virtual bool IsPlayerVisible() => _losResult; // BatchLOSSystem 写入 ``` 通过 BatchLOSSystem 统一处理视线检测,避免 N 个敌人 × N 帧的射线广播,是高性能敌人 AI 的必要优化。 ### PlayerController 事件驱动 FSM ```csharp // 无 Update() switch 语句,状态对象独立封装逻辑 Owner.TransitionTo(Owner.GetState()); ``` 每个 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()` 空池时返回 `null`;`PlaySFX` 增加 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()` 中注册 ServiceLocator;`Instance` 标注 `[System.Obsolete]` | | ⚠️ 本周期修复 | P-3 | `EnemyMovement.cs` | `UpdateFacing()` 改用 `SpriteRenderer.flipX` | | 🔵 优化修复 | D-2 | `SkillManager.cs` | 添加 `using System.Collections.Generic;`,移除内联完整限定名 | | 🔵 优化修复 | A-4 | `TelegraphSystem.cs` | 改用 `ServiceLocator.GetOrDefault()` | | 📋 记录待办 | 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 污染 **问题代码**: ```csharp private AudioSource NextSFXSource() { if (_sfxSources == null || _sfxSources.Length == 0) return _bgmSourceA; // ❌ 污染 BGM Source return _sfxSources[_sfxRoundRobin++ % _sfxSources.Length]; } ``` **修复后**: ```csharp 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 标注过时 ```csharp [System.Obsolete("Use ServiceLocator.Get() instead.")] public static CameraStateController Instance { get; private set; } ``` --- ### Fix A-2 — GameManager 注册 ServiceLocator ```csharp private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; ServiceLocator.Register(this); // ← 新增 DontDestroyOnLoad(transform.root.gameObject); // ... } [System.Obsolete("Use ServiceLocator.Get() instead.")] public static GameManager Instance { get; private set; } ``` --- ### Fix D-1 — DialogueManager _inputReader null 守卫 ```csharp 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 指令 ```csharp // 文件顶部添加 using System.Collections.Generic; // UpdateSkillSet() 内替换 var active = new List(3); // ← 移除 System.Collections.Generic. 限定名 ``` --- ### Fix P-3 — EnemyMovement SpriteRenderer.flipX ```csharp // 新增字段 [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 ```csharp // Before var pool = GlobalObjectPool.Instance; // After var pool = BaseGames.Core.ServiceLocator.GetOrDefault(); ``` --- ## 八、累计修复记录(三轮汇总) | 轮次 | 修复数 | 主题 | |------|-------|------| | 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` 替换为带索引的数据结构(如 `Dictionary`),O(n) Remove → O(1) Remove。 --- *文档生成时间: 2026-Q3 | 工具: GitHub Copilot Claude Sonnet 4.6*