20 KiB
BaseGames Framework — 全面代码评审
评审时间:2026-05-12
评审范围:Assets/Scripts/全目录
评审标准:成熟商业动作 RPG 框架(Unity 2022.3 LTS / C#)
框架定位:新框架,无需向后兼容,追求纯净、统一、无历史残留
目录
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) 是框架通信的统一机制:
// 订阅(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 点)
⑥ TakeDamage(IDamageable)
⑦ 全局事件广播
⑧ 状态效果触发(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 迁移后显得格外突出。
// 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+条)每帧高频调用时有感知延迟。
// 当前: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 分配。形态切换发生频率低,影响有限,但有更干净的写法:
// 建议:固定长度数组,避免 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:
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 驱动,新增功能只需:
- 创建新 SO 资产
- 在 Inspector 绑定
- 无需修改现有代码
典型示范:
- 护符系统:
ICharmEffect接口 +CharmSO.effects[]→ 新增护符效果只需实现接口并创建资产,完美开闭原则 - 技能系统:
FormSkillSO数据 +SkillManager执行 → 形态技能由配置决定 - Boss 系统:
BossSkillSO+SkillSequenceSO+AttackPatternSO三层 → 纯数据驱动 Boss 行为设计
4.2 EventChannel 扩展 ✅ 无限扩展
新增事件类型仅需一行:
[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 使用的服务应尽量对应接口类型:
// 建议:定义接口并注册
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 统一的服务访问模式 ✅
// 全框架统一:ServiceLocator.GetOrDefault<T>()
var saveManager = ServiceLocator.GetOrDefault<SaveManager>();
var questManager = ServiceLocator.GetOrDefault<IQuestManager>();
var audioService = ServiceLocator.GetOrDefault<IAudioService>();
无 Singleton.Instance 混用,框架内服务访问路径唯一。
6.2 统一的事件订阅模式 ✅(95% 完成)
// 全框架统一 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 事件混用
框架中存在两套事件订阅机制:
- EventChannel(SO):
_channel?.Subscribe(H).AddTo(_subs)— 框架标准 - 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 版本执行:
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-1:GameManager 迁移至 RAII
// 添加字段
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-2:SaveMigrator 版本对齐
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-1:SaveManager 静态字段迁移为实例属性
// SaveManager.cs — 删除 static,改为实例属性
public string LastCheckpointScene { get; private set; }
public string LastCheckpointSpawnId { get; private set; }
// DeathRespawnService.cs — 通过 ServiceLocator 获取
var sm = ServiceLocator.GetOrDefault<SaveManager>();
_onSceneLoadRequest?.Raise(new SceneLoadRequest
{
SceneName = sm?.LastCheckpointScene,
EntryTransitionId = sm?.LastCheckpointSpawnId,
// ...
});
Fix M-2:HitStopManager 添加接口
// 新增接口(放在 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-3:EventChannelRegistry 移除 DontDestroyOnLoad
// EventChannelRegistry.Awake() — 删除以下行
// DontDestroyOnLoad(transform.root.gameObject); ← 删除
Fix L-1:AudioManager SFX 查找优化
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 游戏标准,在以下方面有突出表现:
- 事件系统:SO 频道 + RAII CompositeDisposable 的组合,在 Unity 生态中属顶层实践
- 战斗流水线:
HurtBox8 步流水线接口隔离完整,扩展无需修改现有代码 - 存档系统:原子写入 + HMAC 校验 + DLC 扩展字段,工程化程度高于大多数 AAA 之外的游戏
- 数据驱动:SO 驱动护符、技能、Boss、道具,内容迭代不触及代码
- 编辑器工具链:EventBusMonitor + SceneScaffoldTools + 多个专域编辑器窗口,超出同体量框架标准
待解决的核心问题
| 优先级 | 数量 | 说明 |
|---|---|---|
| 🔴 高 | 2 | GameManager 旧式事件订阅;SaveMigrator 版本不一致 |
| 🟡 中 | 3 | SaveManager 静态字段;HitStopManager 缺接口;EventChannelRegistry 重复 DDOL |
| 🟢 低 | 4 | AudioManager O(n) 查找;SkillManager GC;HitBox 容量;GameIds 统一性 |
解决以上 9 个问题后,框架代码质量可达到完全无历史残留、统一机制、商业可发布标准。
本评审基于源码静态分析,未涵盖运行时 Profiler 数据和平台适配专项测试。