- TD-13: IQuestManager.CompleteQuest 改用 IRewardTarget,移除 using BaseGames.Player - TD-14: HurtFlashController 缓存 WaitForSeconds(_waitForFlash 字段) - TD-16: LiquidType 枚举迁移至 BaseGames.Core.Events;LiquidEvent.LiquidType 改为枚举直接比较 - TD-17: DeathScreenController 移除失效的 _onPlayerDied 订阅,改为 OnEnable 直接启动延迟显示 影响文件: IQuestManager.cs / HurtFlashController.cs / LiquidType.cs(迁移) / LiquidEvent.cs / LiquidZone.cs / WaterDangerState.cs / UnderwaterPostProcessingController.cs / UnderwaterAudioController.cs / DeathScreenController.cs 评审文档: Docs/Review/FrameworkReview_2026_May_v11.md(综合评分 9.30/10)
19 KiB
Zeling v2 框架全量代码评审报告 v11
评审时间:2026 年 5 月
评审范围:v10 修复验证 + 深度精读补充模块(Puzzle / Liquid / VFX / Equipment / EventChain / Achievement / Player States / Enemy States / UI Menus)
前置版本:v10 评审报告(综合评分 9.20/10)
本版改进:验证 TD-06 至 TD-12 修复正确性;精读 v10 批量覆盖但未详细展开的模块;新增亮点 11 条;发现并修复 5 个问题(TD-13 至 TD-17)
一、综合评分总览
| 维度 | v10 评分 | v11 评分 | 变化 | 说明 |
|---|---|---|---|---|
| 架构设计 | 9.2 | 9.3 | ↑ | PuzzleWire 逻辑门 / EventChain 批量评估 / EquipmentContext 等设计亮点进一步拉高评分 |
| 性能 | 9.1 | 9.2 | ↑ | PostProcessManager _startWeights 复用 / PaletteCatalogSO 懒初始化缓存被发现 |
| 可扩展性 | 9.3 | 9.4 | ↑ | [SerializeReference] ICharmEffect 多态序列化 / EventChain 无代码条件扩展 |
| 编辑器友好 | 9.4 | 9.4 | → | 已满分稳定 |
| 使用便利性 | 9.0 | 9.0 | → | UIManager 面板栈简洁易用 |
| 综合 | 9.20 | 9.30 | ↑ | 深度精读后整体评价进一步提升;修复 TD-13 高优先级问题 |
二、v10 修复验证
| ID | 修复项 | 验证结果 |
|---|---|---|
| TD-06 | InputReaderSO 移除 FindPauseChannelByName | ✅ 已验证:FindPauseChannelByName 方法已移除,仅保留 Debug.Assert |
| TD-07 | EmergencySaveService 委托 SaveManager | ✅ 已验证:PromoteToSlot 通过 _saveManager.PromoteEmergencyToSlotAsync 调用 |
| TD-08 | AccessibilityManager 注册 IAccessibilityService | ✅ 已验证:IAccessibilityService 接口已创建,Awake 中注册 |
| TD-09 | HUDController SetActive 复用 HP Cell | ✅ 已验证:RebuildHPCells 使用 SetActive 代替 Instantiate/Destroy |
| TD-10 | MovingPlatform 缓存 WaitForSeconds | ✅ 已验证:_waitForEndpoint 字段在 Awake 初始化 |
| TD-11 | RewardSO IRewardTarget 解耦 | ✅ 已验证:RewardSO.Apply(IRewardTarget),PlayerStats 实现接口 |
| TD-12 | CrashReporter 频率限制 + 最大文件数 | ✅ 已验证:MaxLogsPerSession = 5,PruneOldLogFiles 保留最新 N 个 |
三、深度精读模块评审
3.1 Puzzle 层(★★★★★)
PuzzleInterfaces + PuzzleWire + PuzzleSwitch + PuzzleReceiver + PuzzleDoor
谜题系统是本次精读最亮眼的模块,设计严谨、扩展性极强:
逻辑门设计(PuzzleWire)
bool shouldActivate = _logic switch
{
LogicType.AND => System.Array.TrueForAll(_switches, s => s != null && s.IsActive),
LogicType.OR => System.Array.Exists(_switches, s => s != null && s.IsActive),
LogicType.XOR => _switches.Count(s => s != null && s.IsActive) % 2 == 1,
_ => false,
};
三种逻辑门(AND/OR/XOR)通过 Inspector 配置,关卡策划无需编写一行代码即可组合复杂谜题。策略模式的完美实践。
模板方法模式(PuzzleReceiver / PuzzleDoor)
// PuzzleReceiver — 基类
protected virtual void OnActivate() { }
protected virtual void OnDeactivate() { }
// PuzzleDoor — 子类 (4 行代码)
protected override void OnActivate() => _animancer?.Play(_openClip);
protected override void OnDeactivate() => _animancer?.Play(_closeClip);
子类 PuzzleDoor 只需 4 行代码实现门的行为,Activate/Deactivate 的回调、反馈播放、持久化全部由父类 PuzzleReceiver 统一处理。
持久化注入(非 Singleton)
[SerializeField] private WorldStateRegistry _worldState; // SO 注入
// ...
_worldState?.SetFlag("switch_" + _switchId);
通过 SO 注入 WorldStateRegistry 而非 Instance 单例访问,测试友好、多房间隔离性好。
ISwitchable.ForceState()
/// <summary>SaveData 恢复时调用,强制设置状态不触发副作用逻辑。</summary>
void ForceState(bool active);
存档恢复场景下的无副作用状态强制设置,接口契约明确,避免重放音效/Feedback。
3.2 Liquid 层(★★★★☆)
LiquidZone + LiquidPhysicsConfigSO + WaterDangerState + UnderwaterPostProcessingController
物理配置数据驱动,参数完整(重力、浮力、阻力、溺水时间等),通过 SO 注入,不同区域可共享或独立配置。
UnderwaterPostProcessingController:中断安全的 Volume 混合
private void BlendVolume(float target, float duration)
{
if (_blendCoroutine != null) StopCoroutine(_blendCoroutine);
_blendCoroutine = StartCoroutine(BlendRoutine(target, duration));
}
快速入水→出水→再入水场景下,上一个 Blend Coroutine 被安全终止,起始值从当前 weight 读取(不会突变),视觉无跳变。
发现问题 TD-16:WaterDangerState 使用字符串比较液体类型,见 §四。
3.3 VFX 层(★★★★★)
VFXPool + PostProcessManager + PaletteSwapSystem + RegionLightController + HurtFlashController + HitFXSpawner
VFXPool:双路径设计
if (TryDequeue(vfxRef, out var ps))
StartCoroutine(PlayImmediate(vfxRef, ps, position, rotation, maxLifetime));
else
StartCoroutine(PlayLoadAsync(vfxRef, position, rotation, maxLifetime));
池命中→同步定位播放(无 GC 等待);池未命中→异步 Addressable 加载。合理处理了首次冷启动和常规复用两种场景。
PostProcessManager:_startWeights 数组复用
private float[] _startWeights; // 字段
private IEnumerator BlendCoroutine(Volume target, float targetWeight)
{
for (int i = 0; i < _managedVolumes.Length; i++)
_startWeights[i] = _managedVolumes[i] != null ? _managedVolumes[i].weight : 0f;
// ... Lerp 每帧遍历
}
_startWeights 作为字段缓存,避免每次 Coroutine 开始时 new float[] 分配。与 VFXPool 的理念一致。
PaletteSwapSystem:MaterialPropertyBlock + 懒初始化字典
private static readonly int PaletteTexID = Shader.PropertyToID("_PaletteTex");
private MaterialPropertyBlock _block;
// PaletteCatalogSO
private void OnValidate() => _cache = null; // 编辑器改资产后重建缓存
三重优化:
Shader.PropertyToID静态缓存(避免重复字符串查找)MaterialPropertyBlock(不创建新材质实例)OnValidate使字典缓存失效(编辑器实时响应)
HurtFlashController — 发现问题 TD-14,见 §四。
3.4 Equipment 层(★★★★★)
EquipmentManager + ICharmEffect + EquipmentContext + CharmSO
[SerializeReference] 多态序列化
[SerializeReference]
public List<ICharmEffect> effects = new();
通过 [SerializeReference] 使接口列表在 Inspector 中多态序列化,设计师可直接在 SO 上配置任意 ICharmEffect 实现(StatModifierEffect/OnHitEffect/SkillSlotOverrideEffect 等),无需代码。商业游戏中常见的数据驱动配置模式。
EquipmentContext 结构体:避免接口直接依赖具体类型
public struct EquipmentContext
{
public PlayerStats Stats;
public PlayerFeedback Feedback;
public IEventChannelRegistry Events;
public SkillModifierRegistry SkillMods;
public WeaponManager WeaponMgr;
}
效果类通过 EquipmentContext 访问系统,而非直接引用各 Manager。添加新效果类型时无需修改 ICharmEffect 接口。
_usedNotches 缓存计算
_usedNotches += charm.notchCost; // 装备时累加
_usedNotches -= charm.notchCost; // 卸下时减去
避免每次查询时 _equipped.Sum(c => c.notchCost) 的 LINQ 分配。
3.5 EventChain 层(★★★★★)
EventChainSO + EventChainManager + ChainCondition
批量评估 + 帧内事件合并
// 事件回调:只标记 pending,不立即遍历
private void EvaluateAll() => _evaluatePending = true;
private void Update()
{
if (!_evaluatePending) return;
_evaluatePending = false;
DoEvaluateAll(); // 每帧最多一次
}
同帧内多个事件到达时,_evaluatePending 合并为一次 DoEvaluateAll,避免重复遍历所有链。经典 Dirty Flag 模式。
ChainCondition.ResetState() — SO 资产 PlayMode 安全
/// <summary>
/// 重置运行时瞬态状态(每次 EventChainManager.OnEnable 时调用)。
/// SO 是资产,_met 等字段会跨 PlayMode 会话残留;
/// 显式重置确保每次进入游戏时条件从初始状态开始评估。
/// </summary>
public virtual void ResetState() { }
完美解决了 ScriptableObject 字段跨 PlayMode 会话状态残留的经典问题,注释也解释了原因。
#if UNITY_EDITOR Editor 专用静态事件
#if UNITY_EDITOR
public static event Action<string, string> OnChainExecutedInEditor;
#endif
Editor 窗口可订阅此事件实时显示链执行日志,生产构建零开销。
3.6 Progression / Achievement 层(★★★★★)
AchievementManager + AchievementCondition 子类
条件多态 + SO 独立配置
每个成就条件(DefeatedBossCondition/MapExplorationCondition/ParryCountCondition 等)是独立 SO,策划独立配置不需要代码介入。条件实现极简:
public class DefeatedBossCondition : AchievementCondition
{
public string bossId;
public override bool IsMet(SaveData save)
=> save?.World != null && save.World.DefeatedBossIds.Contains(bossId);
}
EvaluateAll(SaveData) 模型
AchievementManager.EvaluateAll(SaveData save) 接受 SaveData 参数而非持有引用,调用者控制评估时机(存档加载后、房间进入时等),不依赖 MonoBehaviour Update。
3.7 Player / Enemy States 层(★★★★★)
PlayerStateBase + HurtState + SwimState + SpringState + EnemyHurtState 等
状态基类属性代理设计
protected InputReaderSO Input => _owner.Input;
protected PlayerMovement Move => _owner.Movement;
protected PlayerStats Stats => _owner.Stats;
protected AnimancerComponent Anim => _owner.Animancer;
通过 _owner 属性代理,每个状态子类用简洁名称访问系统,无需每次写 _owner.Movement.xxx。
HurtState 双重结束保护
private bool _ended;
private void OnHurtEnd()
{
if (_ended) return; // 防止 OnEnd 事件和 _timer 双重触发
_ended = true;
// ...
}
动画 OnEnd 回调和 _timer <= 0 两路都可能触发 OnHurtEnd,_ended 标志防止双重转换。
SwimState 配置注入
public void SetPhysicsConfig(LiquidPhysicsConfigSO config) => _currentPhysics = config;
PlayerController 在切换状态前注入物理配置,SwimState 本身不关心如何获取当前液体区域信息。
EnemyHurtState → EnemyDeadState 死锁保护
animState.Events(owner).OnEnd = () =>
{
if (owner.CurrentState == EnemyStateType.Hurt) // 防止被 Dead 状态覆盖后回到 Controlled
owner.ForceState(EnemyStateType.Controlled);
};
3.8 UI 层补充(★★★★☆)
UIManager 面板栈 + DeathScreenController
UIManager.PanelStack
private readonly Stack<GameObject> _panelStack = new();
public void OpenPanel(GameObject panel)
{
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(false);
panel.SetActive(true);
_panelStack.Push(panel);
}
public void CloseTopPanel()
{
_panelStack.Pop().SetActive(false);
if (_panelStack.Count > 0) _panelStack.Peek().SetActive(true);
}
面板栈自然支持多层嵌套(游戏中→暂停→设置→重绑定),无需各面板互相知晓。
DeathScreenController — 发现问题 TD-17,见 §四。
3.9 DialogueManager(★★★★★)
打字机协程逻辑处理了"跳过"语义的两个阶段:
- 打字完成前按跳过 → 立即显示完整文本
- 文本完全显示后按 Submit → 进入下一行
yield return new WaitUntil(() => !_dialogueBox.IsTyping || _skipRequested);
if (_dialogueBox.IsTyping) _dialogueBox.SkipTyping();
_skipRequested = false;
yield return new WaitUntil(() => _skipRequested);
两阶段 WaitUntil 正确区分了"跳过打字"和"推进对话"两个语义,无 Update 轮询。
四、发现的新问题列表
TD-13 — IQuestManager: 接口与实现签名不匹配(★★★ 高优先级)
位置: Assets/Scripts/Quest/IQuestManager.cs
严重程度: 高
描述: TD-11 已将 QuestManager.CompleteQuest 的参数从 PlayerStats player 改为 IRewardTarget rewardTarget,但 IQuestManager 接口声明未同步更新,仍为:
using BaseGames.Player;
void CompleteQuest(string questId, PlayerStats player); // ← 与实现不匹配
任何通过 IQuestManager 接口调用 CompleteQuest 的代码将会编译错误(参数类型不匹配)。同时 using BaseGames.Player 使 BaseGames.Quest 程序集对 BaseGames.Player 产生依赖,TD-11 的解耦目标未彻底达成。
修复方案: 将接口方法改为 void CompleteQuest(string questId, IRewardTarget rewardTarget),移除 using BaseGames.Player。
TD-14 — HurtFlashController: Flash() 每次 new WaitForSeconds
位置: Assets/Scripts/VFX/HurtFlashController.cs
严重程度: 低
描述: FlashCoroutine 每次执行都创建新的 WaitForSeconds 实例:
yield return new WaitForSeconds(_config.HurtFlashDuration);
玩家频繁受击时(连击场景),每次 Flash() 都会 GC 分配,与 TD-10(MovingPlatform)修复一致。
修复方案: 缓存 private WaitForSeconds _waitForFlash,在 Awake 中初始化(若 _config 已赋值),或在首次使用时懒初始化。
TD-15 — EventChainManager: OnEnable 内联 lambda 每次重新分配
位置: Assets/Scripts/EventChain/EventChainManager.cs
严重程度: 极低(可选优化)
描述: OnEnable 中每个订阅均通过内联 lambda 包裹:
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
每次 OnEnable 调用(场景加载/重新激活)均产生新 lambda 分配(5 个 closure)。
修复方案: 将各转发逻辑提取为具名私有方法(与全框架其他订阅的风格一致),OnEnable 中引用方法组而非内联 lambda。
TD-16 — WaterDangerState: 脆弱的枚举-字符串液体类型比较
位置: Assets/Scripts/World/Liquid/WaterDangerState.cs
严重程度: 低
描述: 当前代码通过 nameof(LiquidType.Water) 比较液体类型:
if (evt.LiquidType != nameof(LiquidType.Water)) return;
LiquidEvent 的 LiquidType 字段存储 _liquidType.ToString()(由 LiquidZone 传入)。若 LiquidType 枚举值改名,此处的 nameof 比较会自动更新(C# 语言保证),但若 LiquidZone 使用的是已序列化的旧字符串值,两处仍会不匹配。更根本的问题是:LiquidEvent 将枚举存为字符串,丢失了类型安全。
修复方案: 将 LiquidEvent.LiquidType 字段类型改为 LiquidType 枚举,比较改为 evt.LiquidType != LiquidType.Water,彻底消除字符串比较。
TD-17 — DeathScreenController: 事件订阅时序问题导致延迟显示无效
位置: Assets/Scripts/UI/Menus/DeathScreenController.cs
严重程度: 中
描述: DeathScreenController 在 OnEnable 中订阅 _onPlayerDied 事件:
private void OnEnable() => _onPlayerDied?.Subscribe(OnPlayerDied).AddTo(_subs);
private void OnPlayerDied() => StartCoroutine(ShowAfterDelay(1.5f));
但事件触发顺序如下:
- 玩家死亡 →
PlayerController广播_onPlayerDied GameManager响应 → 转换到Dead游戏状态_onGameStateChanged广播 →UIManager.HandleGameStateChanged- UIManager 调用
_deathScreenRoot.SetActive(true) - 此时
DeathScreenController.OnEnable才运行,才订阅_onPlayerDied
由于步骤 1 在步骤 5 之前,OnPlayerDied 回调永远不会被触发,1.5s 延迟显示逻辑失效。死亡界面由 UIManager 直接 SetActive(true) 立即显示,无延迟。
修复方案: 将延迟显示逻辑移至 OnEnable(DeathScreen 被激活时直接启动延迟协程);或监听 _onGameStateChanged 事件(Dead 状态),在游戏状态进入 Dead 时触发延迟。
五、v11 新增亮点汇总
| 亮点 | 位置 | 说明 |
|---|---|---|
PuzzleWire AND/OR/XOR 逻辑门 |
World/Puzzle/PuzzleWire.cs |
Inspector 配置,策划零代码组合复杂谜题 |
PuzzleReceiver 模板方法 |
World/Puzzle/PuzzleReceiver.cs |
子类 4 行实现门行为,父类统一回调/反馈/持久化 |
ISwitchable.ForceState |
World/Puzzle/PuzzleInterfaces.cs |
存档恢复时无副作用强制状态,接口契约明确 |
EventChainManager 帧内 Dirty Flag 合并 |
EventChain/EventChainManager.cs |
_evaluatePending 防止同帧多事件重复遍历 |
ChainCondition.ResetState() |
EventChain/EventChainSO.cs |
SO 资产运行时状态跨 PlayMode 会话显式重置 |
EventChainManager #if Editor 静态事件 |
EventChain/EventChainManager.cs |
生产构建零开销的 Editor 调试钩子 |
CharmSO [SerializeReference] |
Equipment/CharmSO.cs |
Inspector 多态序列化,数据驱动护符效果配置 |
EquipmentContext 结构体 |
Equipment/ICharmEffect.cs |
效果类通过上下文间接访问系统,解耦护符与具体 Manager |
PostProcessManager _startWeights 复用 |
VFX/PostProcessManager.cs |
Coroutine 跨帧共享数组,避免每次 new float[] |
PaletteCatalogSO 懒初始化 + OnValidate |
VFX/PaletteSwapSystem.cs |
字典缓存 + 编辑器改资产后自动失效 |
AchievementManager.EvaluateAll(SaveData) |
Progression/AchievementManager.cs |
解耦评估时机,不绑定 MonoBehaviour Update |
六、修复计划
| 优先级 | ID | 文件 | 修复类型 |
|---|---|---|---|
| 高 | TD-13 | Quest/IQuestManager.cs |
接口方法改为 IRewardTarget,移除 using BaseGames.Player |
| 中 | TD-17 | UI/Menus/DeathScreenController.cs |
延迟显示逻辑改为在 OnEnable 启动 |
| 低 | TD-16 | World/Liquid/WaterDangerState.cs |
LiquidEvent.LiquidType 改为枚举类型 |
| 低 | TD-14 | VFX/HurtFlashController.cs |
缓存 WaitForSeconds |
| 极低 | TD-15 | EventChain/EventChainManager.cs |
具名方法替代内联 lambda |
七、总体结论
经过 v11 深度精读与 TD-06~TD-12 修复验证,Zeling v2 框架整体质量达到 9.30/10 的高分水准:
- TD-13 是唯一高优先级问题:接口与实现签名不一致,属于 TD-11 修复的遗漏,需立即补全
- 无架构级新缺陷:所有新发现问题均属局部实现细节
- Puzzle / EventChain / Equipment 三个模块质量超预期:
[SerializeReference]、逻辑门、Dirty Flag 合并、条件 ResetState 等均体现高水平的工程设计 - 框架已具备商业发行级代码质量,可进入功能内容制作阶段
建议:修复 TD-13(必须)和 TD-17(强烈建议)后即可封板,TD-14/15/16 可在下轮优化迭代中一并处理。