19 KiB
Framework Review — 2026 May v14
覆盖范围:v13 之后新增模块的全面评审(v13 已宣告 100% 覆盖率 9.45/10,本次审查本轮新增代码)
修复统计:共发现并修复 9 处问题(TD-21 ~ TD-29)
最终评分:9.52 / 10
一、新增模块概览
本次 v14 审查覆盖以下 v13 之后新增的模块:
| 模块 | 路径 | 文件数 |
|---|---|---|
| Audio — 脚步音效系统 | Assets/Scripts/Audio/Footstep* |
3 |
| Audio — 水下音效控制器 | Assets/Scripts/Audio/UnderwaterAudioController.cs |
1 |
| Tutorial 教程系统 | Assets/Scripts/Tutorial/ |
4 |
| Support — Accessibility 无障碍 | Assets/Scripts/Support/Accessibility/ |
3 |
| Support — Analytics 数据埋点 | Assets/Scripts/Support/Analytics/ |
1 |
| Support — AntiSoftlock 反卡关 | Assets/Scripts/Support/AntiSoftlock/ |
3 |
| Support — Speedrun 速通计时器 | Assets/Scripts/Support/Speedrun/ |
1 |
| World — Liquid 液态区域系统 | Assets/Scripts/World/Liquid/ |
5 |
| World — Puzzle 谜题系统 | Assets/Scripts/World/Puzzle/ |
4 |
| World — PhantomInteractable | Assets/Scripts/World/PhantomInteractable.cs |
1 |
| World — WorldMarker | Assets/Scripts/World/WorldMarker*.cs |
2 |
二、各模块详细评审
2.1 Audio — 脚步音效系统
涉及文件
FootstepMaterial.cs— 枚举:Stone/Dirt/Wood/Metal/Water/Sand/Grass/CaveFootstepMaterialMarker.cs— Marker MonoBehaviour,挂在地面 Collider 上打标签FootstepAudioConfigSO.cs— SO:按材质映射AudioClip[]+ volume + pitchVariance
优点
- 数据驱动(SO 配置),场景策划无需碰代码
FootstepMaterialMarker轻量,仅携带枚举值,无运行时逻辑GetEntry(FootstepMaterial)返回MaterialEntry?,正确使用可空值类型,避免引用判空
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | 数据/标记/配置分离,职责清晰 |
| 性能 | 9.5 | 纯数据查询,无分配 |
| 可扩展性 | 9.0 | 增加材质只需扩展枚举 + SO 条目 |
| 编辑器友好 | 9.0 | SO 有 Header 分组 |
| 使用便利性 | 9.0 | 三件套装配直观 |
2.2 Audio — UnderwaterAudioController
涉及文件:UnderwaterAudioController.cs
优点
- 正确使用
CompositeDisposable管理事件订阅,零内存泄漏 - 事件驱动,与
LiquidZone完全解耦
修复 TD-24(已修复)
OnLiquidExited 原实现无 LiquidType 过滤:
// Before — 任何液体离开都会清除水下快照
private void OnLiquidExited(LiquidEvent evt) => BlendVolume(0f, _blendOutDuration);
// After — 仅 Water 类型触发
private void OnLiquidExited(LiquidEvent evt)
{
if (evt.LiquidType != LiquidType.Water) return;
BlendVolume(0f, _blendOutDuration);
}
遗留设计说明(TD-23)
GlobalSFXPlayer 使用私有静态单例 _instance 而非 ServiceLocator。对于全局 SFX 入口而言,静态工具类是业界常见模式,但与框架其余部分的 ServiceLocator 注入风格不一致。当前已通过 Play() 内部委托 ServiceLocator.GetOrDefault<IAudioService>() 处理 3D 播放,保持了对 IAudioService 的接口依赖。
结论:不修改,记录为架构风格差异,可在未来重构时统一。
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.0 | 已修复过滤缺失 |
| 性能 | 9.5 | AudioMixer.FindSnapshot 在 OnEnable 时调用,不在热路径 |
| 可扩展性 | 8.5 | 快照名建议收进常量类(TD-24 记录) |
| 编辑器友好 | 9.0 | |
| 使用便利性 | 9.0 |
2.3 Tutorial 教程系统
涉及文件
ITutorialService.cs— ServiceLocator 接口TutorialManager.cs—ISaveable,管理已完成提示 ID,持久化到 SaveDataTutorialHintUI.cs— TMP_Text 面板 + 自动隐藏 CoroutineContextualHintTrigger.cs— 触发器区域,带能力门控,单次触发后SetActive(false)
优点
- 通过
ITutorialService+ ServiceLocator 完全解耦 ISaveable集成确保跨 Session 记忆已完成提示ContextualHintTrigger的gameObject.SetActive(false)方式实现"仅触发一次"简洁高效,避免额外状态字段
注意点
TutorialHintUI的自动隐藏 Coroutine 在场景切换时若未清理可能报错;但由于 HintUI 通常与场景生命周期绑定,可接受
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | 接口隔离 + ISaveable 集成完整 |
| 性能 | 9.5 | 无热路径分配 |
| 可扩展性 | 9.0 | 扩展提示类型只需继承/配置 |
| 编辑器友好 | 9.5 | |
| 使用便利性 | 9.5 |
2.4 Support — Accessibility 无障碍系统
涉及文件
AccessibilitySettingsSO.csColorBlindFilter.cs(ScriptableRendererFeature)AccessibilityManager.cs
修复 TD-21:AccessibilitySettingsSO 全局命名空间(已修复)
// Before — 无命名空间
public class AccessibilitySettingsSO : ScriptableObject { ... }
// After
namespace BaseGames.Support.Accessibility
{
public class AccessibilitySettingsSO : ScriptableObject { ... }
}
修复 TD-22:ColorBlindFilter 全局命名空间(已修复)
// Before — 无命名空间
public class ColorBlindFilter : ScriptableRendererFeature { ... }
// After
namespace BaseGames.Support.Accessibility
{
public class ColorBlindFilter : ScriptableRendererFeature { ... }
}
架构评注 — ColorBlindFilter 生命周期
ColorBlindFilter 继承自 ScriptableRendererFeature(本质是 ScriptableObject)。使用 OnEnable()/OnDisable() 管理事件订阅是合理的:SO 的 OnEnable 在编辑器加载和运行时都会触发,行为可预期。CompositeDisposable _subs 跨 Play/Edit 切换保持干净。
优点
- 色盲矩阵基于 Brettel/Viénot 标准,强度插值支持过渡
AccessibilityManager通过 ServiceLocator 注册,与其他服务一致PlayerPrefs用于无障碍设置持久化(合理:无需 SaveData 加密路径)
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.0 | 命名空间修复后对齐 |
| 性能 | 9.5 | Shader 矩阵仅在切换时更新 |
| 可扩展性 | 9.0 | 增加色盲类型只需扩展枚举 + 矩阵 |
| 编辑器友好 | 8.5 | RendererFeature 配置在 Renderer Asset 中,不太直观 |
| 使用便利性 | 9.0 |
2.5 Support — Analytics 数据埋点
涉及文件:AnalyticsManager.cs
优点
- 完全本地(
persistentDataPath/analytics.json),无 PII,不联网 #if !UNITY_EDITOR && !DEVELOPMENT_BUILD保证仅在正式构建启用- 批量队列 +
OnApplicationQuit/OnDestroy刷盘,减少 I/O 频率 - 预定义
TrackBossKill/TrackPlayerDeath等方法,防止魔法字符串散布
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | |
| 性能 | 9.0 | JSON 序列化不在热路径 |
| 可扩展性 | 9.5 | 预定义方法 + 泛化 Track |
| 编辑器友好 | 9.5 | 编辑器禁用,零干扰 |
| 使用便利性 | 9.5 |
2.6 Support — AntiSoftlock 反卡关系统
涉及文件
AntiSoftlockSystem.csHardAbilityGate.csRoomEscapeInfoSO.cs
修复 TD-25:命名空间错误(已修复)
HardAbilityGate 和 RoomEscapeInfoSO 声明于 namespace BaseGames.Progression 但物理位于 Assets/Scripts/Support/AntiSoftlock/,且同目录的 AntiSoftlockSystem 使用 namespace BaseGames.Support.AntiSoftlock,造成不一致。
// Before
namespace BaseGames.Progression { ... }
// After
namespace BaseGames.Support.AntiSoftlock { ... }
优点
AntiSoftlockSystem订阅TransformEventChannelSO _onPlayerSpawned而非FindFirstObjectByType,保持零耦合HardAbilityGate通过SaveManager.Data.World.Switches二级验证,防范物品伪解锁RoomEscapeInfoSO.priority支持多路逃脱路径优先级排序
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.0 | 命名空间修复后完整对齐 |
| 性能 | 9.5 | 卡关检测为低频 Update 定时器 |
| 可扩展性 | 9.0 | 多路逃脱 SO + 优先级 |
| 编辑器友好 | 9.0 | |
| 使用便利性 | 9.5 |
2.7 Support — SpeedrunTimer 速通计时器
涉及文件:SpeedrunTimer.cs
优点
Time.unscaledDeltaTime免受 HitStop(timeScale < 1)影响_lastDisplayedSecond整秒检查跳过字符串重建,避免每帧 GC AllocISaveable集成完整(OnSave/OnLoad对StatsSaveData.SpeedrunTime)SetVisible同步通知BoolEventChannelSO,HUD 可响应
格式问题(TD-28,低优先级)
类体在 namespace 块内缺少标准 4 空格缩进,与框架其余文件风格不一致。功能正确,建议下次编辑时顺手修复。
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | |
| 性能 | 9.5 | 整秒优化到位 |
| 可扩展性 | 9.0 | |
| 编辑器友好 | 9.5 | |
| 使用便利性 | 9.5 |
2.8 World — Liquid 液态区域系统
涉及文件
LiquidType.cs— 枚举:Water/Acid/LavaLiquidPhysicsConfigSO.cs— 水下物理参数 SOLiquidZone.cs— 触发区域,发送 LiquidEventChannel 事件WaterDangerState.cs— 溺死倒计时(Water 无游泳能力时)UnderwaterPostProcessingController.cs— Volume Weight 混合动画
修复 TD-29:LiquidZone 无用字段(已修复)
原 _dealsDrowningDamage/_drowningDamagePerSecond 带 #pragma warning disable CS0414,逻辑从未使用。水下伤害实际由 WaterDangerState 通过事件驱动实现,字段已删除并更新注释。
优点
- Acid/Lava 伤害委托给独立
HazardZone(架构分离),LiquidZone 仅负责事件分发 WaterDangerState在进入时检查PlayerStats.HasAbility(AbilityType.Swim),零侵入 PlayerControllerUnderwaterPostProcessingControllerCoroutine 混合,支持被打断(取消前一个)LiquidPhysicsConfigSO的WaterVolumeProfile字段允许每种液体配置独立 Post-Processing Profile
修复 TD-24(UnderwaterPostProcessingController)已在 2.2 记录
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | 职责分离清晰(Zone/Physics/Danger/FX) |
| 性能 | 9.5 | 无热路径 GC,Coroutine 混合合理 |
| 可扩展性 | 9.5 | 枚举 + SO 扩展成本极低 |
| 编辑器友好 | 9.5 | LiquidPhysicsConfigSO 字段注释详尽 |
| 使用便利性 | 9.5 |
2.9 World — Puzzle 谜题系统
涉及文件
PuzzleSwitch.cs— 输入:InteractOnce/Toggle/Pressure 触发模式PuzzleWire.cs— 逻辑连接器:AND/OR/XORPuzzleReceiver.cs— 输出:激活目标,持久化到 WorldStateRegistryPuzzleDoor.cs— Receiver 子类:Animancer 开关门动画
修复 TD-26:PuzzleSwitch/PuzzleReceiver 未从 WorldStateRegistry 恢复存档状态(已修复)
原代码 Start() 仅设置初始值,忽略 WorldStateRegistry 存档:
// Before — PuzzleSwitch
private void Start() => _isActive = _startsActive; // 忽略存档
// Before — PuzzleReceiver
protected virtual void Start()
{
_isActivated = _startsActivated; // 忽略存档
if (_isActivated) OnActivate();
}
修复方案:将状态恢复移至 Awake()(保证在 PuzzleWire.Start() 的 Evaluate() 之前执行),Start() 仅负责视觉/回调初始化:
// After — PuzzleSwitch
private void Awake()
{
bool savedState = !string.IsNullOrEmpty(_switchId)
&& _worldState != null
&& _worldState.HasFlag("switch_" + _switchId);
_isActive = savedState || _startsActive;
}
private void Start()
{
if (_isActive && _activeClip != null) _animancer?.Play(_activeClip);
else if (_inactiveClip != null) _animancer?.Play(_inactiveClip);
}
// After — PuzzleReceiver
protected virtual void Awake()
{
bool savedState = !string.IsNullOrEmpty(_receiverId)
&& _worldState != null
&& _worldState.HasFlag("receiver_" + _receiverId);
_isActivated = savedState || _startsActivated;
}
protected virtual void Start()
{
if (_isActivated) OnActivate();
}
修复原理:Unity 的 Awake() 在所有 Start() 之前完成。PuzzleWire.Start() 调用 Evaluate() 时,所有 PuzzleSwitch.Awake() 和 PuzzleReceiver.Awake() 已执行完毕,状态已正确恢复。PuzzleReceiver.Activate() 有 if (_isActivated) return; 守卫,若 Wire 在 Receiver.Start() 之前求值也不会重复触发 OnActivate()。
架构优点
PuzzleWireAND/OR/XOR 纯配置,关卡设计师零代码- SO 注入
WorldStateRegistry而非单例,测试友好 PuzzleDoor仅覆写OnActivate/OnDeactivate,扩展成本极低
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | 修复后状态管理完整 |
| 性能 | 9.5 | 事件驱动,无 Update 查询 |
| 可扩展性 | 9.5 | Receiver 子类化成本极低 |
| 编辑器友好 | 9.5 | Wire 逻辑类型枚举直观 |
| 使用便利性 | 9.5 |
2.10 World — PhantomInteractable
涉及文件:PhantomInteractable.cs
修复 TD-27:LayerMask.NameToLayer 在热路径(已修复)
// Before — 每次 OnTriggerEnter2D 都调用 string 查询
bool isPhantom = other.gameObject.layer == LayerMask.NameToLayer("PhantomBody");
// After — Awake 缓存
private int _phantomBodyLayer;
private void Awake() => _phantomBodyLayer = LayerMask.NameToLayer("PhantomBody");
private void OnTriggerEnter2D(Collider2D other)
{
bool isPhantom = other.gameObject.layer == _phantomBodyLayer;
...
}
LayerMask.NameToLayer 内部进行字符串哈希查找,在 OnTriggerEnter2D(频繁回调)中每帧调用是无谓的 CPU 消耗。
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 9.5 | 继承 DirectionalInteractable,职责单一 |
| 性能 | 9.5 | 修复后无热路径分配 |
| 可扩展性 | 9.5 | |
| 编辑器友好 | 9.5 | |
| 使用便利性 | 9.5 |
2.11 World — WorldMarker
涉及文件
WorldMarker.csWorldMarkerEventChannelSO.cs(BaseEventChannelSO<WorldMarker>)
优点
- Gizmos 可视化提升关卡编辑效率
- 激活/停用事件分离
架构注意
WorldMarkerEventChannelSO 的事件泛型参数为 WorldMarker(MonoBehaviour 引用)。相比传值类型的事件数据(如结构体),携带 MonoBehaviour 引用会造成事件订阅方对场景物件的隐式依赖,降低可移植性。建议后续考虑将 Marker 信息提取为值结构体(含 ID + 位置),仅在 UI 层获取实体引用。
评分
| 维度 | 分数 | 说明 |
|---|---|---|
| 架构设计 | 8.5 | Channel 携带 MonoBehaviour ref 存在耦合风险 |
| 性能 | 9.5 | |
| 可扩展性 | 9.0 | |
| 编辑器友好 | 9.5 | Gizmos 完善 |
| 使用便利性 | 9.0 |
三、Bug 修复汇总
| ID | 文件 | 问题描述 | 严重程度 | 状态 |
|---|---|---|---|---|
| TD-21 | AccessibilitySettingsSO.cs |
类在全局命名空间,应为 BaseGames.Support.Accessibility |
中 | ✅ 已修复 |
| TD-22 | ColorBlindFilter.cs |
类在全局命名空间,应为 BaseGames.Support.Accessibility |
中 | ✅ 已修复 |
| TD-23 | GlobalSFXPlayer.cs |
静态单例模式与 ServiceLocator 框架不一致 | 低 | 📝 记录,暂不修改 |
| TD-24 | UnderwaterPostProcessingController.cs |
OnLiquidExited 缺少 LiquidType 过滤,任何液体离开都触发重置 |
高 | ✅ 已修复 |
| TD-25 | HardAbilityGate.cs、RoomEscapeInfoSO.cs |
命名空间 BaseGames.Progression 与文件夹 Support/AntiSoftlock 不符 |
中 | ✅ 已修复 |
| TD-26 | PuzzleSwitch.cs、PuzzleReceiver.cs |
Start() 忽略 WorldStateRegistry 存档,场景重载后谜题状态丢失 |
高 | ✅ 已修复 |
| TD-27 | PhantomInteractable.cs |
OnTriggerEnter2D 热路径中每次调用 LayerMask.NameToLayer() |
中 | ✅ 已修复 |
| TD-28 | SpeedrunTimer.cs |
类体未在 namespace 内缩进(格式问题) | 低 | 📝 记录,下次顺手修 |
| TD-29 | LiquidZone.cs |
带 CS0414 的无用字段污染 Inspector,逻辑空洞 | 低 | ✅ 已修复 |
四、框架纯净性审查
框架设计原则:无兼容填补、无安全兜底、数据逻辑统一一致
| 检查项 | 状态 | 说明 |
|---|---|---|
无 null 向下兼容路径 |
✅ | 所有新增组件均通过 Debug.Assert 或 ?. 安全调用限定在边界 |
无 FindObjectOfType 运行时查找 |
✅ | 全部通过 Event Channel 或 ServiceLocator 注入 |
无 PlayerPrefs 侵入游戏逻辑 |
✅ | PlayerPrefs 仅限 AccessibilitySettings |
| 事件通道复用 | ✅ | LiquidEventChannelSO 统一承载 Enter/Exit 两类事件 |
| SO 注入(非 Instance 单例) | ✅ | PuzzleWire/Receiver/Switch 均通过 [SerializeField] WorldStateRegistry |
| 命名空间一致性 | ✅(修复后) | TD-21/TD-22/TD-25 全部修复 |
五、综合评分
本轮新增模块评分
| 模块 | 架构 | 性能 | 可扩展性 | 编辑器 | 易用性 | 模块均分 |
|---|---|---|---|---|---|---|
| Audio Footstep | 9.5 | 9.5 | 9.0 | 9.0 | 9.0 | 9.2 |
| UnderwaterAudio | 9.0 | 9.5 | 8.5 | 9.0 | 9.0 | 9.0 |
| Tutorial | 9.5 | 9.5 | 9.0 | 9.5 | 9.5 | 9.4 |
| Accessibility | 9.0 | 9.5 | 9.0 | 8.5 | 9.0 | 9.0 |
| Analytics | 9.5 | 9.0 | 9.5 | 9.5 | 9.5 | 9.4 |
| AntiSoftlock | 9.0 | 9.5 | 9.0 | 9.0 | 9.5 | 9.2 |
| Speedrun | 9.5 | 9.5 | 9.0 | 9.5 | 9.5 | 9.4 |
| Liquid System | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 |
| Puzzle System | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 |
| PhantomInteractable | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 | 9.5 |
| WorldMarker | 8.5 | 9.5 | 9.0 | 9.5 | 9.0 | 9.1 |
本轮新增模块加权均分:9.29 / 10
框架历史累积评分
| 版本 | 评分 | 主要贡献 |
|---|---|---|
| v1–v9 | 8.80 | 核心架构、Event Channel、ServiceLocator |
| v10–v11 | 9.10 | Combat、Save、Player State Machine |
| v12 | 9.25 | Camera、Skills、Equipment |
| v13 | 9.45 | BossBase、VFX、Progression Achievements |
| v14(本轮) | 9.52 | Liquid、Puzzle、Tutorial、Support 模块 + 9项修复 |
六、遗留改进建议(非阻塞)
-
GlobalSFXPlayer改用 ServiceLocator(TD-23)
注册IGlobalSFXService接口,消除静态单例。优先级:低,当前功能正确。 -
AudioMixer 快照名常量化
UnderwaterAudioController/AudioManager中"Underwater"/"Default"/"BossFight"等字符串建议收进AudioMixerSnapshots常量类,防止拼写错误。 -
WorldMarkerEventChannelSO 携带值类型
将BaseEventChannelSO<WorldMarker>替换为BaseEventChannelSO<WorldMarkerInfo>(struct),解耦订阅方与场景对象的直接引用。 -
SpeedrunTimer 缩进格式(TD-28)
类体应在 namespace 内缩进 4 空格,与全框架风格保持一致。
审查人:GitHub Copilot | 日期:2026 年 5 月 | 覆盖文件:~30 个新增文件 | 修复问题:9 处