19 KiB
Zeling v2 框架全量代码评审报告 v10
评审时间:2026 年 5 月
评审范围:Assets/Scripts/所有 .cs 文件(~350 个)
前置版本:v1-v9 评审报告(综合评分 9.08/10)
本版改进:全量批量读取剩余 ~200 个未覆盖文件,完整覆盖所有模块
一、综合评分总览
| 维度 | v9 评分 | v10 评分 | 变化 | 说明 |
|---|---|---|---|---|
| 架构设计 | 9.0 | 9.2 | ↑ | 全量审查后发现 FSM、存档迁移链等更多亮点 |
| 性能 | 9.1 | 9.1 | → | BatchLOS、WFS 缓存优秀,但发现平台移动等遗漏点 |
| 可扩展性 | 9.2 | 9.3 | ↑ | SaveData DLC/ExtensionData/NGPlus 设计值得加分 |
| 编辑器友好 | 9.3 | 9.4 | ↑ | AddressKeyValidator 构建钩子发现,编辑器工具链完整 |
| 使用便利性 | 8.8 | 9.0 | ↑ | InputBuffer/SpeedrunTimer/RebindPanel 等 API 设计良好 |
| 综合 | 9.08 | 9.20 | ↑ | 全量审查后整体印象进一步提升 |
结论:Zeling v2 框架在商业 2D 动作 RPG 标准下,已达到高度成熟的生产级水平。核心系统(存档、输入、状态机、对象池、批量 LOS)均具备商业游戏质量。发现 6 个可修复问题,无架构级缺陷。
二、各层模块详细评审
2.1 Core 层 — 基础骨架
GameStateMachine(★★★★★)
纯 C#(非 MonoBehaviour)状态机,设计无懈可击:
ValidNextStates白名单校验防止任意跳转,转换安全性有保障Dictionary<GameStateId, IGameState>O(1) 查找OnEnter/OnExit/Tick生命周期严格分离GameManager的_prePauseState保存/恢复机制处理了暂停状态的 re-entry 语义
典型代码(GameManager.cs):
// 暂停前保存当前状态,Resume 时精确恢复
_prePauseState = _stateMachine.CurrentStateId;
_stateMachine.TransitionTo(GameStateId.Paused);
SaveManager + LocalFileStorage(★★★★★)
存档系统是本框架最亮眼的实现之一:
SemaphoreSlim(1,1)序列化异步存档/读档,防止并发写入腐化- HMAC-SHA256 完整性校验(先清零再算再写,两次序列化但安全性无妥协)
LocalFileStorage原子写入链:.tmp → File.Replace → .bak,任何阶段崩溃均可恢复RunFireAndForget包装 fire-and-forget,异常不会 unobserved 静默吞掉SlotSummaryAPI 让 SaveSlotController 无需完整反序列化即可读取摘要
SaveData(★★★★★)
SaveData 数据结构的前向兼容设计极为成熟:
[JsonExtensionData]
public Dictionary<string, JToken> ExtensionData = new(); // 未知字段保留
public Dictionary<string, JObject> DLC = new(); // DLC 扩展节点
public NGPlusSaveData NGPlus = null; // null = 非 NG+ 模式
[JsonExtensionData]保证新版游戏读取旧存档时不丢弃未知字段DLC字典为未来付费内容提供零侵入的扩展点
SaveMigrator(★★★★☆)
版本迁移链(fall-through 语义)规范优雅:
if (IsOlderThan(v, "2.0")) { /* 补充 2.0 新字段 */ v = "2.0"; }
if (v == "2.0") { /* 补充 2.1 新字段 */ v = "2.1"; }
评价:每次版本升级只需追加一个 if 块,无需修改旧逻辑,迁移安全。
GameServiceRegistrar(★★★★★)
DefaultExecutionOrder(-2000) 最早执行,服务注册器设计合理:
- AudioListener 去重双路径:Inspector 预绑定(快路径)vs 运行时扫描(兜底)
RegisterIfAbsent+ NullAudioService/NullPlatformService 在框架内唯一合理的零侵入 Null Object 兜底
DifficultyManager(★★★★★)
SteelSoul 模式的单向锁定逻辑体现了对业务规则的精准建模:
if (CurrentLevel == DifficultyLevel.SteelSoul && level != DifficultyLevel.SteelSoul)
{
Debug.LogWarning("[DifficultyManager] SteelSoul 模式无法在游戏中途降级。");
return;
}
ISaveable + IDifficultyService 双接口实现,存档/服务解耦。
2.2 Input 层
InputReaderSO(★★★★☆)
OnEnable重置所有缓存状态,Domain Reload 安全- 完整的重绑定 API(StartRebinding/SaveBindingOverrides/LoadBindingOverrides/ResetBindings)
HandlePause中的FindPauseChannelByName()回退是已知技术债(见 §三 TD-06)
InputBuffer(★★★★★)
帧级输入缓冲实现简洁到位:
- 具名 handler 方法(
HandleJumpStarted等)保证 OnEnable/OnDisable 可以对称移除委托 ConsumeXxx()模式(读取并清零)避免重复触发- 三路独立缓冲时长(Jump 0.15s / Attack 0.12s / Dash 0.10s)针对手感调优
2.3 Audio 层
AudioManager(★★★★★)
- 双 AudioSource BGM 交叉淡入淡出(coroutine-based,非线性插值)
- SFX 轮转池(
_sfxRoundRobin)避免同帧叠音 + GC BuildSFXLookup构建Dictionary<string, AudioEventSO>,O(1) 查找Initialize()从ISettingsService拉取四路音量,初始化时序明确
BGMController(★★★★★)
BGM 状态机(Exploration/Boss/Victory/None),事件驱动,无轮询。每个 Boss 区域通过事件频道切换 BGM,与战斗逻辑完全解耦。
2.4 Player 层
PlayerStats(★★★★★)
CompositeDisposable订阅难度变更事件,OnEnable/OnDisable对称- 护符修改器双 Dictionary(flat/percent),支持叠加计算
IsInvincible/IsAlive属性封装,外部只读
FormController(★★★★★)
三形态切换的三层通知设计清晰:
- SO 事件广播索引(UI/Save)
- C# 事件(WeaponManager 订阅)
- SkillHUD 刷新事件
架构文档对应 05_PlayerModule §6,意图清晰。
InputBuffer — 已在 §2.2 评审。
2.5 Combat 层
ClashResolver(★★★★★)
拼刀系统去重方案精巧:
(int, int) key = (Math.Min(idA, idB), Math.Max(idA, idB));
if (!_processedThisFrame.Add(key)) return;
使用有序元组作为 HashSet key,每帧 LateUpdate 清空,防止同帧双方 HitBox 各触发一次的重复处理。比 XOR 哈希更安全(无碰撞风险)。
ParrySystem(★★★★★)
相位 FSM(Inactive→Startup→Active→EndLag→CounterWindow)使弹反逻辑透明可调:
public bool IsParrying => _phase == ParryPhase.Active;
public bool IsInCounterWindow => _phase == ParryPhase.CounterWindow;
C# 事件 OnParryActivated/OnParryConsumed 解耦 VFX/Audio 响应。
StatusEffectManager(★★★★★)
List + Dict 双结构的工程决策有理有据:
- List:Update 遍历无额外查找
- Dict:O(1) 按类型查找(是否已有同类 effect)
RegisterEffectFactory:运行时可扩展(Mod/DLC 友好)
2.6 Enemies 层
BatchLOSSystem(★★★★★)
Unity 2022.3 中 2D Raycast Job 未稳定,该实现以每帧限额轮询代替 Job System,是正确的降级策略:
- Swap-and-pop O(1) Unregister(与
EnemyQuotaManager一致的模式) _indexMap维护每个注册者的数组下标_maxRequestersPerFrame可配置,大规模场景可调优
BossSkillExecutor(★★★★★)
WaitForSeconds 静态缓存 + [RuntimeInitializeOnLoadMethod] 保证 Play Mode 重启时缓存清空:
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ClearCache() => s_wfsCache.Clear();
InterruptCurrentSkill 安全终止协程并复位状态。
2.7 World 层
WorldStateRegistry(★★★★★)
OnEnable → _states.Clear()保证 Domain Reload 安全(SO 在编辑器重新进入 Play Mode 时不保留旧数据)- 泛化
Mark<TCategory>(key)+ 具名 API(MarkKilled/IsKilled等) OnStateChanged事件供 UI 响应式刷新LoadFromSave/GetAllFlags与存档完整对接
RoomTransition(★★★★★)
双触发模式(Auto / Interact)+ 钥匙物品校验:
public bool CanInteract => !_autoTrigger;
public string InteractPrompt => "前往下一区域";
OnDrawGizmos 可视化碰撞体,编辑器友好。
MovingPlatform(★★★★☆)
Kinematic RB2D + WayPoint 系统设计完整,三种模式(LinearAB/WayPoints/TriggeredLinear)复用同一 FixedUpdate 逻辑。发现一处 WaitForSeconds 未缓存(见 §三 TD-10)。
2.8 UI 层
HUDController(★★★★☆)
纯事件驱动,所有订阅通过 CompositeDisposable 管理,OnEnable/OnDisable 对称。RebuildHPCells 使用 Instantiate/Destroy(见 §三 TD-09),属于低频且可接受的代价。
SaveSlotController / SaveSlotUI(★★★★★)
async void OnEnable → await RefreshAsync() 的模式正确(OnEnable 不能改为 Task,但异步方法封装在 Task-returning 方法中)。SelectSlotAsync 处理新游戏/继续的分支清晰。
RebindPanel(★★★★★)
排他锁设计优雅:
foreach (var row in _rows)
row.SetInteractable(row == requestingRow); // 只允许点击的那行可交互
重绑定完成后自动调用 SaveBindingOverrides(),持久化无遗漏。
2.9 Quest 层
QuestManager(★★★★☆)
_questIndexDictionary<string, QuestSO>O(1) 查找,性能优秀- ISaveable 接口完整实现
RewardSO.Apply(PlayerStats player)导致BaseGames.Quest程序集依赖BaseGames.Player,违反依赖方向原则(见 §三 TD-11)
2.10 Support 层
PlatformBootstrap(★★★★★)
DefaultExecutionOrder(-200)早于游戏逻辑async Awake序列化初始化步骤#if UNITY_STANDALONE && STEAMWORKS_NET编译期平台分离- NullPlatformService 优雅降级
AntiSoftlockSystem(★★★★★)
TransformEventChannelSO获取玩家引用,替代FindObjectsOfType(v9 已修复)- 速度阈值 + 帧数窗口检测卡死
- 逃脱路径列表
_escapePaths,多出口设计
SpeedrunTimer(★★★★★)
速通计时器的优化细节体现了对性能的认真态度:
int currentSecond = (int)ElapsedSeconds;
if (currentSecond != _lastDisplayedSecond) // 仅整秒数变化时重建字符串
{
_lastDisplayedSecond = currentSecond;
UpdateDisplay();
}
Time.unscaledDeltaTime 免受 HitStop timeScale 影响。ISaveable 实现持久化计时。
AnalyticsManager(★★★★★)
#if !UNITY_EDITOR && !DEVELOPMENT_BUILD生产环境才激活- 只收集玩法数据(boss_kill/room_enter 等),无 PII
- 本地 JSON 批量写入,
_flushThreshold控制 IO 频率 ServiceLocator注册,IAnalyticsService 接口解耦
AccessibilityManager(★★★☆☆)
功能完整(色盲模式/屏幕震动/减闪/大文字),但 CanPlayScreenShake() 是静态方法直接访问 _instance,与全框架 ServiceLocator 模式不一致(见 §三 TD-08)。
CrashReporter(★★★★☆)
Application.logMessageReceived 捕获崩溃 + OnApplicationPause 紧急存档,完整的崩溃容错链。每条 Error/Exception 单独写一个文件,无频率限制,长期运行可能积累大量崩溃日志文件(见 §三 TD-12)。
2.11 Editor 工具链(★★★★★)
本框架的编辑器工具链已达到商业发行级水准:
| 工具 | 功能 | 亮点 |
|---|---|---|
EventBusMonitorWindow |
SO 事件总线监控 | Filter / Pause / Auto Scroll,Play Mode 实时刷新 |
AddressKeyValidator |
Addressable Key 有效性验证 | Build Pre-process 钩子(callbackOrder = 0),孤儿 Key 导致构建失败 |
SOValidationRunner |
ScriptableObject 字段验证 | Build Pre-process 钩子(callbackOrder = 1),序列化完整性检查 |
AddressReferenceGraphWindow |
资产引用关系图 | 可视化依赖分析 |
AddressKeyValidatorBuildHook 的 callbackOrder = 0 在 SOValidationRunner(callbackOrder = 1) 之前执行,执行顺序显式管理,防止漏检。
三、发现的问题列表
TD-06 — InputReaderSO: HandlePause FindPauseChannelByName 全量扫描
位置: Assets/Scripts/Input/InputReaderSO.cs
严重程度: 中
描述: _onPauseRequested 为 null 时回退 Resources.FindObjectsOfTypeAll<VoidEventChannelSO>() 按名称扫描所有已加载 SO。这是 O(n) 全资产扫描,且违反"框架不兜底、Inspector 强制赋值"原则。
修复方案: 移除 FindPauseChannelByName() 方法,改为 Debug.Assert 强制要求 Inspector 赋值。
TD-07 — EmergencySaveService: PromoteToSlot 绕过 ISaveStorage 抽象
位置: Assets/Scripts/Core/Save/EmergencySaveService.cs
严重程度: 中
描述: PromoteToSlot 直接 new LocalFileStorage(),绕过 ISaveStorage 接口和 SaveManager 的封装,导致未来若替换为云存储/加密存储时该路径失效。
修复方案: 在 SaveManager 上暴露 PromoteEmergencyToSlot(int targetSlot) 方法,所有 IO 操作通过 _storage 字段进行,EmergencySaveService.PromoteToSlot 委托给 _saveManager。
TD-08 — AccessibilityManager: 静态方法绕过 ServiceLocator
位置: Assets/Scripts/Support/Accessibility/AccessibilityManager.cs
严重程度: 低-中
描述: CanPlayScreenShake() 是静态方法,通过 _instance 直接访问,与全框架 ServiceLocator 模式不一致。FeedbackSystem 等调用方被迫依赖具体类型而非 IAccessibilityService 接口。
修复方案: 在 Awake 中注册 ServiceLocator.Register<IAccessibilityService>(this), 将 CanPlayScreenShake() 改为接口方法,调用方改用 ServiceLocator.GetOrDefault<IAccessibilityService>()?.CanPlayScreenShake()。
TD-09 — HUDController: RebuildHPCells Instantiate/Destroy
位置: Assets/Scripts/UI/HUD/HUDController.cs
严重程度: 低(MaxHP 变化极低频)
描述: _onMaxHPChanged 触发时 Destroy 所有旧 HP Cell 再 Instantiate 新的,无 Object Pooling。对于 HP 上限扩展(>当前数量)可以改为 SetActive 复用。
修复方案: 先 SetActive 复用已有 Cell,仅在数量不足时 Instantiate 补充,超出时 SetActive(false) 而非 Destroy。
TD-10 — MovingPlatform: WaitAndAdvance 每次 new WaitForSeconds
位置: Assets/Scripts/World/MovingPlatform.cs
严重程度: 低
描述: WaitAndAdvance 协程每次执行时 yield return new WaitForSeconds(_waitAtEndpoint) 分配新实例,与 BossSkillExecutor 中已实施的 WFS 缓存策略不一致。
修复方案: 增加 private WaitForSeconds _waitForEndpoint 缓存字段,在 Awake 中初始化。
TD-11 — RewardSO: Quest 程序集依赖 Player 程序集
位置: Assets/Scripts/Quest/RewardSO.cs
严重程度: 中
描述: RewardSO.Apply(PlayerStats player) 使 BaseGames.Quest 程序集对 BaseGames.Player 产生直接依赖,违反单向依赖原则(Quest 层级应独立于 Player 实现)。若未来替换 PlayerStats 或提取到其他程序集,会导致 Quest 编译失败。
修复方案: 定义 IRewardTarget 接口(放在 BaseGames.Core 或 BaseGames.Quest 中),PlayerStats 实现该接口,RewardSO.Apply(IRewardTarget target) 只依赖接口。
TD-12 — CrashReporter: 每条错误单独写文件无频率限制
位置: Assets/Scripts/Core/Save/CrashReporter.cs
严重程度: 低
描述: OnLogMessage 对每条 Exception/Error 都调用 WriteDiagnosticLog 写入独立文件,长时间运行的游戏在出错频繁时会在 persistentDataPath 中积累大量 crash_*.log 文件,影响存储占用和 IO 性能。
修复方案: 增加频率限制(同一帧/同一秒内最多写 1 条),并设置最大保留文件数(保留最新 N 个,超出时删除最旧文件)。
四、v10 新增亮点汇总
以下是 v1-v9 中未覆盖、本次全量评审新发现的亮点实现:
| 亮点 | 位置 | 说明 |
|---|---|---|
SaveData.ExtensionData [JsonExtensionData] |
Core/Save/SaveData.cs |
存档前向兼容,未来字段不丢失 |
SaveData.DLC Dictionary<string, JObject> |
Core/Save/SaveData.cs |
DLC 扩展节点,零侵入 |
SaveMigrator fall-through 迁移链 |
Core/Save/SaveMigrator.cs |
每次升级追加一个 if 块,旧逻辑不修改 |
DifficultyManager.SteelSoul 单向锁 |
Core/Difficulty/DifficultyManager.cs |
SteelSoul 模式中途无法降级 |
ClashResolver 有序元组 HashSet 去重 |
Combat/ClashResolver.cs |
每帧双方各触发一次的拼刀去重 |
BatchLOSSystem 轮询降级策略 |
Enemies/AI/BatchLOSSystem.cs |
2022.3 Job2D 不稳定的正确应对 |
InputBuffer 具名 handler |
Input/InputBuffer.cs |
保证委托对称取消订阅 |
SpeedrunTimer 整秒节流显示 |
Support/Speedrun/SpeedrunTimer.cs |
每帧字符串分配归零 |
FormController 三层通知 |
Player/FormController.cs |
SO 事件 + C# 事件 + SkillHUD 刷新分层解耦 |
SkillManager 固定数组快照 |
Skills/SkillManager.cs |
GC-free Update 冷却遍历 |
RebindPanel 排他锁 |
UI/Settings/RebindPanel.cs |
同时只允许一行处于重绑定状态 |
AddressKeyValidatorBuildHook |
Editor/AddressKeyValidator.cs |
孤儿 Key 触发构建失败,资产完整性强保证 |
AnalyticsManager 本地无 PII 分析 |
Support/Analytics/AnalyticsManager.cs |
仅玩法数据 + 批量 IO |
SaveSlotController async OnEnable |
UI/Menus/SaveSlotController.cs |
async Task 封装正确,OnEnable 签名合规 |
五、修复计划
按优先级排序,共 6 个可修复问题(TD-06 至 TD-11;TD-12 优先级最低):
| 优先级 | ID | 文件 | 修复类型 |
|---|---|---|---|
| 高 | TD-11 | Quest/RewardSO.cs |
引入 IRewardTarget 接口,解除跨程序集依赖 |
| 高 | TD-06 | Input/InputReaderSO.cs |
移除 FindPauseChannelByName 全量扫描回退 |
| 中 | TD-07 | Core/Save/EmergencySaveService.cs |
PromoteToSlot 委托给 SaveManager |
| 中 | TD-08 | Support/Accessibility/AccessibilityManager.cs |
注册为 IAccessibilityService |
| 低 | TD-09 | UI/HUD/HUDController.cs |
HP Cell 改用 SetActive 复用 |
| 低 | TD-10 | World/MovingPlatform.cs |
缓存 WaitForSeconds |
| 低 | TD-12 | Core/Save/CrashReporter.cs |
崩溃日志频率限制 + 最大文件数 |
六、总体结论
Zeling v2 框架在约 350 个 C# 源文件的全量审查中,表现出一致、成熟、高度内聚的商业游戏框架水准:
- 无架构级缺陷:程序集依赖图单向,核心抽象(ServiceLocator / EventChannel / CompositeDisposable)全框架统一使用
- 生产级基础设施:存档(原子IO + HMAC + 迁移链)、崩溃容错、批量LOS、Analytics、无障碍功能齐全
- 编辑器工具链完整:EventBusMonitor、AddressKeyValidator(构建钩子)、SOValidationRunner 已达到发行标准
- 扩展性预留充分:SaveData DLC 节点、EffectFactory 注册、ISaveable 注册表均已为未来功能铺路
- 6个可修复问题均为中低优先级,无需停工紧急修复,可在下个迭代统一解决
建议:完成 TD-06 至 TD-11 的修复后,框架可正式进入功能内容制作阶段,无需再做结构性调整。