# 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(impl)` — 标准注册 - `GetOrDefault()` — 安全获取,不抛异常 - `Unregister(impl)` — 防止场景切换时旧实例残留(安全版重载是亮点) - `OverrideForTest` / `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 迁移后显得格外突出。 ```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` 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,O(1) 查询 private Dictionary _sfxLookup; private void Awake() { _sfxLookup = new Dictionary(_sfxRegistry.Length); foreach (var e in _sfxRegistry) _sfxLookup[e.Key] = e.Event; } ``` **⚠️ 问题 3-B(低)**:`SkillManager.UpdateSkillSet()` 每次切换形态都创建 `new List(3)` 并 `ToArray()`,产生两次 GC 分配。形态切换发生频率低,影响有限,但有更干净的写法: ```csharp // 建议:固定长度数组,避免 List + ToArray private readonly FormSkillSO[] _activeSkills = new FormSkillSO[3]; private int _activeSkillCount; ``` **⚠️ 问题 3-C(中)**:`HitBox` 中 `_hitThisActivation`(HashSet)和 `_hitCooldownTimers`(Dictionary)在每次 `Activate/Deactivate` 时调用 `Clear()` 而不是预分配容量后复用,多次激活/停用时会引发字典内部数组的 GC。建议在初始化时预设 capacity: ```csharp private readonly HashSet _hitThisActivation = new(8); private readonly Dictionary _hitCooldownTimers = new(8); ``` **⚠️ 问题 3-D(低)**:`SaveManager._saveables` 使用 `List`,每次 `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 { } ``` `VoidBaseEventChannelSO` 和 `BaseEventChannelSO` 两个基类覆盖全部需求。 ### 4.3 存档扩展 ✅ 支持 DLC `SaveData.DLC = new Dictionary()` 专用字段 + `[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(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() var saveManager = ServiceLocator.GetOrDefault(); var questManager = ServiceLocator.GetOrDefault(); var audioService = ServiceLocator.GetOrDefault(); ``` 无 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. **EventChannel(SO)**:`_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-1:GameManager 迁移至 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-2:SaveMigrator 版本对齐 ```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-1:SaveManager 静态字段迁移为实例属性 ```csharp // SaveManager.cs — 删除 static,改为实例属性 public string LastCheckpointScene { get; private set; } public string LastCheckpointSpawnId { get; private set; } ``` ```csharp // DeathRespawnService.cs — 通过 ServiceLocator 获取 var sm = ServiceLocator.GetOrDefault(); _onSceneLoadRequest?.Raise(new SceneLoadRequest { SceneName = sm?.LastCheckpointScene, EntryTransitionId = sm?.LastCheckpointSpawnId, // ... }); ``` ### Fix M-2:HitStopManager 添加接口 ```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() != null) { Destroy(gameObject); return; } ServiceLocator.Register(this); } private void OnDestroy() { Time.timeScale = _baseTimeScale; ServiceLocator.Unregister(this); } } // ClashResolver.cs ServiceLocator.GetOrDefault()?.FreezeFrames(...); ``` ### Fix M-3:EventChannelRegistry 移除 DontDestroyOnLoad ```csharp // EventChannelRegistry.Awake() — 删除以下行 // DontDestroyOnLoad(transform.root.gameObject); ← 删除 ``` ### Fix L-1:AudioManager SFX 查找优化 ```csharp private Dictionary _sfxLookup; private void Awake() { // ... 其他初始化 _sfxLookup = new Dictionary(_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 GC;HitBox 容量;GameIds 统一性 | 解决以上 9 个问题后,框架代码质量可达到**完全无历史残留、统一机制、商业可发布标准**。 --- *本评审基于源码静态分析,未涵盖运行时 Profiler 数据和平台适配专项测试。*