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

356 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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<FormSkillSO>` 内联书写,降低可读性 |
| 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<Collider2D>` 防重复命中,在高密度战斗中避免 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<T> 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<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()` 空池时返回 `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<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 污染
**问题代码**
```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<ICameraService>() 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<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 守卫
```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<FormSkillSO>(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<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*