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

22 KiB
Raw Blame History

BaseGames Framework — 代码评审 v2修订版

评审时间2026-05-13修订
评审范围:Assets/Scripts/ 全目录
评审标准:成熟商业动作 RPG 框架Unity 2022.3 LTS / C#
框架定位:新框架,无需向后兼容,追求纯净、统一、无历史残留
修订说明v1 评审中的 9 项问题均已修复,本版记录当前实际状态并识别新发现的问题。


目录

  1. 总体评分
  2. 架构设计
  3. 性能
  4. 可扩展性
  5. 编辑器友好性
  6. 使用便利性
  7. v1 问题修复状态
  8. 当前问题清单
  9. 修复方案
  10. 综合结论

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.CoreBaseGames.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

  • EventSubscriptionreadonly 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-2SaveableMonoBehaviour(及 DifficultyManager、LocalizationManager、MapManager、QuestManager、MapPin、ShopController均直接调用 ServiceLocator.GetOrDefault<SaveManager>()?.Register/Unregister,对具体类产生 7 处以上跨模块依赖。应提取 ISaveableRegistry 接口消除这些依赖。


2.4 存档系统 设计优秀

三层存档架构:

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

亮点:

  • 原子写入.tmpFile.Replace.bak,断电安全
  • HMAC-SHA256 校验和:防止存档篡改,校验失败时仅警告不拒绝加载
  • [JsonExtensionData]未知字段保留DLC 扩展数据隔离
  • 异步 I/O + SemaphoreSlim:串行化并发请求,无数据竞争
  • CrashReporter:异常退出时同步写入崩溃日志 + 触发紧急存档槽
  • ISaveable + SaveableMonoBehaviour:组件自动注册/注销

⚠️ 问题 A-3SaveMigrator.Migrate() 虽版本常量已对齐(CurrentVersion = "2.1"),但无任何实际迁移分支——遇到旧版存档只发出警告,直接将版本覆写为当前值,字段迁移逻辑缺失,存档升级时数据静默丢失。


2.5 战斗系统 架构精良

8 步伤害流水线HurtBox.ReceiveDamage

① 无敌帧检查
② 弹反检查ParrySystem跨程序集接口隔离
③ 霸体检查IPoiseSource
④ 护盾拦截IShieldable玩家专属
⑤ 防御减免(最低 1 点)
⑥ TakeDamageIDamageable
⑦ 全局事件广播
⑧ 状态效果触发IStatusEffectable

所有步骤通过接口隔离,零直接类型依赖,高度符合开闭原则。

DamageInfostruct 值类型热路径零堆分配,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 运行时调试支持 良好

  • HurtBoxOnDrawGizmos() 三色可视化受击盒状态
  • HitBox.Awake() 运行时验证 IsTrigger
  • PlayerController#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 事件混用 合理

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

  1. EventChannelSO:跨程序集游戏事件,框架标准
  2. C# 原生 eventInputReaderSO → 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-1aClashResolver → 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-1bSettingsManager → 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-1cDifficultyManager → 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-1dVFXPool → 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-1eMapManager → 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-3SaveMigrator 添加迁移分支

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.0Settings 子对象从顶层迁移至 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.1Tutorial 子对象新增
    private static void MigrateFrom_2_0_To_2_1(SaveData data)
    {
        // data.Tutorial 已在 SaveData 构造时初始化,此处无需额外处理
        // 若有旧字段需要搬迁,在此操作 data.ExtensionData
    }
}

10. 综合结论

框架总体水平

本框架的架构质量达到商业独立 AA 游戏标准,突出优势:

  1. 事件系统SO 频道 + RAII CompositeDisposable全框架统一零泄漏
  2. 战斗流水线HurtBox 8 步接口隔离完整,扩展无需修改现有代码
  3. 存档系统:原子写入 + HMAC 校验 + DLC 扩展字段,工程化程度高
  4. 数据驱动SO 驱动护符、技能、Boss、道具内容迭代不触及代码
  5. 编辑器工具链EventBusMonitor + SceneScaffoldTools + 多个专域编辑器窗口

待解决的核心问题

优先级 # 说明
🔴 5 ClashResolver、SettingsManager、DifficultyManager、VFXPool、MapManager 缺接口,违反依赖倒置
🟡 2 ISaveableRegistry 缺失7 处耦合SaveMigrator 迁移逻辑为空(数据静默丢失风险)

解决以上 7 个问题后,框架将达到完全接口化、数据一致、零历史残留的商业发布标准


本评审基于源码静态分析2026-05-13。v12026-05-12中识别的 9 项问题均已在上一轮修复中解决。