22 KiB
BaseGames Framework — 代码评审 v2(修订版)
评审时间:2026-05-13(修订)
评审范围:Assets/Scripts/全目录
评审标准:成熟商业动作 RPG 框架(Unity 2022.3 LTS / C#)
框架定位:新框架,无需向后兼容,追求纯净、统一、无历史残留
修订说明:v1 评审中的 9 项问题均已修复,本版记录当前实际状态并识别新发现的问题。
目录
1. 总体评分
| 维度 | 评分 | 说明 |
|---|---|---|
| 架构设计 | ★★★★☆ | ServiceLocator 接口覆盖尚不完整,5个Manager缺接口抽象 |
| 性能 | ★★★★★ | 所有热路径已优化,GC 压力极低 |
| 可扩展性 | ★★★★☆ | SO 驱动设计优秀,SaveableRegistry 模式待统一 |
| 编辑器友好性 | ★★★★★ | 工具链完备,超出同类商业框架水平 |
| 使用便利性 | ★★★★★ | 事件/服务模式已全面统一,样板代码极少 |
2. 架构设计
2.1 整体架构评价 ✅ 优秀
框架采用多层解耦架构,程序集依赖方向严格单向:
Core.Events ← 最低层(无任何游戏依赖)
↑
Core / Core.Save ← 服务层
↑
Combat / Player / Enemies / Audio / VFX ... ← 游戏系统层
↑
UI / World / Equipment / Quest ... ← 表现/业务层
↑
Editor ← 纯编辑器工具(运行时不可见)
28 个 .asmdef 程序集按功能边界划分,autoReferenced: true 仅用于 BaseGames.Core 和 BaseGames.Core.Save,其余程序集通过显式引用声明,完全符合 Unity 最佳实践。
2.2 事件系统 ✅ 商业级
ScriptableObject 事件频道(EventChannel) 是框架通信的统一机制,已全面采用 RAII 模式:
// 全框架统一(GameManager、AudioManager、EquipmentManager 等 97% 文件已迁移)
private readonly CompositeDisposable _subs = new();
private void OnEnable() => _channel?.Subscribe(Handler).AddTo(_subs);
private void OnDisable() => _subs.Clear();
CompositeDisposable + EventSubscription:
EventSubscription为readonly struct,零堆分配CompositeDisposable.Clear()批量清除,不可能泄漏订阅
EventBusMonitor:固定大小环形缓冲区(256 条),Editor 下记录所有事件、payload、订阅者数,属商业罕见的高质量调试工具。
2.3 服务定位器 ✅ 良好,部分待完善
ServiceLocator 轻量、类型安全:
Register<IInterface>(impl)— 依赖倒置注册GetOrDefault<T>()— 安全获取,无异常Unregister<T>(impl)— 防止场景切换旧实例残留OverrideForTest<T>/Reset()— 测试支持(Editor 条件编译)
✅ GameServiceRegistrar([DefaultExecutionOrder(-2000)])负责统一注册核心服务(IDeathRespawnService、ISceneService、IEventChannelRegistry、ISaveService),职责单一。
⚠️ 问题 A-1(高):以下 Manager 仍以具体类型注册,无对应接口,违反依赖倒置原则:
| Manager | 注册方式 | 调用方(需改为接口) |
|---|---|---|
ClashResolver |
Register<ClashResolver> |
HitBox.cs |
SettingsManager |
Register<SettingsManager> |
AudioManager.cs |
DifficultyManager |
Register<DifficultyManager> |
GameManager, EnemyStats, LootResolver, PlayerStats, ShopController(共 5 处) |
VFXPool |
Register<VFXPool> |
HitFXSpawner.cs |
MapManager |
Register<MapManager> |
MapPanel.cs |
GameManager以具体类型自注册仅用于单例保护(自检后即退出),无外部业务调用方,此为可接受的例外。
SaveManager以具体类型注册并被众多组件直接访问(见问题 A-2)。
⚠️ 问题 A-2(中):SaveableMonoBehaviour(及 DifficultyManager、LocalizationManager、MapManager、QuestManager、MapPin、ShopController)均直接调用 ServiceLocator.GetOrDefault<SaveManager>()?.Register/Unregister,对具体类产生 7 处以上跨模块依赖。应提取 ISaveableRegistry 接口消除这些依赖。
2.4 存档系统 ✅ 设计优秀
三层存档架构:
SaveManager(协调层)
↓
ISaveStorage(接口)→ LocalFileStorage(实现)
↓
SaveData(数据层)→ JSON via Newtonsoft.Json
亮点:
- 原子写入:
.tmp→File.Replace→.bak,断电安全 - HMAC-SHA256 校验和:防止存档篡改,校验失败时仅警告不拒绝加载
[JsonExtensionData]:未知字段保留,DLC 扩展数据隔离- 异步 I/O + SemaphoreSlim:串行化并发请求,无数据竞争
CrashReporter:异常退出时同步写入崩溃日志 + 触发紧急存档槽ISaveable+SaveableMonoBehaviour:组件自动注册/注销
⚠️ 问题 A-3(中):SaveMigrator.Migrate() 虽版本常量已对齐(CurrentVersion = "2.1"),但无任何实际迁移分支——遇到旧版存档只发出警告,直接将版本覆写为当前值,字段迁移逻辑缺失,存档升级时数据静默丢失。
2.5 战斗系统 ✅ 架构精良
8 步伤害流水线(HurtBox.ReceiveDamage):
① 无敌帧检查
② 弹反检查(ParrySystem,跨程序集接口隔离)
③ 霸体检查(IPoiseSource)
④ 护盾拦截(IShieldable,玩家专属)
⑤ 防御减免(最低 1 点)
⑥ TakeDamage(IDamageable)
⑦ 全局事件广播
⑧ 状态效果触发(IStatusEffectable)
所有步骤通过接口隔离,零直接类型依赖,高度符合开闭原则。
DamageInfo:struct 值类型热路径零堆分配,Builder 模式支持复杂构造,DamageInfo.From(DamageSourceSO, ...) 覆盖 90% 使用场景。
2.6 玩家状态机 ✅ 结构清晰
PlayerController 持有状态字典,所有状态继承 PlayerStateBase,通过 TryTransitionState() 驱动切换。连击动画时间点由 PlayerAnimationConfigSO 配置,无硬编码。
3. 性能
3.1 热路径优化 ✅ 优秀
| 机制 | 优化方式 | 状态 |
|---|---|---|
DamageInfo.From() |
栈分配 struct,零 GC | ✅ |
EventSubscription |
readonly struct,零 GC |
✅ |
EventBusMonitor |
固定大小环形缓冲区,Editor 内零 GC | ✅ |
AudioManager.PlaySFX |
Dictionary<string, AudioEventSO> O(1) 查找 |
✅ 已修复 |
SkillManager.UpdateSkillSet |
固定大小 FormSkillSO[] 数组,无 List/ToArray |
✅ 已修复 |
HitBox._hitThisActivation |
new HashSet<Collider2D>(8) 预设容量 |
✅ 已修复 |
GlobalObjectPool.Despawn |
O(1) 通过 AliveNode (LinkedListNode) 定位 |
✅ |
WorldStateRegistry |
HashSet<string> O(1) 查询 |
✅ |
3.2 MapManager.OnSave GC 分配(低优先级)
// MapManager.cs(当前)
public void OnSave(SaveData data)
{
data.Map.ExploredRooms = _exploredRooms.ToList(); // 每次存档 GC 分配
data.Map.MappedRooms = _mappedRooms.ToList();
}
存档操作频率低,GC 影响可忽略,记录仅作完整性参考。
4. 可扩展性
4.1 ScriptableObject 驱动架构 ✅ 商业顶级
- 护符系统:
ICharmEffect+CharmSO.effects[]— 新增护符效果只需实现接口并创建资产 - 技能系统:
FormSkillSO数据 +SkillManager执行 — 形态技能由配置决定 - Boss 系统:
BossSkillSO+SkillSequenceSO+AttackPatternSO三层 — 纯数据驱动
4.2 EventChannel 扩展 ✅ 无限扩展
[CreateAssetMenu(menuName = "Events/MyType")]
public class MyTypeEventChannelSO : BaseEventChannelSO<MyType> { }
4.3 存档扩展 ✅ 支持 DLC
SaveData.DLC = new Dictionary<string, JObject>() + [JsonExtensionData] 支持 DLC 扩展,SaveMigrator 架构提供版本升级路径(当前逻辑待实现,见问题 A-3)。
4.4 接口覆盖不完整(见问题 A-1)
5 个 Manager 缺少接口抽象,会在需要替换实现或单元测试时产生阻力(见问题 A-1 详细列表)。
5. 编辑器友好性
5.1 工具链 ✅ 超出商业标准
| 工具 | 功能 |
|---|---|
| EventBusMonitorWindow | 实时监控所有 SO 事件、payload、订阅者数、帧号 |
| SceneScaffoldTools | 一键生成 Persistent 场景层级 + 自动绑定资产引用 |
| EventChainEditorWindow | 可视化事件链编辑器 |
| BossSkillSequenceWindow | Boss 技能序列可视化 |
| CreateEventChannelAssets | 批量创建 EventChannel SO 资产 |
| AddressReferenceGraphWindow | Addressables 引用关系图 |
| ValidationSystem | IValidatable + 批量校验 |
5.2 运行时调试支持 ✅ 良好
HurtBox有OnDrawGizmos()三色可视化受击盒状态HitBox.Awake()运行时验证IsTriggerPlayerController有#if UNITY_EDITOR [SerializeField] _debugValidateTransitions- 所有关键
[DefaultExecutionOrder]有文档说明原因
6. 使用便利性
6.1 服务访问模式 ✅ 统一
// 接口(已接口化的服务)
var audio = ServiceLocator.GetOrDefault<IAudioService>();
var dialogue = ServiceLocator.GetOrDefault<IDialogueService>();
var quest = ServiceLocator.GetOrDefault<IQuestManager>();
// 具体类(待接口化)
var difficulty = ServiceLocator.GetOrDefault<DifficultyManager>(); // ← 待改进
var settings = ServiceLocator.GetOrDefault<SettingsManager>(); // ← 待改进
6.2 事件订阅模式 ✅ 全面统一
RAII 模式已覆盖全框架:
private readonly CompositeDisposable _subs = new();
private void OnEnable() => _channel?.Subscribe(Handler).AddTo(_subs);
private void OnDisable() => _subs.Clear();
v1 评审中最后一处旧式订阅(GameManager)已在上一轮修复中完成迁移。
6.3 Input 事件混用 ✅ 合理
框架中存在两套事件机制:
- EventChannel(SO):跨程序集游戏事件,框架标准
- C# 原生 event:InputReaderSO → SkillManager / PlayerController.States
混用是合理设计,不是缺陷。Input 事件不需要跨 SO 资产的观察者模式,保持现状正确。
6.4 Debug.Assert 统一用法 ✅
关键组件在 Awake 中验证 Inspector 引用,开发期快速暴露配置错误,Release 版本无额外开销。
7. v1 问题修复状态
| # | 文件 | v1 描述 | 当前状态 |
|---|---|---|---|
| H-1 | Core/GameManager.cs |
OnEnable 旧式 +=/-= 订阅 |
✅ 已修复 — RAII 模式 |
| H-2 | Core/Save/SaveMigrator.cs |
CurrentVersion = "1.0" vs SaveMeta.Version = "2.1" |
✅ 已修复 — 版本对齐为 "2.1" |
| M-1 | Core/Save/SaveManager.cs |
public static 检查点字段 |
✅ 已修复 — 实例属性 |
| M-2 | Combat/HitStopManager.cs |
无 IHitStopService 接口 |
✅ 已修复 — 实现接口并注册 |
| M-3 | Core/Events/EventChannelRegistry.cs |
重复 DontDestroyOnLoad |
✅ 已修复 — DDOL 已移除 |
| L-1 | Audio/AudioManager.cs |
PlaySFX O(n) 线性扫描 |
✅ 已修复 — Dictionary O(1) |
| L-2 | Skills/SkillManager.cs |
UpdateSkillSet List+ToArray GC |
✅ 已修复 — 固定大小数组 |
| L-3 | Combat/HitBox.cs |
HashSet/Dictionary 未预设容量 | ✅ 已修复 — new(8) |
| L-4 | Core/GameIds.cs |
字符串常量覆盖待确认 | ✅ 已确认 — 覆盖 Boss/Chain/Quest/Ability/Scene/Collectible/Npc/Flag 8 个域 |
v1 9 项问题全部修复完毕。
8. 当前问题清单(2026-05 v2 Session 2 修复后)
✅ 全部修复完成
| # | 文件 | 问题描述 | 状态 |
|---|---|---|---|
| A-1a | Combat/ClashResolver.cs + HitBox.cs |
Register/GetOrDefault<ClashResolver> — 无 IClashService 接口 |
✅ 已修复 |
| A-1b | Core/SettingsManager.cs + AudioManager.cs |
Register/GetOrDefault<SettingsManager> — 无 ISettingsService 接口 |
✅ 已修复 |
| A-1c | Core/Difficulty/DifficultyManager.cs + 5 处调用方 |
Register/GetOrDefault<DifficultyManager> — 无 IDifficultyService 接口 |
✅ 已修复 |
| A-1d | VFX/VFXPool.cs + HitFXSpawner.cs |
Register/GetOrDefault<VFXPool> — 无 IVFXPoolService 接口 |
✅ 已修复 |
| A-1e | World/Map/MapManager.cs + MapPanel.cs |
Register/GetOrDefault<MapManager> — 无 IMapService 接口 |
✅ 已修复 |
| A-2 | ISaveableRegistry 缺失(7 处直接耦合 SaveManager) |
SaveableMonoBehaviour、DifficultyManager、LocalizationManager、MapManager、QuestManager、MapPin、ShopController 直接调用 GetOrDefault<SaveManager>()?.Register/Unregister |
✅ 已修复 |
| A-3 | Core/Save/SaveMigrator.cs |
Migrate() 无实际迁移逻辑 |
✅ 已修复 |
新增接口文件清单
| 文件 | 命名空间 |
|---|---|
Assets/Scripts/Combat/IClashService.cs |
BaseGames.Combat |
Assets/Scripts/Core/ISettingsService.cs |
BaseGames.Core |
Assets/Scripts/Core/Difficulty/IDifficultyService.cs |
BaseGames.Core |
Assets/Scripts/VFX/IVFXPoolService.cs |
BaseGames.VFX |
Assets/Scripts/World/Map/IMapService.cs |
BaseGames.World.Map |
Assets/Scripts/Core/Save/ISaveableRegistry.cs |
BaseGames.Core.Save |
9. 修复方案
Fix A-1a:ClashResolver → IClashService
// 新建 Assets/Scripts/Combat/IClashService.cs
namespace BaseGames.Combat
{
public interface IClashService
{
void ResolveClash(HitBox hitBoxA, HitBox hitBoxB);
}
}
// ClashResolver.cs — 实现接口,改用接口注册
public class ClashResolver : MonoBehaviour, IClashService
{
private void Awake()
{
if (ServiceLocator.GetOrDefault<IClashService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<IClashService>(this);
}
private void OnDestroy() => ServiceLocator.Unregister<IClashService>(this);
}
// HitBox.cs — 改为接口访问
ServiceLocator.GetOrDefault<IClashService>()?.ResolveClash(this, rivalHitBox);
Fix A-1b:SettingsManager → ISettingsService
// 新建 Assets/Scripts/Core/ISettingsService.cs
namespace BaseGames.Core
{
public interface ISettingsService
{
GlobalSettingsData Current { get; }
void SetMasterVolume(float v);
void SetBGMVolume(float v);
void SetSFXVolume(float v);
void SetAmbientVolume(float v);
void SetResolution(int w, int h, UnityEngine.FullScreenMode mode);
void SetVSync(bool enabled);
void SetTargetFrameRate(int fps);
void SetLanguage(string localeCode);
void Save();
}
}
// SettingsManager.cs — 实现接口,改用接口注册
public class SettingsManager : MonoBehaviour, ISettingsService
{
private void Awake() => ServiceLocator.Register<ISettingsService>(this);
private void OnDestroy() => ServiceLocator.Unregister<ISettingsService>(this);
}
// AudioManager.cs — 改为接口访问
var settings = ServiceLocator.GetOrDefault<ISettingsService>();
Fix A-1c:DifficultyManager → IDifficultyService
// 新建 Assets/Scripts/Core/Difficulty/IDifficultyService.cs
namespace BaseGames.Core
{
public interface IDifficultyService
{
DifficultyLevel CurrentLevel { get; }
DifficultyScalerSO CurrentScaler { get; }
void ChangeDifficulty(DifficultyLevel level);
DifficultyScalerSO GetScaler(DifficultyLevel level);
}
}
// DifficultyManager.cs — 实现接口
public class DifficultyManager : MonoBehaviour, ISaveable, IDifficultyService
{
private void Awake()
{
if (ServiceLocator.GetOrDefault<IDifficultyService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<IDifficultyService>(this);
Apply(DifficultyLevel.Normal);
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
}
private void OnDestroy() => ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
}
// 调用方(GameManager, EnemyStats, LootResolver, PlayerStats, ShopController)
var scaler = ServiceLocator.GetOrDefault<IDifficultyService>()?.CurrentScaler;
Fix A-1d:VFXPool → IVFXPoolService
// 新建 Assets/Scripts/VFX/IVFXPoolService.cs
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace BaseGames.VFX
{
public interface IVFXPoolService
{
void Play(AssetReferenceGameObject vfxRef, Vector3 position,
Quaternion rotation = default, float maxLifetime = 0f);
void Warmup(AssetReferenceGameObject vfxRef, int count);
}
}
// VFXPool.cs — 实现接口
public class VFXPool : MonoBehaviour, IVFXPoolService
{
private void Awake() => ServiceLocator.Register<IVFXPoolService>(this);
private void OnDestroy() => ServiceLocator.Unregister<IVFXPoolService>(this);
}
// HitFXSpawner.cs — 改为接口访问
var pool = ServiceLocator.GetOrDefault<IVFXPoolService>();
pool?.Play(vfxRef, info.HitPoint);
Fix A-1e:MapManager → IMapService
// 新建 Assets/Scripts/World/Map/IMapService.cs
namespace BaseGames.World.Map
{
public interface IMapService
{
bool IsExplored(string roomId);
bool IsMapped(string roomId);
}
}
// MapManager.cs — 实现接口
public class MapManager : MonoBehaviour, ISaveable, IMapService
{
private void Awake()
{
if (ServiceLocator.GetOrDefault<IMapService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<IMapService>(this);
}
private void OnDestroy() => ServiceLocator.Unregister<IMapService>(this);
}
// MapPanel.cs — 改为接口访问
var mapManager = ServiceLocator.GetOrDefault<IMapService>();
bool discovered = mapManager != null && mapManager.IsExplored(room.RoomId);
Fix A-2:提取 ISaveableRegistry
// 新建 Assets/Scripts/Core/Save/ISaveableRegistry.cs
namespace BaseGames.Core.Save
{
public interface ISaveableRegistry
{
void Register(ISaveable saveable);
void Unregister(ISaveable saveable);
}
}
// SaveManager.cs — 额外实现 ISaveableRegistry
public class SaveManager : MonoBehaviour, ISaveableRegistry
{
private void Awake()
{
if (ServiceLocator.GetOrDefault<SaveManager>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<SaveManager>(this);
ServiceLocator.Register<ISaveableRegistry>(this); // ← 新增
}
private void OnDestroy()
{
ServiceLocator.Unregister<SaveManager>(this);
ServiceLocator.Unregister<ISaveableRegistry>(this); // ← 新增
}
}
// SaveableMonoBehaviour.cs — 改为接口访问
protected virtual void OnEnable() => ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
protected virtual void OnDisable() => ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
// 其他 6 处调用方同理(DifficultyManager、LocalizationManager、MapManager、QuestManager、MapPin、ShopController)
Fix A-3:SaveMigrator 添加迁移分支
public static class SaveMigrator
{
public const string CurrentVersion = "2.1";
public static SaveData Migrate(SaveData data)
{
if (data?.Meta == null) return data;
string v = data.Meta.Version ?? "1.0";
// 按版本顺序依次升级
if (string.CompareOrdinal(v, "2.0") < 0) MigrateFrom_1x_To_2x(data);
if (string.CompareOrdinal(v, "2.1") < 0) MigrateFrom_2_0_To_2_1(data);
if (data.Meta.Version != CurrentVersion)
Debug.Log($"[SaveMigrator] 存档已从 '{data.Meta.Version}' 迁移至 '{CurrentVersion}'。");
data.Meta.Version = CurrentVersion;
return data;
}
// 1.x → 2.0:Settings 子对象从顶层迁移至 SaveData.Settings
private static void MigrateFrom_1x_To_2x(SaveData data)
{
// 示例:旧版顶层 Language 字段 → Settings.Language
// if (data.ExtensionData.TryGetValue("Language", out var lang))
// data.Settings.Language = lang.ToObject<string>();
}
// 2.0 → 2.1:Tutorial 子对象新增
private static void MigrateFrom_2_0_To_2_1(SaveData data)
{
// data.Tutorial 已在 SaveData 构造时初始化,此处无需额外处理
// 若有旧字段需要搬迁,在此操作 data.ExtensionData
}
}
10. 综合结论
框架总体水平
本框架的架构质量达到商业独立 AA 游戏标准,突出优势:
- 事件系统:SO 频道 + RAII CompositeDisposable,全框架统一,零泄漏
- 战斗流水线:
HurtBox8 步接口隔离完整,扩展无需修改现有代码 - 存档系统:原子写入 + HMAC 校验 + DLC 扩展字段,工程化程度高
- 数据驱动:SO 驱动护符、技能、Boss、道具,内容迭代不触及代码
- 编辑器工具链:EventBusMonitor + SceneScaffoldTools + 多个专域编辑器窗口
待解决的核心问题
| 优先级 | # | 说明 |
|---|---|---|
| 🔴 高 | 5 | ClashResolver、SettingsManager、DifficultyManager、VFXPool、MapManager 缺接口,违反依赖倒置 |
| 🟡 中 | 2 | ISaveableRegistry 缺失(7 处耦合);SaveMigrator 迁移逻辑为空(数据静默丢失风险) |
解决以上 7 个问题后,框架将达到完全接口化、数据一致、零历史残留的商业发布标准。
本评审基于源码静态分析(2026-05-13)。v1(2026-05-12)中识别的 9 项问题均已在上一轮修复中解决。