Files
zeling_v2/Docs/Review/FrameworkReview_2026_May_v11.md
Joywayer 1135139bc6 v11 全量评审:修复 TD-13 至 TD-17
- 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)
2026-05-12 16:34:03 +08:00

19 KiB
Raw Blame History

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 = 5PruneOldLogFiles 保留最新 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-16WaterDangerState 使用字符串比较液体类型,见 §四。


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 的理念一致。

PaletteSwapSystemMaterialPropertyBlock + 懒初始化字典

private static readonly int PaletteTexID = Shader.PropertyToID("_PaletteTex");
private MaterialPropertyBlock _block;

// PaletteCatalogSO
private void OnValidate() => _cache = null;  // 编辑器改资产后重建缓存

三重优化:

  1. Shader.PropertyToID 静态缓存(避免重复字符串查找)
  2. MaterialPropertyBlock(不创建新材质实例)
  3. 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★★★★★

打字机协程逻辑处理了"跳过"语义的两个阶段:

  1. 打字完成前按跳过 → 立即显示完整文本
  2. 文本完全显示后按 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-10MovingPlatform修复一致。

修复方案: 缓存 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;

LiquidEventLiquidType 字段存储 _liquidType.ToString()(由 LiquidZone 传入)。若 LiquidType 枚举值改名,此处的 nameof 比较会自动更新C# 语言保证),但若 LiquidZone 使用的是已序列化的旧字符串值,两处仍会不匹配。更根本的问题是:LiquidEvent 将枚举存为字符串,丢失了类型安全。

修复方案: 将 LiquidEvent.LiquidType 字段类型改为 LiquidType 枚举,比较改为 evt.LiquidType != LiquidType.Water,彻底消除字符串比较。


TD-17 — DeathScreenController: 事件订阅时序问题导致延迟显示无效

位置: Assets/Scripts/UI/Menus/DeathScreenController.cs
严重程度: 中
描述: DeathScreenControllerOnEnable 中订阅 _onPlayerDied 事件:

private void OnEnable() => _onPlayerDied?.Subscribe(OnPlayerDied).AddTo(_subs);

private void OnPlayerDied() => StartCoroutine(ShowAfterDelay(1.5f));

但事件触发顺序如下:

  1. 玩家死亡 → PlayerController 广播 _onPlayerDied
  2. GameManager 响应 → 转换到 Dead 游戏状态
  3. _onGameStateChanged 广播 → UIManager.HandleGameStateChanged
  4. UIManager 调用 _deathScreenRoot.SetActive(true)
  5. 此时 DeathScreenController.OnEnable 才运行,才订阅 _onPlayerDied

由于步骤 1 在步骤 5 之前,OnPlayerDied 回调永远不会被触发1.5s 延迟显示逻辑失效。死亡界面由 UIManager 直接 SetActive(true) 立即显示,无延迟。

修复方案: 将延迟显示逻辑移至 OnEnableDeathScreen 被激活时直接启动延迟协程);或监听 _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 可在下轮优化迭代中一并处理。