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

529 lines
20 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.
# BaseGames Framework — 全面代码评审
> 评审时间2026-05-12
> 评审范围:`Assets/Scripts/` 全目录
> 评审标准:成熟商业动作 RPG 框架Unity 2022.3 LTS / C#
> 框架定位:新框架,无需向后兼容,追求纯净、统一、无历史残留
---
## 目录
1. [总体评分](#1-总体评分)
2. [架构设计](#2-架构设计)
3. [性能](#3-性能)
4. [可扩展性](#4-可扩展性)
5. [编辑器友好性](#5-编辑器友好性)
6. [使用便利性](#6-使用便利性)
7. [问题清单(优先级排序)](#7-问题清单优先级排序)
8. [修复方案](#8-修复方案)
9. [综合结论](#9-综合结论)
---
## 1. 总体评分
| 维度 | 评分 | 说明 |
|------------------|----------|-------------------------------|
| 架构设计 | ★★★★☆ | 结构清晰,少量不一致待修复 |
| 性能 | ★★★★☆ | 热路径优化良好,若干小 GC 点待处理 |
| 可扩展性 | ★★★★☆ | SO 驱动设计优秀,接口覆盖可再完善 |
| 编辑器友好性 | ★★★★★ | 工具链完备,超出同类商业框架水平 |
| 使用便利性 | ★★★★☆ | 模式统一,极少数订阅方式需对齐 |
---
## 2. 架构设计
### 2.1 整体架构评价 ✅ 优秀
框架采用多层解耦架构,核心设计如下:
**层次清晰,程序集边界合理**
```
Core.Events ← 最低层(无任何游戏依赖)
Core / Core.Save ← 服务层
Combat / Player / Enemies / Audio ... ← 游戏系统层
UI / World / Equipment ... ← 表现/业务层
Editor ← 纯编辑器工具(运行时不可见)
```
每个程序集通过 `.asmdef` 显式声明依赖,`BaseGames.Core.Events` 是纯净基础层,无对任何游戏程序集的引用。程序集结构完全符合 Unity 最佳实践。
---
### 2.2 事件系统 ✅ 商业级
**ScriptableObject 事件频道EventChannel** 是框架通信的统一机制:
```csharp
// 订阅RAII 模式97% 的文件已全面迁移)
_channel?.Subscribe(Handler).AddTo(_subs);
// 广播(零耦合,跨程序集无问题)
_channel?.Raise(payload);
```
**`CompositeDisposable` + `EventSubscription`** 实现了 Rx.js/UniRx 风格的生命周期安全订阅,属于行业领先实践:
- `EventSubscription``readonly struct`,零堆分配
- `CompositeDisposable` 批量管理OnDisable 一行清空,不可能泄漏
**`EventBusMonitor`** 固定大小环形缓冲区256条Editor 下零 GC 记录所有事件调用,订阅者计数精确,这在商业框架中都属罕见的优质工具。
---
### 2.3 服务定位器 ✅ 良好
`ServiceLocator` 轻量、类型安全,支持接口类型注册(依赖倒置):
- `Register<IInterface>(impl)` — 标准注册
- `GetOrDefault<T>()` — 安全获取,不抛异常
- `Unregister<T>(impl)` — 防止场景切换时旧实例残留(安全版重载是亮点)
- `OverrideForTest<T>` / `Reset()` — Editor 条件编译的测试支持
**GameServiceRegistrar** 注册顺序由 `[DefaultExecutionOrder(-2000)]` 保证最早执行,且仅负责统一注册,不做业务逻辑,职责单一。
⚠️ **问题 2-A**`HitStopManager` 迁移至 ServiceLocator 后以具体类型注册,无 `IHitStopManager` 接口,导致 `ClashResolver` 对具体实现类产生依赖,降低可测试性。
---
### 2.4 存档系统 ✅ 设计优秀
**三层存档架构:**
```
SaveManager协调层
ISaveStorage接口→ LocalFileStorage实现
SaveData数据层→ JSON via Newtonsoft.Json
```
亮点:
- **原子写入**`.tmp``File.Replace``.bak`,断电安全
- **HMAC-SHA256 校验和**:防止存档被篡改,校验失败时仍允许加载(仅警告)
- **`[JsonExtensionData]`**未知字段保留DLC 扩展数据隔离,优雅的前向兼容
- **异步 I/O + SemaphoreSlim**:并发存档请求串行化,无数据竞争
- **`CrashReporter`**:异常退出时同步写入崩溃日志 + 触发紧急存档槽(异步不可靠时的正确降级)
- **`ISaveable`接口 + `SaveableMonoBehaviour`基类**:组件自动注册/注销,消除样板代码
⚠️ **问题 2-B**`SaveManager.LastCheckpointScene``LastCheckpointSpawnId``public static` 字段,破坏了框架的实例化服务模型。`DeathRespawnService``AntiSoftlockSystem` 通过 `SaveManager.LastCheckpoint*` 静态访问绕过了 ServiceLocator。
⚠️ **问题 2-C**`SaveMigrator.CurrentVersion = "1.0"``SaveMeta.Version = "2.1"` 不一致,每次加载存档都会触发警告,且 `Migrate()` 内无实际迁移逻辑,等同于空实现。
---
### 2.5 战斗系统 ✅ 架构精良
**8 步伤害流水线**`HurtBox.ReceiveDamage`
```
① 无敌帧检查
② 弹反检查ParrySystem跨程序集接口隔离
③ 霸体检查IPoiseSource
④ 护盾拦截IShieldable玩家专属
⑤ 防御减免(最低 1 点)
⑥ TakeDamageIDamageable
⑦ 全局事件广播
⑧ 状态效果触发IStatusEffectable
```
所有步骤通过接口隔离,零直接类型依赖,高度符合开闭原则。
**`DamageInfo`** 设计优雅:
- `struct` 值类型,热路径零堆分配
- `Builder` 模式支持复杂构造
- `DamageInfo.From(DamageSourceSO, ...)` 静态工厂方法覆盖 90% 使用场景
---
### 2.6 玩家状态机 ✅ 结构清晰
`PlayerController` 持有状态字典,所有状态继承 `PlayerStateBase`,通过 `TryTransitionState()` 驱动切换。`AttackState` 中连击动画时间点由 `PlayerAnimationConfigSO` 配置,无硬编码,是优质的数据驱动设计。
---
### 2.7 残留设计不一致(需修复)
**⚠️ 问题 2-D**`GameManager.OnEnable/OnDisable` 仍使用旧式 `OnEventRaised +=/-=` 模式,是框架内唯一遗留的旧式订阅,在所有 MonoBehaviour 已完成 RAII 迁移后显得格外突出。
```csharp
// GameManager.cs当前——旧格式
private void OnEnable()
{
if (_onPlayerDied) _onPlayerDied.OnEventRaised += HandlePlayerDied;
// ...
}
// 应统一为
private readonly CompositeDisposable _subs = new();
private void OnEnable() => _onPlayerDied?.Subscribe(HandlePlayerDied).AddTo(_subs);
private void OnDisable() => _subs.Clear();
```
---
## 3. 性能
### 3.1 热路径优化 ✅ 优秀
| 机制 | 优化方式 |
|------|---------|
| `DamageInfo.From()` | 栈分配 struct零 GC |
| `EventSubscription` | `readonly struct`,零 GC |
| `EventBusMonitor` | 固定大小环形缓冲区Editor 内零 GC |
| `HitBox` 命中去重 | HashSet/Dictionary 缓存,避免重复伤害逻辑 |
| `SkillManager.Update` | `FormSkillSO[]` 快照数组无分配遍历 |
| `GlobalObjectPool` | Addressables 异步预热Spawn 无实例化开销 |
| `WorldStateRegistry` | `HashSet<string>` O(1) 查询ScriptableObject 常驻内存 |
### 3.2 需优化点
**⚠️ 问题 3-A**`AudioManager.PlaySFX(string key)` 使用 `foreach` 线性扫描 `_sfxRegistry` 数组O(n)。当 SFX 条目多时50+条)每帧高频调用时有感知延迟。
```csharp
// 当前O(n) 线性扫描
foreach (var entry in _sfxRegistry)
if (entry.Key == key) ...
// 建议Awake 时构建 Dictionary<string, AudioEventSO>O(1) 查询
private Dictionary<string, AudioEventSO> _sfxLookup;
private void Awake() {
_sfxLookup = new Dictionary<string, AudioEventSO>(_sfxRegistry.Length);
foreach (var e in _sfxRegistry) _sfxLookup[e.Key] = e.Event;
}
```
**⚠️ 问题 3-B**`SkillManager.UpdateSkillSet()` 每次切换形态都创建 `new List<FormSkillSO>(3)``ToArray()`,产生两次 GC 分配。形态切换发生频率低,影响有限,但有更干净的写法:
```csharp
// 建议:固定长度数组,避免 List + ToArray
private readonly FormSkillSO[] _activeSkills = new FormSkillSO[3];
private int _activeSkillCount;
```
**⚠️ 问题 3-C**`HitBox``_hitThisActivation`HashSet`_hitCooldownTimers`Dictionary<Collider2D, float>)在每次 `Activate/Deactivate` 时调用 `Clear()` 而不是预分配容量后复用,多次激活/停用时会引发字典内部数组的 GC。建议在初始化时预设 capacity
```csharp
private readonly HashSet<Collider2D> _hitThisActivation = new(8);
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new(8);
```
**⚠️ 问题 3-D**`SaveManager._saveables` 使用 `List<ISaveable>`,每次 `Unregister` 是 O(n) 线性搜索。存档对象通常 < 30 个,实际无影响,记录仅作完整性参考。
---
## 4. 可扩展性
### 4.1 ScriptableObject 驱动架构 ✅ 商业顶级
整个框架数据层由 SO 驱动,新增功能只需:
1. 创建新 SO 资产
2. 在 Inspector 绑定
3. 无需修改现有代码
典型示范:
- **护符系统**`ICharmEffect` 接口 + `CharmSO.effects[]` → 新增护符效果只需实现接口并创建资产,完美开闭原则
- **技能系统**`FormSkillSO` 数据 + `SkillManager` 执行 → 形态技能由配置决定
- **Boss 系统**`BossSkillSO` + `SkillSequenceSO` + `AttackPatternSO` 三层 → 纯数据驱动 Boss 行为设计
### 4.2 EventChannel 扩展 ✅ 无限扩展
新增事件类型仅需一行:
```csharp
[CreateAssetMenu(menuName = "Events/MyType")]
public class MyTypeEventChannelSO : BaseEventChannelSO<MyType> { }
```
`VoidBaseEventChannelSO``BaseEventChannelSO<T>` 两个基类覆盖全部需求。
### 4.3 存档扩展 ✅ 支持 DLC
`SaveData.DLC = new Dictionary<string, JObject>()` 专用字段 + `[JsonExtensionData]` 未知字段保留,支持 DLC 在不修改主存档结构的前提下扩展数据。`SaveMigrator` 架构(现虽为空)提供了版本升级路径。
### 4.4 接口覆盖不完整
**⚠️ 问题 4-A**`HitStopManager` 以具体类注册无接口抽象。ServiceLocator 使用的服务应尽量对应接口类型:
```csharp
// 建议:定义接口并注册
public interface IHitStopService {
void FreezeFrames(int frames);
void FreezeDuration(float seconds);
float BaseTimeScale { get; set; }
}
ServiceLocator.Register<IHitStopService>(this);
```
**⚠️ 问题 4-B**`DialogueManager` 直接以具体类注册到 ServiceLocator而框架中 `IDialogueService` 接口未定义TutorialManager 也类似。如未来需要替换对话系统,需修改所有调用方。
---
## 5. 编辑器友好性
### 5.1 工具链 ✅ 超出商业标准
| 工具 | 功能 |
|------|------|
| **EventBusMonitorWindow** | 实时监控所有 SO 事件调用、payload、订阅者数、帧号过滤搜索自动滚动 |
| **SceneScaffoldTools** | 一键生成 Persistent 场景完整 GameObject 层级 + 自动绑定已知资产引用 |
| **EventChainEditorWindow** | 可视化事件链编辑器 |
| **BossSkillSequenceWindow** | Boss 技能序列可视化 |
| **CreateEventChannelAssets** | 批量创建 EventChannel SO 资产 |
| **AddressReferenceGraphWindow** | Addressables 引用关系图 |
| **NavSurfaceBakeShortcut** | 快捷 NavSurface Bake |
| **ScriptExecutionOrderTools** | 执行顺序管理工具 |
| **ValidationSystem** | `IValidatable` 接口 + 批量校验 |
| **Editor/Combat / Equipment / World** | 各领域专属编辑器 Inspector 扩展 |
`SceneScaffoldTools` 能自动查找资产(通过名称模式匹配)并通过反射自动绑定字段引用,这一功能在独立游戏工具链中属罕见的高完成度实现。
### 5.2 运行时调试支持 ✅ 良好
- `HurtBox``OnDrawGizmos()` 可视化受击盒状态(激活/无敌/非激活三种颜色)
- `HitBox` 中 Awake 对 `IsTrigger` 做运行时验证并日志警告
- 所有关键 `[DefaultExecutionOrder]` 有文档注释说明原因
- `PlayerController``#if UNITY_EDITOR [SerializeField] private bool _debugValidateTransitions`
### 5.3 小问题
**⚠️ 问题 5-A**`EventChannelRegistry.Awake()` 自己调用 `DontDestroyOnLoad(transform.root.gameObject)`,但 `GameServiceRegistrar` 已经负责 Persistent 场景 Root GameObject 的生命周期管理。两处 DDOL 可能导致场景层级重复,应由 `GameServiceRegistrar` 统一管理,`EventChannelRegistry` 删除 DDOL 调用。
---
## 6. 使用便利性
### 6.1 统一的服务访问模式 ✅
```csharp
// 全框架统一ServiceLocator.GetOrDefault<T>()
var saveManager = ServiceLocator.GetOrDefault<SaveManager>();
var questManager = ServiceLocator.GetOrDefault<IQuestManager>();
var audioService = ServiceLocator.GetOrDefault<IAudioService>();
```
无 Singleton.Instance 混用,框架内服务访问路径唯一。
### 6.2 统一的事件订阅模式 ✅95% 完成)
```csharp
// 全框架统一 RAII 模式
private readonly CompositeDisposable _subs = new();
private void OnEnable() => _channel?.Subscribe(Handler).AddTo(_subs);
private void OnDisable() => _subs.Clear();
```
**⚠️ 问题 6-A已识别**`GameManager.cs` 是框架内唯一未完成 RAII 迁移的 MonoBehaviour使用旧式 `OnEventRaised +=/-=`(见问题 2-D
### 6.3 Input 事件混用
框架中存在两套事件订阅机制:
1. **EventChannelSO**`_channel?.Subscribe(H).AddTo(_subs)` — 框架标准
2. **C# 原生 event**`_inputReader.AttackEvent += Handler` — InputReaderSO 和各 State 使用
**这是合理的混用,而非缺陷。** InputReaderSO 的 `event Action` 不需要跨程序集 SO 资产,是 Unity Input System 和状态机配合的常规写法。SkillManager、PlayerController.States 等使用 `event +=/-=` 是正确选择。**无需统一为 EventChannel保持现状。**
### 6.4 `Debug.Assert` 统一用法 ✅
关键组件在 Awake 中用 `Debug.Assert` 验证 Inspector 引用,开发期快速发现配置错误,不会在 Release 版本执行:
```csharp
Debug.Assert(_config != null, "[PlayerStats] _config 未赋值,请在 Inspector 中指定 PlayerStatsSO。", this);
```
---
## 7. 问题清单(优先级排序)
### 🔴 高优先级(影响框架一致性/正确性)
| # | 文件 | 问题描述 |
|---|------|---------|
| H-1 | `Core/GameManager.cs` | `OnEnable/OnDisable` 仍用旧式 `OnEventRaised +=/-=`,框架内唯一残留,破坏事件订阅统一性 |
| H-2 | `Core/Save/SaveMigrator.cs` | `CurrentVersion = "1.0"``SaveMeta.Version = "2.1"` 不一致,每次加载都触发无意义警告 |
### 🟡 中优先级(影响架构纯净度)
| # | 文件 | 问题描述 |
|---|------|---------|
| M-1 | `Core/Save/SaveManager.cs` | `LastCheckpointScene``LastCheckpointSpawnId``public static`,破坏实例化服务模型,应改为实例属性 |
| M-2 | `Combat/HitStopManager.cs` | 无 `IHitStopService` 接口,直接注册具体类,可测试性受限 |
| M-3 | `Core/Events/EventChannelRegistry.cs` | `DontDestroyOnLoad` 应由 `GameServiceRegistrar` 统一管理,此处重复 |
### 🟢 低优先级(性能/代码质量小改进)
| # | 文件 | 问题描述 |
|---|------|---------|
| L-1 | `Audio/AudioManager.cs` | `PlaySFX` 线性扫描 `_sfxRegistry`,应在 Awake 构建 `Dictionary` |
| L-2 | `Player/SkillManager.cs` | `UpdateSkillSet` 每次 `new List + ToArray`,应用固定数组 |
| L-3 | `Combat/HitBox.cs` | `_hitThisActivation` / `_hitCooldownTimers` 未预设 capacity多次 Clear 后再 Add 可能触发扩容 |
| L-4 | `Core/GameIds.cs` | 待确认框架中 GameId 字符串常量是否已统一使用此文件(防止硬编码字符串散落各处) |
---
## 8. 修复方案
### Fix H-1GameManager 迁移至 RAII
```csharp
// 添加字段
private readonly CompositeDisposable _subs = new();
// 替换 OnEnable
private void OnEnable()
{
_onPlayerDied? .Subscribe(HandlePlayerDied).AddTo(_subs);
_onPauseRequested? .Subscribe(HandlePauseRequested).AddTo(_subs);
_onResumeRequested? .Subscribe(HandleResumeRequested).AddTo(_subs);
_onBossFightStarted? .Subscribe(HandleBossFightStarted).AddTo(_subs);
_onBossFightEnded? .Subscribe(HandleBossFightEnded).AddTo(_subs);
_onDeathScreenConfirmed?.Subscribe(HandleDeathScreenConfirmed).AddTo(_subs);
}
// 替换 OnDisable
private void OnDisable() => _subs.Clear();
// 删除 _deathScreenConfirmed bool 字段DeathRespawnService 已有局部订阅方案)
```
### Fix H-2SaveMigrator 版本对齐
```csharp
public static class SaveMigrator
{
// 与 SaveMeta.Version 对齐
public const string CurrentVersion = "2.1";
public static SaveData Migrate(SaveData data)
{
if (data?.Meta == null) return data;
// 实际迁移分支(示意)
if (data.Meta.Version == "1.0") MigrateFrom_1_0(data);
if (data.Meta.Version == "2.0") MigrateFrom_2_0(data);
data.Meta.Version = CurrentVersion;
return data;
}
private static void MigrateFrom_1_0(SaveData data) { /* 1.0 → 2.x 迁移逻辑 */ }
private static void MigrateFrom_2_0(SaveData data) { /* 2.0 → 2.1 迁移逻辑 */ }
}
```
### Fix M-1SaveManager 静态字段迁移为实例属性
```csharp
// SaveManager.cs — 删除 static改为实例属性
public string LastCheckpointScene { get; private set; }
public string LastCheckpointSpawnId { get; private set; }
```
```csharp
// DeathRespawnService.cs — 通过 ServiceLocator 获取
var sm = ServiceLocator.GetOrDefault<SaveManager>();
_onSceneLoadRequest?.Raise(new SceneLoadRequest
{
SceneName = sm?.LastCheckpointScene,
EntryTransitionId = sm?.LastCheckpointSpawnId,
// ...
});
```
### Fix M-2HitStopManager 添加接口
```csharp
// 新增接口(放在 Combat 程序集)
public interface IHitStopService
{
void FreezeFrames(int frames);
void FreezeDuration(float unscaledSeconds);
float BaseTimeScale { get; set; }
}
// HitStopManager 实现接口
public class HitStopManager : MonoBehaviour, IHitStopService
{
private void Awake()
{
if (ServiceLocator.GetOrDefault<IHitStopService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<IHitStopService>(this);
}
private void OnDestroy()
{
Time.timeScale = _baseTimeScale;
ServiceLocator.Unregister<IHitStopService>(this);
}
}
// ClashResolver.cs
ServiceLocator.GetOrDefault<IHitStopService>()?.FreezeFrames(...);
```
### Fix M-3EventChannelRegistry 移除 DontDestroyOnLoad
```csharp
// EventChannelRegistry.Awake() — 删除以下行
// DontDestroyOnLoad(transform.root.gameObject); ← 删除
```
### Fix L-1AudioManager SFX 查找优化
```csharp
private Dictionary<string, AudioEventSO> _sfxLookup;
private void Awake()
{
// ... 其他初始化
_sfxLookup = new Dictionary<string, AudioEventSO>(_sfxRegistry?.Length ?? 0);
if (_sfxRegistry != null)
foreach (var entry in _sfxRegistry)
if (!string.IsNullOrEmpty(entry.Key) && entry.Event != null)
_sfxLookup[entry.Key] = entry.Event;
}
public void PlaySFX(string key)
{
if (!_sfxLookup.TryGetValue(key, out var evt))
{
Debug.LogWarning($"[AudioManager] SFX key '{key}' 未注册。");
return;
}
PlayAudioEvent(evt);
}
```
---
## 9. 综合结论
### 框架总体水平
本框架的架构质量**达到商业独立 AA 游戏标准**,在以下方面有突出表现:
1. **事件系统**SO 频道 + RAII CompositeDisposable 的组合,在 Unity 生态中属顶层实践
2. **战斗流水线**`HurtBox` 8 步流水线接口隔离完整,扩展无需修改现有代码
3. **存档系统**:原子写入 + HMAC 校验 + DLC 扩展字段,工程化程度高于大多数 AAA 之外的游戏
4. **数据驱动**SO 驱动护符、技能、Boss、道具内容迭代不触及代码
5. **编辑器工具链**EventBusMonitor + SceneScaffoldTools + 多个专域编辑器窗口,超出同体量框架标准
### 待解决的核心问题
| 优先级 | 数量 | 说明 |
|--------|------|------|
| 🔴 高 | 2 | GameManager 旧式事件订阅SaveMigrator 版本不一致 |
| 🟡 中 | 3 | SaveManager 静态字段HitStopManager 缺接口EventChannelRegistry 重复 DDOL |
| 🟢 低 | 4 | AudioManager O(n) 查找SkillManager GCHitBox 容量GameIds 统一性 |
解决以上 9 个问题后,框架代码质量可达到**完全无历史残留、统一机制、商业可发布标准**。
---
*本评审基于源码静态分析,未涵盖运行时 Profiler 数据和平台适配专项测试。*