22 KiB
BaseGames Framework — 第六轮完整评审
日期: 2026 年 5 月
评审轮次: v6(接续 v5 修复后全量复审)
版本基线: Unity 2022.3 LTS / C# 9 / 28+ .asmdef 程序集
上轮评分: v5 加权 8.73 / 10
一、评审说明
本轮对 Assets/Scripts 全部模块代码进行系统性阅读,覆盖范围:
| 模块 | 主要文件 |
|---|---|
| Core | GameManager, GameStateMachine, SceneLoader, DeathRespawnService, ServiceLocator, AssetLoader, AddressKeyRegistry, DifficultyManager, SaveManager, WorldStateRegistry, EventChannelRegistry |
| Core.Events | BaseEventChannelSO, CompositeDisposable, EventBusMonitor, IValidatable |
| Player | PlayerController, PlayerMovement, ParrySystem, SpellManager |
| Equipment | EquipmentManager, StatModifierEffect |
| Combat | HitBox, ClashResolver, StatusEffectManager |
| EventChain | EventChainManager, EventChainSO |
| Quest | QuestManager |
| Progression | AchievementManager |
| World | WorldStateRegistry, MapManager, ShopController, LiquidZone, PuzzleInterfaces |
| VFX | PaletteSwapSystem |
| Tutorial | TutorialManager |
| Support | DebugCheatSystem, SpeedrunTimer, SteamPlatformService |
| Editor | EventBusMonitorWindow, BossSkillSequenceWindow, SOValidationRunner, AddressReferenceGraphWindow |
评分标准:以成熟商业 2D Action RPG 框架水准(类 Hollow Knight / Celeste 量级代码质量)为基准,不要求向下兼容兜底,强调框架纯净性与数据逻辑一致性。
二、各维度评审
2.1 架构设计 Architecture Design — 9.1 / 10
亮点
事件驱动解耦彻底
BaseEventChannelSO<T> SO 频道模式落实全面,无任何 SendMessage / BroadcastMessage / FindWithTag(已在 v5 修复 BD_MoveToPlayer 最后一处)。所有跨系统通信均通过频道 SO 或 ServiceLocator 接口,程序集依赖始终单向:Core.Events → Core.Save → Core → 业务模块。
纯 C# 游戏状态机
GameStateMachine 不继承 MonoBehaviour,ValidNextStates 守卫确保状态转移合法,Tick(float dt) 由 GameManager.Update 托管,生命周期职责清晰分离。
ServiceLocator 接口契约统一
所有服务注册均面向接口(Register<IXxx>(this)),实现与接口在同一程序集。OverrideForTest / Reset 仅编译进 Editor,生产代码零开销。
EquipmentContext struct 捆绑模式
护符效果(ICharmEffect.OnEquip/OnUnequip)通过 EquipmentContext 接收全部依赖,而非硬编码访问 PlayerController 单例,依赖倒置彻底,测试友好。
StatusEffectManager 开放/封闭设计
RegisterEffectFactory(DamageType, Func<StatusEffect>) 允许外部(Boss 模块、道具模块)在运行时注入新效果类型,无需修改基类——符合 OCP。双结构(List + Dictionary)在顺序遍历与 O(1) 类型查找间取得平衡。
ClashResolver 去重方案无碰撞风险
用 (Math.Min(a,b), Math.Max(a,b)) 元组而非 XOR 哈希,逐帧 HashSet.Clear(),同帧双方各触发的回调只处理一次,设计细节严谨。
EventChannelRegistry 动态查找兜底
动态 Prefab(CharmEffect SO 等无法持有 [SerializeField] 的对象)可通过 IEventChannelRegistry.Get<T>(key) 按名称查找频道,避免在非 MonoBehaviour 中出现 FindObjectOfType 调用。
不足
A-1(中)GameManager 暂停恢复无子状态记忆
Paused 状态退出时固定调用 RequestTransition(GameStates.Gameplay),若暂停发生在 BossFight 状态内,恢复后将错误进入 Gameplay。Boss BGM、HUD 显示逻辑将对应失效。
// GameManager.cs — 当前实现(问题)
case GameStates.Paused:
RequestTransition(GameStates.Gameplay); // ← 不应硬编码
break;
建议:GameManager 增加 _prePrePauseState 字段,进入 Paused 时记录,退出时恢复。
A-2(低)DeathRespawnService.StartGameOverCoroutine 用硬抛出版本
// 其余代码均使用 GetOrDefault
ServiceLocator.Get<ISceneService>() // ← GameOver 路径唯一用硬抛出的地方
若 ISceneService 在异常流程中已注销,会产生难以定位的 KeyNotFoundException。建议改为 GetOrDefault<ISceneService>()?.LoadMainMenuCoroutine(),一致性与容错性均更好。
2.2 性能 Performance — 8.6 / 10
亮点
PaletteSwapSystem — MaterialPropertyBlock
ApplyPalette() 通过 MaterialPropertyBlock 修改纹理属性,不触碰共享材质,零 GC,零实例化材质。形态切换热路径完全无分配。
SpeedrunTimer — 按整秒更新
_lastDisplayedSecond 缓存避免每帧字符串 $"{hours:00}:..." 重建,高精度 UI 只在整秒边界才分配一次字符串,设计细腻。
EventBusMonitor — #if UNITY_EDITOR 零运行时开销
Record() 调用与 _subscriberCount 字段均在 Editor 条件内,生产构建下 BaseEventChannelSO.Raise() 完全不产生额外开销。
PaletteCatalogSO — 懒初始化 + OnValidate 清理
_cache = new Dictionary... 在首次 TryGetPalette 时懒建,OnValidate() 置 null 触发重建——编辑器友好且运行时只初始化一次。
ClashResolver — 逐帧清理
LateUpdate 清除 _processedThisFrame,每帧内去重 HashSet 大小上限为场景中同帧碰撞数量(通常 < 10),内存占用可控。
不足
P-1(中)PlayerMovement.CheckGrounded — 分配版 OverlapBox
Physics2D.OverlapBox(pos, size, angle, mask) // 返回 Collider2D,内部分配
地面检测每帧执行,应改用 OverlapBoxNonAlloc 并预分配结果缓冲:
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
// 调用改为:
int count = Physics2D.OverlapBoxNonAlloc(pos, size, angle, _groundBuffer, mask);
P-2(中)ShopController.GetAvailableItems() — LINQ + 每次新建 List
return _inventory.DefaultInventory
.Take(_inventory.MaxDisplaySlots)
.Where(...)
.ToList(); // 每次 UI 刷新时分配新 List
UI 打开后若频繁刷新(如动画帧回调)会持续分配。建议缓存结果,仅在 _purchaseCounts / _soldUniqueItems 变更时重建(_isDirty 标记模式)。
P-3(低)MapManager.OnSave() — 每次存档分配 List
data.Map.ExploredRooms = _exploredRooms.ToList(); // 新 List 分配
data.Map.MappedRooms = _mappedRooms.ToList();
存档非高频操作,可接受,但若改为直接清空再填充已有 List 字段更优:
data.Map.ExploredRooms.Clear();
data.Map.ExploredRooms.AddRange(_exploredRooms);
P-4(低)SaveManager._saveables.Remove(s) — O(n) 列表移除
_saveables 是 List<ISaveable>,Unregister 时 Remove 为 O(n) 线性扫描。大型场景若存档对象超过 100 个,频繁进出房间会有轻微开销。可改为 HashSet<ISaveable> + 顺序快照:
// 保存时:
foreach (var s in _saveables.ToList()) s.OnSave(_current);
// 注销时 O(1):
_saveables.Remove(s); // HashSet.Remove = O(1) amortized
2.3 可扩展性 Scalability — 9.0 / 10
亮点
EventChain 条件系统纯 SO 扩展
ChainCondition 抽象基类 + CreateAssetMenu 派生类,新增触发条件只需创建新 SO 类型,零运行时代码修改,Designer 友好。
WorldStateRegistry — WorldObjectCategory 枚举扩展入口
增加新世界对象类别只需新增枚举值与对应语义化 API 方法,底层 Dictionary<Category, HashSet<string>> 自动扩展。
AchievementManager — 数据驱动条件注册
通过 _achievements: AchievementSO[] 数组驱动所有成就,运行时 _states 字典 O(1) 查找,新增成就无需修改 AchievementManager 代码。
StatusEffectManager.RegisterEffectFactory — 运行时注入
Boss 技能、特殊道具可在 Awake 后调用 RegisterEffectFactory 覆盖或扩展效果类型,不需要修改核心 Combat 程序集。
PlatformService — 编译守卫隔离
SteamPlatformService 全文在 #if UNITY_STANDALONE && STEAMWORKS_NET 内,切换/增加平台(如 Epic、Console)只需实现新 IPlatformService,无需修改业务代码。
不足
S-1(低)SpellManager 单槽注释未兑现
注释中提到"如需多槽可扩展为数组",但当前 _equippedSpell: SpellSO 为单字段,TryCastSpell() 也仅处理一个。若游戏后期引入双法术槽,改动面较广(Player UI、InputReader 需同步扩展)。建议尽早将槽数提取为 [SerializeField] private int _slotCount = 1,内部用 SpellSO[] 并按索引管理,API 改动最小。
S-2(低)UIManager Panel Stack 无 Z 层优先级管理
_panelStack: Stack<GameObject> 假设面板遵循严格 LIFO 顺序,若并发触发(如成就解锁弹窗 + 商店同时打开)会导致遮盖关系混乱。商业游戏通常需要优先级/层级系统(如 Overlay / Modal / Notification 分层)。
2.4 编辑器友好 Editor-Friendliness — 9.3 / 10
亮点
EventBusMonitorWindow — 生产级调试面板
Filter / Pause / AutoScroll / Clear 工具栏组合;逐帧渲染监听数为 0 的频道红色高亮(事件死区检测);列宽固定排版整洁。快捷键 Ctrl+Shift+E 即开即用。
BossSkillSequenceWindow — 甘特图可视化
Windup / Active / Recovery 三段时序条颜色区分;VulnerabilityWindow 绿色覆盖层;DurationNormalized < 0.1 变红警告;点击阶段条 PingObject 跳转资产——与商业工具集的差距已接近可忽略。
AddressReferenceGraphWindow — 孤儿 Key 检测
扫描全部 .cs 文件引用 AddressKeys.X 的模式,可视化孤儿 Key(无引用/无对应 Addressables)+ CSV 导出,有效防止地址字符串腐烂,属框架级质量保障。
SOValidationRunner — 构建时数据守卫
IPreprocessBuildWithReport(callbackOrder = 1)在每次构建前扫描所有 IValidatable SO,错误时 throw BuildFailedException 中止构建,将数据错误拦截在出包前。
PaletteCatalogSO.OnValidate
_cache = null 确保 Inspector 中修改调色板条目后立即生效,避免运行时用旧缓存,设计自洽。
WorldStateRegistry.OnEnable
每次 Enter Play Mode(或 Domain Reload)时 _states.Clear(),ScriptableObject 跨 PlayMode 状态残留问题彻底解决,无需用户手动重置。
不足
E-1(低)BossSkillSequenceWindow 无保存/分享功能
甘特图无法导出图片或 JSON 快照,多人协作时只能截屏分享,建议增加「导出 PNG」或「复制时序数据」按钮(低优先级)。
E-2(低)DebugCheatSystem 指令集无自动补全
控制台输入框为纯文本,Tab 补全、历史记录均未实现,开发效率受限。建议存 List<string> _history 并用 ↑↓ 键翻历史(约 30 行改动)。
2.5 使用便利性 Developer UX — 8.9 / 10
亮点
RAII 订阅模式统一
全框架均使用 _channel?.Subscribe(Handler).AddTo(_subs) + OnDisable → _subs.Clear(),订阅/注销代码模式零歧义,阅读一个文件即可掌握全框架约定。
TutorialManager — 去重幂等设计
ShowHint(hintId, text, duration) 内部已检查 _completedHints.Contains,调用方无需关心重复触发,API 表面积最小。
TryPurchase 返回 bool + TryEquipCharm 返回 string?
ShopController.TryPurchase 返回 bool(调用方只需知道成功/失败),EquipmentManager.TryEquipCharm 返回 string?(null = 成功,字符串 = 失败原因)——两种场景选择恰当的返回类型,API 表达力强。
GameStateId struct 值语义
if (state == GameStates.Gameplay) 直接比较,无装箱,无枚举强转,阅读直观。
EquipmentContext 一次传递所有依赖
护符效果实现者接收单一 ctx 参数即可访问 Stats / Feedback / EventChannels / Skills,无需各自持有引用,代码简洁。
不足
U-1(中)AchievementManager.Awake 缺少重复注册防护
private void Awake()
{
ServiceLocator.Register<IAchievementService>(this); // ← 无 GetOrDefault 防护
...
}
其余所有 Manager 均有 GetOrDefault != null → Destroy 模式(如 TutorialManager、QuestManager、DialogueManager),AchievementManager 是唯一例外,破坏一致性,多场景叠加时会抛 InvalidOperationException。
U-2(低)SpeedrunTimer 注释描述与行为不符
/// 使用 Time.unscaledDeltaTime 在游戏暂停时停止(不受 timeScale 影响)。
注释存在误导:unscaledDeltaTime 不受 timeScale 影响,意味着 Time.timeScale = 0 时定时器不会自动停止。实际依靠外部调用 PauseTimer() 控制 _paused 布尔值。注释应改为:
使用
Time.unscaledDeltaTime以免被 HitStop(timeScale < 1)拉慢;游戏暂停时须由外部调用PauseTimer()。
2.6 框架纯净度 Framework Purity — 9.3 / 10
亮点
编译守卫严格分层
DebugCheatSystem:#if UNITY_EDITOR || DEVELOPMENT_BUILD全文保护BD_MoveToPlayer等全部 BehaviorDesigner 任务:#if GRAPH_DESIGNERSteamPlatformService:#if UNITY_STANDALONE && STEAMWORKS_NETEventBusMonitor.Record():#if UNITY_EDITOR内联
生产构建完全无调试/第三方残留,剥离彻底。
无全局 MonoBehaviour 单例
所有服务通过 ServiceLocator 按接口访问,无任何 public static Instance 或 DontDestroyOnLoad + FindObjectOfType 模式(SaveManager 私有 static _instance 仅用于内部防重复,不对外暴露)。
SO 事件频道零跨场景污染
BaseEventChannelSO.OnEventRaised 是 C# event(堆对象),频道 SO 资产本身不保存运行时状态;WorldStateRegistry.OnEnable 清理 SO 跨 PlayMode 状态——SO 资产永不携带运行时脏数据。
ICharmEffect 纯数据驱动
护符效果均为 [Serializable] POCO 类实现 ICharmEffect,不继承 MonoBehaviour / ScriptableObject,装配时零 Unity 序列化开销。
不足
PU-1(低)DebugCheatSystem.CmdHeal 用 FindFirstObjectByType
var player = FindFirstObjectByType<PlayerController>(); // dev-only 但破坏 ServiceLocator 一致性
虽有 #if 保护,框架层面 PlayerController 应通过 _onPlayerSpawned 事件频道或 ServiceLocator.GetOrDefault<IPlayerService>() 访问。FindFirstObjectByType 是框架约定的例外,可酌情整改。
2.7 数据逻辑一致性 Data Consistency — 9.1 / 10
亮点
WorldStateRegistry — 单一数据源
全部世界状态(Collectible / SavePoint / Door / Destroyed / Flag)统一存储在单 Dictionary<WorldObjectCategory, HashSet<string>> 中,OnStateChanged 事件提供响应式接口;LoadFromSave / GetAllFlags 完整对称,持久化路径无歧义。
SaveManager — 序列化完整性
双 Pass 序列化(先置 null 再计算 Checksum,再序列化含 Checksum 版本)确保存档文件自校验;SemaphoreSlim(1,1) 防止并发写入;_current.Meta.SaveCount++ 追踪覆写次数,利于调试。
EventChain — SO 运行时状态清理
ChainCondition.ResetState() 在每次 OnEnable 时调用,防止 SO 资产在编辑器反复进入 Play Mode 时携带旧条件状态,数据干净性有保障。
EquipmentManager — _usedNotches 缓存同步
护符格子数通过 _usedNotches 字段缓存(而非每次 LINQ Sum),装备/卸下时同步更新,查询 O(1) 且与真实状态始终一致。
QuestManager — 存档双向完整
_questStates + _objectiveStates 完整写入 / 恢复,OnLoad 时构建索引,存取路径对称,无悬空引用。
不足
DC-1(低)SceneLoader._currentRoomScene 加载失败时不清空
// SceneLoader.cs — LoadRoomAsync 失败路径未清理
catch (Exception e)
{
Debug.LogError(...);
// ← _currentRoomScene 仍保持旧值
}
若加载失败后再次加载同一场景,_currentHandle 卸载逻辑可能基于旧的错误场景名,导致内存泄漏。建议在 catch 块中明确 _currentRoomScene = null; _currentHandle = default;。
2.8 可测试性 Testability — 7.9 / 10
亮点
ServiceLocator.OverrideForTest / Reset
仅 Editor 编译的测试 API,可在 Unity Test Runner 的 [SetUp] / [TearDown] 中替换任意服务,无需修改生产代码。
IValidatable + SOValidationRunner
SO 数据合法性测试通过 Validate() 方法内嵌,单元测试可直接实例化 SO 并调用 Validate(),无需场景。
EventSubscription readonly struct
轻量无装箱,测试代码中可用 using var sub = channel.Subscribe(...) 保证解订。
接口隔离彻底
ISceneService / IDialogueService / IQuestManager / IAchievementService / IDifficultyService 等均可轻松 Mock,不依赖具体 MonoBehaviour 实现。
不足
T-1(中)GameManager 死亡流程用协程 + bool 标志
DeathFlow 协程通过 _waitingForDeathScreenInput bool 等待玩家输入,无法用 Unity Test Runner 的同步 API 覆盖,需要 PlayMode 测试,测试成本高。建议将死亡确认逻辑抽成 UniTask / 事件驱动异步方法,便于在测试中直接 Raise(confirmChannel)。
T-2(低)StatusEffectManager.Awake 内联工厂注册难覆盖
RegisterEffectFactory(DamageType.Fire, () => new FireEffect());
RegisterEffectFactory(DamageType.Poison, () => new PoisonEffect());
测试中若需替换 FireEffect 为 Mock,需在 Awake 后立即调用 RegisterEffectFactory 覆盖,初始化顺序敏感,单测需要注意 [SetUp] 时序。
三、综合问题清单
需修复(Bugs / 一致性破坏)
| ID | 严重度 | 模块 | 描述 |
|---|---|---|---|
| U-1 | 中 | AchievementManager | Awake 缺少 GetOrDefault 防重复注册守卫,多场景时会抛异常 |
| A-2 | 低 | DeathRespawnService | StartGameOverCoroutine 用 Get<ISceneService>()(硬抛出)而非 GetOrDefault |
| DC-1 | 低 | SceneLoader | 加载失败后 _currentRoomScene 不清空,存在内存泄漏风险 |
| U-2 | 低 | SpeedrunTimer | 注释"在游戏暂停时停止"描述与 unscaledDeltaTime 行为相悖,有误导性 |
建议优化(性能 / 架构改进)
| ID | 优先级 | 模块 | 描述 |
|---|---|---|---|
| P-1 | 中 | PlayerMovement | CheckGrounded 改用 OverlapBoxNonAlloc + 预分配缓冲 |
| P-2 | 中 | ShopController | GetAvailableItems() 增加 _isDirty 缓存,避免 UI 频繁刷新时 LINQ 分配 |
| A-1 | 中 | GameManager | 增加 _prePauseState 字段记录暂停前状态,resume 时恢复正确状态 |
| P-3 | 低 | MapManager | OnSave 复用已有 List 而非每次 ToList() 分配 |
| P-4 | 低 | SaveManager | _saveables 改为 HashSet 以获得 O(1) Remove |
| S-1 | 低 | SpellManager | 提前将单槽改为 SpellSO[],未来多槽改动面更小 |
| PU-1 | 低 | DebugCheatSystem | 用 ServiceLocator.GetOrDefault<IPlayerService>() 替换 FindFirstObjectByType |
四、各维度评分
| 维度 | 权重 | 本轮得分 | v5 得分 | 变化 |
|---|---|---|---|---|
| 架构设计 | 20% | 9.1 | 9.0 | +0.1 |
| 性能 | 18% | 8.6 | 8.5 | +0.1 |
| 可扩展性 | 15% | 9.0 | 8.8 | +0.2 |
| 编辑器友好 | 12% | 9.3 | 8.5 | +0.8 ↑ |
| 使用便利性 | 12% | 8.9 | 8.8 | +0.1 |
| 框架纯净度 | 8% | 9.3 | 9.3 | — |
| 数据一致性 | 8% | 9.1 | 9.2 | -0.1 |
| 可测试性 | 7% | 7.9 | 7.5 | +0.4 |
加权总分
Score = 9.1 \times 0.20 + 8.6 \times 0.18 + 9.0 \times 0.15 + 9.3 \times 0.12 + 8.9 \times 0.12 + 9.3 \times 0.08 + 9.1 \times 0.08 + 7.9 \times 0.07
= 1.82 + 1.548 + 1.35 + 1.116 + 1.068 + 0.744 + 0.728 + 0.553 = \mathbf{8.93 / 10}
较 v5(8.73)提升 +0.20,主要驱动因素:编辑器工具套件(+0.8)显著提升编辑器友好维度,v5 修复六项问题带动各维度小幅改善。
五、横向竞品对标参考
| 比较维度 | BaseGames v6 | Hollow Knight(逆向参考) | 典型中型商业框架 |
|---|---|---|---|
| 事件解耦 | SO 频道 + RAII | 手工 UnityEvent / 直接引用 | MVC/MVVM 混用 |
| 服务定位 | ServiceLocator + 接口隔离 | 静态 GameManager 访问 | DI 框架(Zenject 等) |
| 编辑器工具 | 3 个专用 EditorWindow | 基本 Inspector | 视项目规模 |
| 数据守卫 | IValidatable + 构建时中止 | 运行时检查 | 无或需手动测试 |
| 平台隔离 | 编译守卫 + 接口 | 内联 ifdef | 服务抽象层 |
BaseGames 框架在事件解耦与编辑器工具方面已超越多数中型商业项目的平均水准,主要差距集中在状态机子状态栈(暂停恢复逻辑)与可测试性(协程异步流)两点。
六、下一轮修复建议优先级
[必须修复] U-1 AchievementManager 补充重复注册防护
[必须修复] A-2 DeathRespawnService 改用 GetOrDefault
[建议修复] A-1 GameManager 暂停恢复记忆 _prePauseState
[建议修复] P-1 PlayerMovement OverlapBoxNonAlloc
[建议修复] P-2 ShopController GetAvailableItems 缓存
[低优先级] DC-1 SceneLoader 失败路径清空 _currentRoomScene
[低优先级] U-2 SpeedrunTimer 注释修正
[低优先级] P-3 MapManager OnSave 复用 List
[低优先级] P-4 SaveManager HashSet<ISaveable>
评审人:GitHub Copilot(Claude Sonnet 4.6)
上一版:FrameworkReview_2026_May_v5.md(加权 8.73)