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

20 KiB
Raw Blame History

BaseGames Framework — 全面代码评审

评审时间2026-05-12
评审范围:Assets/Scripts/ 全目录
评审标准:成熟商业动作 RPG 框架Unity 2022.3 LTS / C#
框架定位:新框架,无需向后兼容,追求纯净、统一、无历史残留


目录

  1. 总体评分
  2. 架构设计
  3. 性能
  4. 可扩展性
  5. 编辑器友好性
  6. 使用便利性
  7. 问题清单(优先级排序)
  8. 修复方案
  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 是框架通信的统一机制:

// 订阅RAII 模式97% 的文件已全面迁移)
_channel?.Subscribe(Handler).AddTo(_subs);

// 广播(零耦合,跨程序集无问题)
_channel?.Raise(payload);

CompositeDisposable + EventSubscription 实现了 Rx.js/UniRx 风格的生命周期安全订阅,属于行业领先实践:

  • EventSubscriptionreadonly 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-AHitStopManager 迁移至 ServiceLocator 后以具体类型注册,无 IHitStopManager 接口,导致 ClashResolver 对具体实现类产生依赖,降低可测试性。


2.4 存档系统 设计优秀

三层存档架构:

SaveManager协调层
  ↓
ISaveStorage接口→ LocalFileStorage实现
  ↓
SaveData数据层→ JSON via Newtonsoft.Json

亮点:

  • 原子写入.tmpFile.Replace.bak,断电安全
  • HMAC-SHA256 校验和:防止存档被篡改,校验失败时仍允许加载(仅警告)
  • [JsonExtensionData]未知字段保留DLC 扩展数据隔离,优雅的前向兼容
  • 异步 I/O + SemaphoreSlim:并发存档请求串行化,无数据竞争
  • CrashReporter:异常退出时同步写入崩溃日志 + 触发紧急存档槽(异步不可靠时的正确降级)
  • ISaveable接口 + SaveableMonoBehaviour基类:组件自动注册/注销,消除样板代码

⚠️ 问题 2-BSaveManager.LastCheckpointSceneLastCheckpointSpawnIdpublic static 字段,破坏了框架的实例化服务模型。DeathRespawnServiceAntiSoftlockSystem 通过 SaveManager.LastCheckpoint* 静态访问绕过了 ServiceLocator。

⚠️ 问题 2-CSaveMigrator.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-DGameManager.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-AAudioManager.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-BSkillManager.UpdateSkillSet() 每次切换形态都创建 new List<FormSkillSO>(3)ToArray(),产生两次 GC 分配。形态切换发生频率低,影响有限,但有更干净的写法:

// 建议:固定长度数组,避免 List + ToArray
private readonly FormSkillSO[] _activeSkills = new FormSkillSO[3];
private int _activeSkillCount;

⚠️ 问题 3-CHitBox_hitThisActivationHashSet_hitCooldownTimersDictionary<Collider2D, float>)在每次 Activate/Deactivate 时调用 Clear() 而不是预分配容量后复用,多次激活/停用时会引发字典内部数组的 GC。建议在初始化时预设 capacity

private readonly HashSet<Collider2D>            _hitThisActivation = new(8);
private readonly Dictionary<Collider2D, float>  _hitCooldownTimers = new(8);

⚠️ 问题 3-DSaveManager._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 扩展 无限扩展

新增事件类型仅需一行:

[CreateAssetMenu(menuName = "Events/MyType")]
public class MyTypeEventChannelSO : BaseEventChannelSO<MyType> { }

VoidBaseEventChannelSOBaseEventChannelSO<T> 两个基类覆盖全部需求。

4.3 存档扩展 支持 DLC

SaveData.DLC = new Dictionary<string, JObject>() 专用字段 + [JsonExtensionData] 未知字段保留,支持 DLC 在不修改主存档结构的前提下扩展数据。SaveMigrator 架构(现虽为空)提供了版本升级路径。

4.4 接口覆盖不完整

⚠️ 问题 4-AHitStopManager 以具体类注册无接口抽象。ServiceLocator 使用的服务应尽量对应接口类型:

// 建议:定义接口并注册
public interface IHitStopService {
    void FreezeFrames(int frames);
    void FreezeDuration(float seconds);
    float BaseTimeScale { get; set; }
}
ServiceLocator.Register<IHitStopService>(this);

⚠️ 问题 4-BDialogueManager 直接以具体类注册到 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 运行时调试支持 良好

  • HurtBoxOnDrawGizmos() 可视化受击盒状态(激活/无敌/非激活三种颜色)
  • HitBox 中 Awake 对 IsTrigger 做运行时验证并日志警告
  • 所有关键 [DefaultExecutionOrder] 有文档注释说明原因
  • PlayerController#if UNITY_EDITOR [SerializeField] private bool _debugValidateTransitions

5.3 小问题

⚠️ 问题 5-AEventChannelRegistry.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 事件混用

框架中存在两套事件订阅机制:

  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 版本执行:

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 LastCheckpointSceneLastCheckpointSpawnIdpublic 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

// 添加字段
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 版本对齐

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 静态字段迁移为实例属性

// 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-2HitStopManager 添加接口

// 新增接口(放在 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

// EventChannelRegistry.Awake() — 删除以下行
// DontDestroyOnLoad(transform.root.gameObject);  ← 删除

Fix L-1AudioManager 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 游戏标准,在以下方面有突出表现:

  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 数据和平台适配专项测试。