# 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)** ```csharp 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)** ```csharp // 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)** ```csharp [SerializeField] private WorldStateRegistry _worldState; // SO 注入 // ... _worldState?.SetFlag("switch_" + _switchId); ``` 通过 SO 注入 `WorldStateRegistry` 而非 `Instance` 单例访问,测试友好、多房间隔离性好。 **ISwitchable.ForceState()** ```csharp /// SaveData 恢复时调用,强制设置状态不触发副作用逻辑。 void ForceState(bool active); ``` 存档恢复场景下的无副作用状态强制设置,接口契约明确,避免重放音效/Feedback。 --- ### 3.2 Liquid 层(★★★★☆) #### LiquidZone + LiquidPhysicsConfigSO + WaterDangerState + UnderwaterPostProcessingController 物理配置数据驱动,参数完整(重力、浮力、阻力、溺水时间等),通过 SO 注入,不同区域可共享或独立配置。 **UnderwaterPostProcessingController:中断安全的 Volume 混合** ```csharp 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:双路径设计** ```csharp if (TryDequeue(vfxRef, out var ps)) StartCoroutine(PlayImmediate(vfxRef, ps, position, rotation, maxLifetime)); else StartCoroutine(PlayLoadAsync(vfxRef, position, rotation, maxLifetime)); ``` 池命中→同步定位播放(无 GC 等待);池未命中→异步 Addressable 加载。合理处理了首次冷启动和常规复用两种场景。 **PostProcessManager:_startWeights 数组复用** ```csharp 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 + 懒初始化字典** ```csharp 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] 多态序列化** ```csharp [SerializeReference] public List effects = new(); ``` 通过 `[SerializeReference]` 使接口列表在 Inspector 中多态序列化,设计师可直接在 SO 上配置任意 `ICharmEffect` 实现(`StatModifierEffect`/`OnHitEffect`/`SkillSlotOverrideEffect` 等),无需代码。商业游戏中常见的数据驱动配置模式。 **EquipmentContext 结构体:避免接口直接依赖具体类型** ```csharp public struct EquipmentContext { public PlayerStats Stats; public PlayerFeedback Feedback; public IEventChannelRegistry Events; public SkillModifierRegistry SkillMods; public WeaponManager WeaponMgr; } ``` 效果类通过 `EquipmentContext` 访问系统,而非直接引用各 Manager。添加新效果类型时无需修改 `ICharmEffect` 接口。 **_usedNotches 缓存计算** ```csharp _usedNotches += charm.notchCost; // 装备时累加 _usedNotches -= charm.notchCost; // 卸下时减去 ``` 避免每次查询时 `_equipped.Sum(c => c.notchCost)` 的 LINQ 分配。 --- ### 3.5 EventChain 层(★★★★★) #### EventChainSO + EventChainManager + ChainCondition **批量评估 + 帧内事件合并** ```csharp // 事件回调:只标记 pending,不立即遍历 private void EvaluateAll() => _evaluatePending = true; private void Update() { if (!_evaluatePending) return; _evaluatePending = false; DoEvaluateAll(); // 每帧最多一次 } ``` 同帧内多个事件到达时,`_evaluatePending` 合并为一次 `DoEvaluateAll`,避免重复遍历所有链。经典 Dirty Flag 模式。 **ChainCondition.ResetState() — SO 资产 PlayMode 安全** ```csharp /// /// 重置运行时瞬态状态(每次 EventChainManager.OnEnable 时调用)。 /// SO 是资产,_met 等字段会跨 PlayMode 会话残留; /// 显式重置确保每次进入游戏时条件从初始状态开始评估。 /// public virtual void ResetState() { } ``` 完美解决了 ScriptableObject 字段跨 PlayMode 会话状态残留的经典问题,注释也解释了原因。 **#if UNITY_EDITOR Editor 专用静态事件** ```csharp #if UNITY_EDITOR public static event Action OnChainExecutedInEditor; #endif ``` Editor 窗口可订阅此事件实时显示链执行日志,生产构建零开销。 --- ### 3.6 Progression / Achievement 层(★★★★★) #### AchievementManager + AchievementCondition 子类 **条件多态 + SO 独立配置** 每个成就条件(`DefeatedBossCondition`/`MapExplorationCondition`/`ParryCountCondition` 等)是独立 SO,策划独立配置不需要代码介入。条件实现极简: ```csharp 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 等 **状态基类属性代理设计** ```csharp 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 双重结束保护** ```csharp private bool _ended; private void OnHurtEnd() { if (_ended) return; // 防止 OnEnd 事件和 _timer 双重触发 _ended = true; // ... } ``` 动画 OnEnd 回调和 `_timer <= 0` 两路都可能触发 `OnHurtEnd`,`_ended` 标志防止双重转换。 **SwimState 配置注入** ```csharp public void SetPhysicsConfig(LiquidPhysicsConfigSO config) => _currentPhysics = config; ``` PlayerController 在切换状态前注入物理配置,SwimState 本身不关心如何获取当前液体区域信息。 **EnemyHurtState → EnemyDeadState 死锁保护** ```csharp animState.Events(owner).OnEnd = () => { if (owner.CurrentState == EnemyStateType.Hurt) // 防止被 Dead 状态覆盖后回到 Controlled owner.ForceState(EnemyStateType.Controlled); }; ``` --- ### 3.8 UI 层补充(★★★★☆) #### UIManager 面板栈 + DeathScreenController **UIManager.PanelStack** ```csharp private readonly Stack _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 → 进入下一行 ```csharp 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` 接口声明未同步更新,仍为: ```csharp 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` 实例: ```csharp 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 包裹: ```csharp _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)` 比较液体类型: ```csharp 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` 事件: ```csharp 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)` 立即显示,无延迟。 **修复方案**: 将延迟显示逻辑移至 `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 可在下轮优化迭代中一并处理。