多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,355 @@
# 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*