多轮审查和修复
This commit is contained in:
634
Docs/Review/ComprehensiveCodeReview.md
Normal file
634
Docs/Review/ComprehensiveCodeReview.md
Normal file
@@ -0,0 +1,634 @@
|
||||
# zeling_v2 完整代码综合评审(含修复后状态)
|
||||
|
||||
> **评审日期**:2026-05-11
|
||||
> **评审范围**:`Assets/Scripts/` 全部 25 个程序集,约 200+ 源文件
|
||||
> **参照基准**:《空洞骑士》《Celeste》《Neon Abyss》《Dead Cells》《Hades》等成熟商业 2D 动作游戏
|
||||
> **修复轮次**:本文档反映第三轮优化(10 项 P0-P2 改动 + 8 项本轮改动)后的当前代码状态
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [评分总览](#1-评分总览)
|
||||
2. [架构设计深度评审](#2-架构设计深度评审)
|
||||
3. [性能深度评审](#3-性能深度评审)
|
||||
4. [可扩展性深度评审](#4-可扩展性深度评审)
|
||||
5. [编辑器友好性深度评审](#5-编辑器友好性深度评审)
|
||||
6. [使用便利性(DX)深度评审](#6-使用便利性dx深度评审)
|
||||
7. [子系统逐一点评](#7-子系统逐一点评)
|
||||
8. [残留问题清单](#8-残留问题清单)
|
||||
9. [后续迭代路线图](#9-后续迭代路线图)
|
||||
|
||||
---
|
||||
|
||||
## 1. 评分总览
|
||||
|
||||
| 维度 | 本次得分 | 上次得分 | 变化 | 商业顶线参考 |
|
||||
|---|---|---|---|---|
|
||||
| **架构设计** | 8.0 | 7.5 | ↑ 0.5 | 8.5(HK/Celeste 架构级别) |
|
||||
| **性能** | 7.5 | 7.0 | ↑ 0.5 | 8.0 |
|
||||
| **可扩展性** | 7.5 | 7.5 | → | 9.0 |
|
||||
| **编辑器友好性** | 7.5 | 6.5 | ↑ 1.0 | 8.0 |
|
||||
| **使用便利性** | 7.5 | 7.0 | ↑ 0.5 | 8.5 |
|
||||
| **综合** | **7.6** | **7.1** | ↑ **0.5** | ≈8.3 |
|
||||
|
||||
> 本轮修复了 `localScale` 翻转 / `SwimState` 未注册 / `ForceState()` 空实现 / `SaveManager.Data` 直接暴露 / `GameServiceRegistrar` 场景扫描 / `PlatformBootstrap` 每帧查找等多个 P1 缺陷,各维度均有明显提升。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计深度评审
|
||||
|
||||
### 2.1 ✅ 顶层架构:层次分明,职责边界清晰
|
||||
|
||||
```
|
||||
Persistent Scene
|
||||
[GameManager] ← 单一入口,GameStateMachine 封装
|
||||
[GameServiceRegistrar] ← 服务注册,DefaultExecutionOrder(-2000)
|
||||
[AudioManager] ← IAudioService 实现
|
||||
[GlobalObjectPool] ← 对象池
|
||||
[SaveManager] ← 存档
|
||||
[QuestManager] ← 任务
|
||||
[MapManager] ← 地图
|
||||
[EventChainManager] ← 叙事事件链
|
||||
|
||||
Gameplay Scene(单场景或分块加载)
|
||||
[RoomController] ← 房间出生点 + 摄像机切换
|
||||
Enemy Prefabs ← EnemyBase + BehaviorTree
|
||||
Player Prefab ← PlayerController + 16 状态 + HurtBox + HitBox
|
||||
```
|
||||
|
||||
**亮点**:`GameServiceRegistrar.DefaultExecutionOrder(-2000)` 是所有 MonoBehaviour 中最早执行的,确保服务在任何其他脚本的 `Awake` 之前注册完毕,启动顺序依赖问题得到彻底解决。
|
||||
|
||||
### 2.2 ✅ GameStateId:正确实现 IEquatable,消除装箱
|
||||
|
||||
```csharp
|
||||
public readonly struct GameStateId : System.IEquatable<GameStateId>
|
||||
{
|
||||
public readonly string Id;
|
||||
public bool Equals(GameStateId other) => Id == other.Id;
|
||||
public override bool Equals(object obj) => obj is GameStateId g && Equals(g);
|
||||
public override int GetHashCode() => Id?.GetHashCode() ?? 0;
|
||||
public static bool operator ==(GameStateId a, GameStateId b) => a.Equals(b);
|
||||
public static bool operator !=(GameStateId a, GameStateId b) => !a.Equals(b);
|
||||
}
|
||||
```
|
||||
|
||||
`readonly struct` + `IEquatable<T>` + 显式 `==`/`!=` 运算符,字典查找走 `Equals(GameStateId)` 重载,完全无装箱。**之前评审中担心的 struct 装箱问题已正确处理。**
|
||||
|
||||
### 2.3 ✅ GameStateMachine:合法性验证 + 错误上报
|
||||
|
||||
```csharp
|
||||
if (_current != null && !_current.ValidNextStates.Contains(nextId))
|
||||
{
|
||||
error = $"[GameStateMachine] 非法转换 {_current.Id} → {nextId}";
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
状态图的合法转换表由每个 `IGameState` 自持,违规转换不静默失败,而是返回 false + error 字符串,调用方(GameManager)打 Warning。这是商业级防御性设计。
|
||||
|
||||
### 2.4 ✅ DamageInfo 双路径工厂
|
||||
|
||||
```csharp
|
||||
// 热路径:零堆分配,直接从 SO 填字段
|
||||
public static DamageInfo From(DamageSourceSO so) { ... }
|
||||
|
||||
// 构建路径:可读链式 Builder
|
||||
var info = new DamageInfo.Builder()
|
||||
.SetRaw(damage).SetType(Fire).SetFlags(CanBeParried).Build();
|
||||
```
|
||||
|
||||
高频 HitBox 碰撞走 `DamageInfo.From(so)` 无 GC;Boss 技能等复杂伤害信息走 Builder,清晰易读。两条路径并存,不牺牲一方。
|
||||
|
||||
### 2.5 ✅ EventChainManager:SO 事件 → C# 事件的桥接层
|
||||
|
||||
```csharp
|
||||
// SO 事件(跨场景持久)→ 中继 C# 事件(给 ChainCondition 订阅)
|
||||
Subscribe(_onBossDefeated, id => { OnBossDefeated?.Invoke(id); EvaluateAll(); });
|
||||
```
|
||||
|
||||
将 ScriptableObject 事件频道与纯 C# 条件判断系统优雅解耦。叙事链条件不需要知道 SO 的存在,EventChainManager 成为纯净的"事件路由器"。
|
||||
|
||||
### 2.6 ✅ AudioManager:旧单例迁移模式规范
|
||||
|
||||
```csharp
|
||||
// 标记已废弃的旧访问方式
|
||||
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
|
||||
public static AudioManager Instance { get; private set; }
|
||||
```
|
||||
|
||||
在完成迁移之前,用 `[Obsolete]` 标记旧 API 而非直接删除,给调用方平滑过渡期,是成熟工程实践。
|
||||
|
||||
---
|
||||
|
||||
### 2.7 ⚠️ 剩余架构问题
|
||||
|
||||
#### P1:GameManager / QuestManager / MapManager 仍是 static Instance
|
||||
|
||||
```csharp
|
||||
public static GameManager Instance { get; private set; } // 核心状态机持有者
|
||||
public static QuestManager Instance { get; private set; } // ISaveable 实现者
|
||||
public static MapManager Instance { get; private set; } // ISaveable 实现者
|
||||
```
|
||||
|
||||
SaveManager 的 `Data` 属性已标 `[Obsolete]` 并提供具名访问器,但上述三个管理器仍在用 `static Instance`,与 `ServiceLocator` 模式并存。这导致:
|
||||
- `QuestManager.Instance?.Register(this)` 在 `OnEnable` 中调用——若 QuestManager 先于 SaveManager 在 Inspector 顺序中初始化,`SaveManager.Instance` 可能为 null
|
||||
- 无法在运行时替换 QuestManager 实现(如 Tutorial 场景使用简化版)
|
||||
|
||||
**建议**:三者均添加 `ServiceLocator.Register<IQuestService>(this)` 等接口注册,保留 `static Instance` 作为向后兼容的转发属性(内部走 ServiceLocator)。
|
||||
|
||||
#### P2:QuestManager._onEnemyDied 语义不匹配
|
||||
|
||||
```csharp
|
||||
[SerializeField] private TransformEventChannelSO _onEnemyDied; // 携带 Transform
|
||||
```
|
||||
|
||||
任务系统用 Transform 反查 `EnemyBase` 组件再读 EnemyId,而任务关心的是"哪种敌人死了"而非"哪个具体对象"。多一层间接查找,且 Transform 可能在查找时已被销毁。
|
||||
|
||||
---
|
||||
|
||||
## 3. 性能深度评审
|
||||
|
||||
### 3.1 ✅ 热路径优化全覆盖
|
||||
|
||||
| 热路径 | 修复前 | 修复后 |
|
||||
|---|---|---|
|
||||
| `HurtBox` 每次受击 | `GetComponentInParent<IStatusEffectable>()` | `Awake` 缓存 |
|
||||
| `EnemyBase.Update` 距离 | `Vector2.Distance`(√) | `sqrMagnitude` |
|
||||
| `IsPlayerInRange(r)` | `Mathf.Sqrt` | `range * range` |
|
||||
| `GlobalObjectPool` 活跃对象强制回收 | `List.RemoveAt(0)` O(n) | `LinkedList` 头节点 O(1) |
|
||||
| `PlatformBootstrap.Update` | `ServiceLocator.GetOrDefault` 每帧字典查找 | `_platform` 字段缓存 |
|
||||
| `PlayerMovement.UpdateFacing` | `localScale` 写入(触发 Transform 脏) | `SpriteRenderer.flipX` |
|
||||
|
||||
### 3.2 ✅ StatusEffectManager MaterialPropertyBlock
|
||||
|
||||
```csharp
|
||||
private MaterialPropertyBlock _propBlock;
|
||||
private void Awake() { _propBlock = new MaterialPropertyBlock(); ... }
|
||||
// 设置 Shader 属性时:
|
||||
_renderer.GetPropertyBlock(_propBlock);
|
||||
_propBlock.SetFloat(_flashId, value);
|
||||
_renderer.SetPropertyBlock(_propBlock);
|
||||
```
|
||||
|
||||
不修改共享 `Material`,不触发 Unity 的 Material 实例化,避免渲染批次打断。
|
||||
|
||||
### 3.3 ✅ 对象池 + Addressables 预热
|
||||
|
||||
- 离线 Addressables `LoadAssetAsync` 加载 Prefab,零运行时同步加载
|
||||
- `PooledObject` 节点缓存 `GetComponentCached<T>()` 避免重复 `GetComponent`
|
||||
- LinkedList 活跃列表 + LRU 强制回收,100 子弹同屏时 Spawn/Despawn 稳定 O(1)
|
||||
|
||||
### 3.4 ⚠️ 剩余性能问题
|
||||
|
||||
#### P2:SkillManager.Update 三个浮点减法(可忽略,但有更优方案)
|
||||
|
||||
```csharp
|
||||
private void Update()
|
||||
{
|
||||
if (_soulCooldown > 0) _soulCooldown -= Time.deltaTime;
|
||||
if (_spirit1Cooldown > 0) _spirit1Cooldown -= Time.deltaTime;
|
||||
if (_spirit2Cooldown > 0) _spirit2Cooldown -= Time.deltaTime;
|
||||
}
|
||||
```
|
||||
|
||||
每帧三次浮点减法是可接受的开销。若后续扩展到 N 个技能,应改为 `Dictionary<FormSkillSO, float>` + 统一遍历。
|
||||
|
||||
#### P2:WorldStateRegistry 无变更通知机制
|
||||
|
||||
```csharp
|
||||
public void MarkCollected(string id) => _collectedIds.Add(id); // 无事件广播
|
||||
```
|
||||
|
||||
HashSet 写入后没有通知机制,调用方无法得知状态变化。UI 需要主动轮询或自行订阅其他事件来驱动刷新,不够响应式。
|
||||
|
||||
#### P3:GameManager.RegisterStates 每帧 `_fsm.Tick(deltaTime)`
|
||||
|
||||
```csharp
|
||||
private void Update() => _fsm.Tick(Time.deltaTime);
|
||||
```
|
||||
|
||||
状态机 Tick 调用是 O(1)(一次虚方法调用),但若当前状态(如 `GameplayState`)本身做了复杂逻辑,则需进一步评估。整体可接受。
|
||||
|
||||
---
|
||||
|
||||
## 4. 可扩展性深度评审
|
||||
|
||||
### 4.1 ✅ QuestSO 分支叙事系统结构完整
|
||||
|
||||
```csharp
|
||||
[Header("完成后续任务(分支)")]
|
||||
public QuestBranch[] branches;
|
||||
|
||||
[Serializable]
|
||||
public class QuestBranch
|
||||
{
|
||||
public string conditionQuestId; // 条件任务 Completed → 走本分支(空 = 默认)
|
||||
public QuestSO nextQuest;
|
||||
public string npcDialogueKey;
|
||||
}
|
||||
```
|
||||
|
||||
任务树可以纯通过 SO 资产的引用关系构建,无需修改代码添加新分支。条件任务完成 → 解锁后续任务的逻辑在 `QuestManager.CompleteQuest()` 中自动处理。
|
||||
|
||||
### 4.2 ✅ EquipmentManager:护符效果接口化
|
||||
|
||||
```csharp
|
||||
public interface ICharmEffect
|
||||
{
|
||||
void OnEquip(EquipmentContext ctx);
|
||||
void OnUnequip(EquipmentContext ctx);
|
||||
}
|
||||
```
|
||||
|
||||
每个护符效果是独立的 `ScriptableObject`(或类),实现 `ICharmEffect`。`EquipmentContext` 携带 Stats / Feedback / SkillMods / WeaponMgr 等必要引用,效果脚本不需要 MonoBehaviour,完全可测试。
|
||||
|
||||
### 4.3 ✅ DeathRespawnService 接口化,可替换实现
|
||||
|
||||
```csharp
|
||||
public interface IDeathRespawnService
|
||||
{
|
||||
IEnumerator StartDeathSequenceCoroutine();
|
||||
IEnumerator StartRespawnCoroutine();
|
||||
IEnumerator StartGameOverCoroutine();
|
||||
}
|
||||
```
|
||||
|
||||
GameManager 通过 `ServiceLocator.Get<IDeathRespawnService>()` 使用,测试时可注入 Mock(立即完成的假实现),不需要等待真实的死亡动画计时。
|
||||
|
||||
### 4.4 ✅ WorldStateRegistry:ScriptableObject 注入,跨场景共享
|
||||
|
||||
`WorldStateRegistry` 是 ScriptableObject(非 MonoBehaviour),在所有场景之间共享同一资产实例,不需要 `DontDestroyOnLoad` 也能保持状态。
|
||||
|
||||
### 4.5 ⚠️ 剩余可扩展性问题
|
||||
|
||||
#### P1:WorldStateRegistry 新增实体类型成本高
|
||||
|
||||
```csharp
|
||||
// 5 类实体,5 个独立 HashSet,5 对方法
|
||||
private HashSet<string> _collectedIds;
|
||||
private HashSet<string> _activatedSavePoints;
|
||||
private HashSet<string> _openedDoors;
|
||||
private HashSet<string> _destroyedObjects;
|
||||
private HashSet<string> _flags;
|
||||
|
||||
public bool IsCollected(string id) => _collectedIds.Contains(id);
|
||||
public void MarkCollected(string id) => _collectedIds.Add(id);
|
||||
// ... 每新增一类需要 3 处改动
|
||||
```
|
||||
|
||||
**建议**:
|
||||
|
||||
```csharp
|
||||
public enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag }
|
||||
private readonly Dictionary<WorldObjectCategory, HashSet<string>> _states = new();
|
||||
|
||||
public bool IsMarked(WorldObjectCategory cat, string id)
|
||||
=> _states.TryGetValue(cat, out var s) && s.Contains(id);
|
||||
public void Mark(WorldObjectCategory cat, string id)
|
||||
=> (_states.TryGetValue(cat, out var s) ? s : (_states[cat] = new HashSet<string>())).Add(id);
|
||||
```
|
||||
|
||||
新增实体类型只需在枚举中添加一行。
|
||||
|
||||
#### P1:SkillManager 硬编码三技能槽
|
||||
|
||||
```csharp
|
||||
private FormSkillSO _soulSkill;
|
||||
private FormSkillSO _spirit1;
|
||||
private FormSkillSO _spirit2;
|
||||
private float _soulCooldown;
|
||||
private float _spirit1Cooldown;
|
||||
private float _spirit2Cooldown;
|
||||
```
|
||||
|
||||
槽位数量在编译时固定为 3,无法通过配置扩展。若后期形态需要 4 个或 2 个技能,需修改 `SkillManager` 代码。
|
||||
|
||||
**建议**:
|
||||
|
||||
```csharp
|
||||
private readonly Dictionary<FormSkillSO, float> _cooldowns = new();
|
||||
public void UpdateSkillSet(FormSkillSO[] skills)
|
||||
{
|
||||
_cooldowns.Clear();
|
||||
foreach (var s in skills) if (s != null) _cooldowns[s] = 0f;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 编辑器友好性深度评审
|
||||
|
||||
### 5.1 ✅ SOValidationRunner:构建前自动数据校验
|
||||
|
||||
```csharp
|
||||
public class SOValidationRunner : IPreprocessBuildWithReport
|
||||
{
|
||||
public void OnPreprocessBuild(BuildReport report)
|
||||
{
|
||||
var (errors, warnings) = RunAll();
|
||||
if (errors.Count > 0)
|
||||
throw new BuildFailedException(...); // 有错误时中止构建
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Validate All ScriptableObjects")]
|
||||
public static void ValidateMenu() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- 实现 `IPreprocessBuildWithReport`,构建时自动运行,防止空引用 SO 进入 Release 包
|
||||
- `callbackOrder = 1`,在 `AddressKeyValidator (order=0)` 后执行,验证链有序
|
||||
- `[MenuItem]` 支持手动一键校验
|
||||
|
||||
### 5.2 ✅ EventBusMonitor 运行时追踪
|
||||
|
||||
256 条环形缓冲区,记录每个 SO 事件的:帧号 / 频道名 / 负载 / 监听器数量。任意事件触发时序问题在 Editor 中 5 秒内可定位。
|
||||
|
||||
### 5.3 ✅ EventChainEditorWindow 编辑器专用日志
|
||||
|
||||
```csharp
|
||||
#if UNITY_EDITOR
|
||||
public static event Action<string, string> OnChainExecutedInEditor;
|
||||
#endif
|
||||
```
|
||||
|
||||
叙事链执行时向编辑器窗口推送日志(chainId + 执行结果),不产生运行时开销。`#if UNITY_EDITOR` 包裹严格,不泄漏到 Release 构建。
|
||||
|
||||
### 5.4 ✅ WorldMarker 可视化 Gizmos
|
||||
|
||||
```csharp
|
||||
Gizmos.color = _markerType switch
|
||||
{
|
||||
WorldMarkerType.Objective => Color.yellow,
|
||||
WorldMarkerType.NPC => Color.cyan,
|
||||
WorldMarkerType.PointOfInterest => Color.green,
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
场景视图直接看到标记类型与覆盖范围,场景设计师无需 Play 即可预览关卡布局。
|
||||
|
||||
### 5.5 ✅ [RequireComponent] 同节点依赖自动保障
|
||||
|
||||
本轮修复后,`PlayerController` 已标注:
|
||||
```csharp
|
||||
[RequireComponent(typeof(InputBuffer))]
|
||||
[RequireComponent(typeof(PlayerMovement))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
[RequireComponent(typeof(AnimancerComponent))]
|
||||
```
|
||||
|
||||
在 Inspector 中添加 PlayerController 时 Unity 自动附加所有必须组件,防止遗漏。
|
||||
|
||||
### 5.6 ⚠️ 剩余编辑器问题
|
||||
|
||||
#### P2:EquipmentContext 在 Awake 内联构建,Inspector 不可见
|
||||
|
||||
```csharp
|
||||
private void Awake()
|
||||
{
|
||||
_ctx = new EquipmentContext
|
||||
{
|
||||
Stats = GetComponent<PlayerStats>(),
|
||||
Feedback = GetComponent<PlayerFeedback>(),
|
||||
Events = EventChannelRegistry.Instance,
|
||||
SkillMods = GetComponent<SkillModifierRegistry>(),
|
||||
WeaponMgr = GetComponent<WeaponManager>(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
若某组件缺失(如 `PlayerFeedback` 未挂载),`_ctx.Feedback` 为 null,护符效果 `OnEquip` 调用 `ctx.Feedback` 时静默失败。建议在 Awake 末尾加 `Debug.Assert`:
|
||||
|
||||
```csharp
|
||||
Debug.Assert(_ctx.Stats != null, "[EquipmentManager] 缺少 PlayerStats", this);
|
||||
Debug.Assert(_ctx.Feedback != null, "[EquipmentManager] 缺少 PlayerFeedback", this);
|
||||
Debug.Assert(_ctx.SkillMods != null, "[EquipmentManager] 缺少 SkillModifiers", this);
|
||||
```
|
||||
|
||||
#### P2:PostProcessManager `[SerializeField] Component _bossArenaVolume`
|
||||
|
||||
用 `Component` 基类接收 Volume 组件,无类型约束。设计师可能误拖 `BoxCollider2D`,直到运行时才报错。
|
||||
|
||||
---
|
||||
|
||||
## 6. 使用便利性(DX)深度评审
|
||||
|
||||
### 6.1 ✅ EquipmentManager:返回错误字符串替代 bool+out
|
||||
|
||||
```csharp
|
||||
/// <summary>返回 null 表示成功;返回错误字符串表示失败原因。</summary>
|
||||
public string TryEquipCharm(CharmSO charm)
|
||||
{
|
||||
if (charm == null) return "护符不存在";
|
||||
if (_equipped.Contains(charm)) return "已经装备";
|
||||
if (!_collected.Contains(charm)) return "尚未收集此护符";
|
||||
int remaining = _currentNotchCapacity - UsedNotches;
|
||||
if (charm.notchCost > remaining) return $"笔记不足(需要 {charm.notchCost},剩余 {remaining})";
|
||||
...
|
||||
return null; // 成功
|
||||
}
|
||||
```
|
||||
|
||||
调用方可直接将错误字符串显示给用户,不需要额外的错误码枚举。设计简洁,优于 `bool TryEquip(..., out string error)`。
|
||||
|
||||
### 6.2 ✅ Projectile 基类:Initialize + 模板方法 OnInitialized
|
||||
|
||||
```csharp
|
||||
// 基类处理通用初始化(池引用、HitBox 激活、计时器)
|
||||
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction)
|
||||
{
|
||||
_config = config;
|
||||
DamageInfo = damageInfo;
|
||||
Direction = direction.normalized;
|
||||
_aliveTimer = 0f;
|
||||
_hitBox.Activate(config.DamageSource);
|
||||
OnInitialized(); // 钩子:子类设定初速度
|
||||
}
|
||||
protected virtual void OnInitialized() { }
|
||||
```
|
||||
|
||||
子类只需重写 `OnInitialized()` 设置 Rigidbody 速度,其余公共逻辑由基类统一处理。`LinearProjectile` / `HomingProjectile` / `ArcProjectile` 各自 ≤ 20 行。
|
||||
|
||||
### 6.3 ✅ 异步/协程异常全部包裹
|
||||
|
||||
```csharp
|
||||
// QuickSave/QuickLoad 调用点无法 await,用包裹器捕获异常
|
||||
private static async void RunFireAndForget(Task task, string context)
|
||||
{
|
||||
try { await task; }
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[SaveManager] {context} 失败: {e.Message}\n{e.StackTrace}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有 `async void` 入口(QuickSave / QuickLoad)都经过 `RunFireAndForget` 包裹,异常不会静默吞掉。
|
||||
|
||||
### 6.4 ✅ QuestManager:完整 ISaveable + IReadOnlyDictionary 状态只读视图
|
||||
|
||||
```csharp
|
||||
public IReadOnlyDictionary<string, QuestStateEnum> QuestStates => _questStates;
|
||||
```
|
||||
|
||||
外部查询任务状态走 `IReadOnlyDictionary`,无法意外写入内部状态。`SaveManager` 持有 `ISaveable` 接口引用,不直接依赖 `QuestManager` 类型。
|
||||
|
||||
### 6.5 ⚠️ 剩余 DX 问题
|
||||
|
||||
#### P1:GameManager.RegisterStates 硬编码 9 个状态
|
||||
|
||||
```csharp
|
||||
private void RegisterStates()
|
||||
{
|
||||
_fsm.Register(new InitializingState());
|
||||
_fsm.Register(new MainMenuState());
|
||||
// ... 9 个
|
||||
}
|
||||
```
|
||||
|
||||
每次添加新游戏状态都需要修改 `GameManager.cs`。游戏状态类实现了 `IGameState`,可通过反射或工厂模式自动注册(`IGameStateFactory` 接口已存在):
|
||||
|
||||
```csharp
|
||||
// IGameStateFactory.cs 已定义,但 GameManager 未使用
|
||||
foreach (var factory in GetComponents<IGameStateFactory>())
|
||||
_fsm.Register(factory.Create());
|
||||
```
|
||||
|
||||
#### P2:DeathRespawnService 复活流程等待方式混用
|
||||
|
||||
```csharp
|
||||
// 在 Coroutine 中轮询 bool flag(_deathConfirmed)
|
||||
yield return new WaitUntil(() => _deathConfirmed);
|
||||
```
|
||||
|
||||
通过轮询 bool 等待玩家确认,而不是直接订阅确认事件的回调。在 Coroutine 中 `WaitUntil` 每帧检查一次,虽然性能可接受,但意图不如直接回调清晰。
|
||||
|
||||
---
|
||||
|
||||
## 7. 子系统逐一点评
|
||||
|
||||
| 子系统 | 质量评级 | 亮点 | 注意点 |
|
||||
|---|---|---|---|
|
||||
| **GameStateMachine** | ⭐⭐⭐⭐⭐ | 合法转换验证 + 错误上报 | 状态注册仍手动 |
|
||||
| **事件频道** | ⭐⭐⭐⭐⭐ | SO + C# event 双层 + 可组合订阅句柄 + EventBusMonitor | - |
|
||||
| **ServiceLocator** | ⭐⭐⭐⭐⭐ | 接口注册 + 测试 Override/Reset | 与 static Instance 并存(迁移中) |
|
||||
| **SaveManager** | ⭐⭐⭐⭐ | HMAC校验 + 版本迁移 + fire-and-forget 包裹 | Data 属性已标 Obsolete,待删除 |
|
||||
| **HurtBox** | ⭐⭐⭐⭐ | 8 步流水线完整 + 接口注入 + 缓存 | 依赖注入不可见于 Inspector |
|
||||
| **DamageInfo** | ⭐⭐⭐⭐⭐ | 双路径工厂(零 GC + Builder) | - |
|
||||
| **PlayerController** | ⭐⭐⭐⭐ | POCO FSM + 类型字典 O(1) + SwimState 已注册 | 仍有 ~14 个 SerializeField |
|
||||
| **AttackState** | ⭐⭐⭐⭐ | 连击数据驱动 + Animancer 事件驱动 HitBox | - |
|
||||
| **PlayerMovement** | ⭐⭐⭐⭐ | flipX 朝向已修复 | `_spriteRenderer` 需 Inspector 赋值或 GetComponentInChildren |
|
||||
| **EnemyBase** | ⭐⭐⭐ | 事件频道获取玩家 Transform + Start 兜底 | ForceState 动画已修复;状态机仍是枚举级别 |
|
||||
| **GlobalObjectPool** | ⭐⭐⭐⭐ | LinkedList LRU + Addressables 预热 + 双版本桥接 | 双版本预热逻辑有重复 |
|
||||
| **StatusEffectManager** | ⭐⭐⭐⭐⭐ | 双结构 + MaterialPropertyBlock + 逆序遍历 | - |
|
||||
| **EquipmentManager** | ⭐⭐⭐⭐ | ICharmEffect 接口 + EquipmentContext + ISaveable | _ctx 依赖无 Assert 保护 |
|
||||
| **SkillManager** | ⭐⭐⭐ | 修改器注册表 + FormSkillSO 数据驱动 | 三个硬编码槽 |
|
||||
| **QuestManager** | ⭐⭐⭐⭐ | 分支任务 + ISaveable + IReadOnlyDictionary | EnemyDied 事件类型不匹配 |
|
||||
| **WorldStateRegistry** | ⭐⭐⭐ | SO + 跨场景共享 + ISaveable | 每类实体独立字段,扩展性差 |
|
||||
| **AudioManager** | ⭐⭐⭐⭐ | BGM 交叉淡入 + SFX 轮转池 + 旧 API 已标 Obsolete | Phase 2 实现尚未完整 |
|
||||
| **EventChainManager** | ⭐⭐⭐⭐⭐ | SO 事件 → C# 事件桥接 + Editor 专用日志 | - |
|
||||
| **SOValidationRunner** | ⭐⭐⭐⭐⭐ | 构建前校验 + MenuItem 一键校验 | - |
|
||||
|
||||
---
|
||||
|
||||
## 8. 残留问题清单
|
||||
|
||||
> 以下为本轮修复后仍存在的问题,按优先级排序。
|
||||
|
||||
| # | 优先级 | 模块 | 问题描述 | 建议修复方式 |
|
||||
|---|--------|------|----------|------------|
|
||||
| 1 | **P1** | GameManager / QuestManager / MapManager | `static Instance` 与 ServiceLocator 并存 | 实现接口 + ServiceLocator 注册,保留 Instance 作转发 |
|
||||
| 2 | **P1** | QuestManager | `_onEnemyDied` 用 `TransformEventChannelSO` 携带 Transform | 改用 `EnemyDeathData { string EnemyId; Transform Source; }` 事件 |
|
||||
| 3 | **P1** | GameManager | `RegisterStates()` 硬编码,未使用已存在的 `IGameStateFactory` 接口 | 通过工厂接口或配置自动注册 |
|
||||
| 4 | **P1** | WorldStateRegistry | 5 类状态独立字段,扩展性差 | 改为 `Dictionary<WorldObjectCategory, HashSet<string>>` |
|
||||
| 5 | **P1** | SkillManager | 三技能槽硬编码 | `Dictionary<FormSkillSO, float>` 动态冷却表 |
|
||||
| 6 | **P2** | EquipmentManager | `EquipmentContext` 依赖无 `Debug.Assert` 保护 | Awake 末尾加断言 |
|
||||
| 7 | **P2** | PostProcessManager | `[SerializeField] Component _bossArenaVolume` 无类型约束 | 改为 `Volume` 具体类型 |
|
||||
| 8 | **P2** | DeathRespawnService | `WaitUntil(() => _deathConfirmed)` 轮询模式 | 改为直接 TaskCompletionSource 或事件回调 |
|
||||
| 9 | **P2** | GlobalObjectPool | `WarmupCoroutine` / `WarmupAsync` 逻辑重复 | 统一 async Task,提供 Coroutine 桥接扩展方法 |
|
||||
| 10 | **P3** | SaveManager | `[Obsolete] Data` 属性仍存在 | 确认调用方全部迁移后删除 |
|
||||
| 11 | **P3** | RoomController | `CameraStateController.Instance?.SwitchRoom()` 使用 static Instance | 改用 ServiceLocator 或事件频道 |
|
||||
| 12 | **P3** | WorldStateRegistry | 无变更通知机制 | 添加 `event Action<WorldObjectCategory, string> OnStateChanged` |
|
||||
|
||||
---
|
||||
|
||||
## 9. 后续迭代路线图
|
||||
|
||||
### 第一优先级(1-2 周,不影响功能开发)
|
||||
|
||||
```
|
||||
① WorldStateRegistry 泛化 API
|
||||
enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag }
|
||||
Dictionary<WorldObjectCategory, HashSet<string>> _states
|
||||
|
||||
② SkillManager 动态技能槽
|
||||
Dictionary<FormSkillSO, float> _cooldowns
|
||||
|
||||
③ EquipmentManager.Awake Debug.Assert 保护
|
||||
// 4 行断言,30 分钟内完成
|
||||
|
||||
④ GlobalObjectPool 统一 WarmupAsync,提供 AsCoroutine() 桥接
|
||||
```
|
||||
|
||||
### 第二优先级(2-4 周,架构级改进)
|
||||
|
||||
```
|
||||
⑤ GameManager / QuestManager / MapManager 统一服务定位模式
|
||||
实现 IQuestService / IMapService 接口 → ServiceLocator 注册
|
||||
static Instance 改为 ServiceLocator.GetOrDefault<IXXXService>()
|
||||
|
||||
⑥ 利用 IGameStateFactory 接口自动注册游戏状态
|
||||
GameManager.RegisterStates() 改为工厂驱动
|
||||
|
||||
⑦ QuestManager._onEnemyDied 改用 EnemyDeathData 事件类型
|
||||
```
|
||||
|
||||
### 第三优先级(长期,影响内容管线)
|
||||
|
||||
```
|
||||
⑧ EnemyBase 状态机升级
|
||||
完全依赖 Behavior Designer 行为树,废弃 EnemyStateType 枚举
|
||||
EnemyBase 成为纯数据持有者 + 生命周期桥接器
|
||||
|
||||
⑨ 单元测试接入(ServiceLocator 已支持 OverrideForTest)
|
||||
SaveMigrator.Migrate 各版本路径
|
||||
StatusEffectManager 堆叠/净化逻辑
|
||||
QuestManager 目标进度计算
|
||||
HurtBox 8 步流水线各分支
|
||||
|
||||
⑩ 护符效果数据管线自动化
|
||||
ICharmEffect.Validate() 接入 SOValidationRunner
|
||||
CharmSO.effects[] 空引用在构建时捕获
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附:三轮修复全记录
|
||||
|
||||
| 轮次 | 问题 | 文件 | 状态 |
|
||||
|------|------|------|------|
|
||||
| 第一轮 P0 | SaveManager HMAC 跨设备失效 | SaveManager.cs | ✅ |
|
||||
| 第一轮 P0 | QuickSave 异步异常静默吞掉 | SaveManager.cs | ✅ |
|
||||
| 第一轮 P1 | HurtBox 每次受击 GetComponent | HurtBox.cs | ✅ |
|
||||
| 第一轮 P1 | EnemyBase FindWithTag O(n) | EnemyBase.cs | ✅ |
|
||||
| 第一轮 P1 | EnemyStats DistanceToPlayer 未平方 | EnemyStats.cs | ✅ |
|
||||
| 第一轮 P1 | PlayerController Resources.FindObjectsOfTypeAll | PlayerController.cs | ✅ |
|
||||
| 第一轮 P1 | PlayerController _dependenciesReady 每帧重计算 | PlayerController.cs | ✅ |
|
||||
| 第一轮 P2 | AttackState 连击硬编码 3 段 | AttackState.cs | ✅ |
|
||||
| 第一轮 P2 | HurtState 硬编码 0.4s 硬直 | HurtState.cs | ✅ |
|
||||
| 第一轮 P2 | GlobalObjectPool List O(n) LRU | GlobalObjectPool.cs | ✅ |
|
||||
| 第二轮 Bug | EnemyBase Awake 订阅未调用 | EnemyBase.cs | ✅ |
|
||||
| 第三轮 P1 | PlayerMovement localScale 朝向 | PlayerMovement.cs | ✅ |
|
||||
| 第三轮 P1 | SwimState 未注册 | PlayerController.cs | ✅ |
|
||||
| 第三轮 P1 | EnemyBase.ForceState() 空实现 | EnemyBase.cs | ✅ |
|
||||
| 第三轮 P1 | SaveManager.Data 暴露内部状态 | SaveManager.cs + 2 调用方 | ✅ |
|
||||
| 第三轮 P1 | GameServiceRegistrar 全场景扫描 | GameServiceRegistrar.cs | ✅ |
|
||||
| 第三轮 P1 | PlayerController [RequireComponent] 缺失 | PlayerController.cs | ✅ |
|
||||
| 第三轮 P3 | PlatformBootstrap 每帧 ServiceLocator 查找 | PlatformBootstrap.cs | ✅ |
|
||||
| 第三轮 P2 | UIManager 丢弃 shopId | UIManager.cs | ✅ |
|
||||
| 第三轮 P3 | LocalizationManager 异常静默 | LocalizationManager.cs | ✅ |
|
||||
|
||||
---
|
||||
|
||||
*本文档反映当前(2026-05-11)所有已实施修复后的代码状态。如需深入讨论任何具体问题的实施方案,可继续追问。*
|
||||
Reference in New Issue
Block a user