22 KiB
框架代码全量评审 v9 — 2026 May
范围:
Assets/Scripts/全量覆盖(v1-v9 累计)
对比基准:v8(综合 8.99/10,已修复 BreadcrumbTracker + GlobalObjectPool)
v9 新增覆盖:Audio、Cutscene、Dialogue、VFX、Equipment/Effects、Progression、EventChain、Tutorial、World/Puzzle 等 35+ 文件
评审人:GitHub Copilot(Claude Sonnet 4.6)
一、综合评分(v9)
| 维度 | v8 分数 | v9 分数 | 变化 | 说明 |
|---|---|---|---|---|
| 架构设计 | 9.3 | 9.4 | +0.1 | ICharmEffect OCP 模式 + EventChain 条件批评估优秀 |
| 性能 | 8.8 | 9.0 | +0.2 | DialogueUI StringBuilder、HurtFlash MaterialPropertyBlock、PostProcess 复用数组三项零分配亮点 |
| 可扩展性 | 9.2 | 9.3 | +0.1 | Equipment Effects 插件式扩展 + PuzzleReceiver 虚方法设计 |
| 编辑器友好 | 9.0 | 9.1 | +0.1 | CameraTriggerZone ExecuteAlways+OnDrawGizmos、EventChain 编辑器静态事件 |
| 使用便利性 | 9.0 | 9.0 | 0 | 维持高水位,部分小问题抵消 |
| 代码一致性 | 8.8 | 8.8 | 0 | GlobalSFXPlayer 仍使用 static singleton(同 AccessibilityManager) |
| 数据层设计 | 9.2 | 9.2 | 0 | 维持 |
| 综合 | 8.99 | 9.08 | +0.09 | — |
修复后预计:9.12 / 10
二、v9 新覆盖模块总览
| 模块 | 文件数 | 质量评价 |
|---|---|---|
Audio/ |
3 | GlobalSFXPlayer✓(架构约定问题),FootstepMaterialMarker⭐精简,UnderwaterAudioController⚠缺事件订阅 |
Camera/CameraTriggerZone |
1 | ⭐ ExecuteAlways + OnDrawGizmos 编辑器可视化范本 |
Cutscene/ |
2 | CutsceneSO 数据设计完整,CutsceneTrigger 4模式灵活 |
Dialogue/ |
2 | ⭐⭐ DialogueUI StringBuilder 零分配打字机 |
VFX/ |
5 | ⭐⭐ HurtFlashController 零 GC,PostProcessManager 数组复用 |
UI/ |
2 | BossHPBar 事件驱动完整,FloatingDamageText Canvas适配问题 |
Equipment/Effects/ |
7 | ⭐⭐ ICharmEffect OCP 设计极佳 |
Equipment/EquipmentManager |
1 | ⭐ Notch系统+ISaveable+Result模式 |
Progression/ |
3 | AchievementManager ISaveable 完整,HPContainerPickup 事件解耦 |
EventChain/ |
2 | ⭐ 批量评估 + 编辑器调试事件 |
Tutorial/ |
2 | TutorialManager ISaveable+HashSet O(1),能力门控优雅 |
World/ |
4 | SavePoint/DeathShade/Collectible 设计清晰 |
Core/Assets/ |
2 | AssetLoader + AssetReleaseTracker 组合完整 |
三、v9 正面亮点(新发现)
⭐⭐ P1:DialogueUI — StringBuilder 零分配打字机
// Assets/Scripts/Dialogue/DialogueUI.cs
private StringBuilder _sb = new StringBuilder(256);
private IEnumerator TypeLine(string fullText)
{
_sb.Clear();
for (int i = 0; i < fullText.Length; i++)
{
_sb.Append(fullText[i]);
_dialogueText.SetText(_sb); // TMP 直接接受 StringBuilder,零字符串分配
yield return _typeInterval;
}
}
意义:逐帧调用 TMP_Text.SetText(StringBuilder) 而非 text = string.Substring(...),完全规避打字机效果中每帧的字符串 GC。在对话密集型游戏中,这是生产级别的性能优化写法。
⭐⭐ P2:HurtFlashController — MaterialPropertyBlock 零 GC 着色器修改
// Assets/Scripts/VFX/HurtFlashController.cs
private static readonly int _flashColorId = Shader.PropertyToID("_FlashColor");
private static readonly int _flashAmountId = Shader.PropertyToID("_FlashAmount");
private MaterialPropertyBlock _mpb;
private void Awake() => _mpb = new MaterialPropertyBlock();
public void Flash()
{
if (_flashCoroutine != null) StopCoroutine(_flashCoroutine);
_flashCoroutine = StartCoroutine(DoFlash());
}
private IEnumerator DoFlash()
{
_renderer.GetPropertyBlock(_mpb);
_mpb.SetColor(_flashColorId, _flashColor);
for (float t = 0; t < 1f; t += Time.deltaTime / _flashDuration)
{
_mpb.SetFloat(_flashAmountId, Mathf.Lerp(1f, 0f, t));
_renderer.SetPropertyBlock(_mpb);
yield return null;
}
_mpb.SetFloat(_flashAmountId, 0f);
_renderer.SetPropertyBlock(_mpb);
}
意义:静态 Shader.PropertyToID 缓存 + MaterialPropertyBlock 修改 —— 每帧零材质拷贝、零 GC,标准 Unity 性能最佳实践。Flash 重入时 StopCoroutine + 重启 保证不叠层。
⭐⭐ P3:ICharmEffect — OCP 插件式装备效果
// 6 种效果:StatModifier / AttackSpeed / SoulSpell / OnHit / SkillNumeric / SkillSlotOverride / WeaponOverride
public interface ICharmEffect
{
void OnEquip(EquipmentContext ctx);
void OnUnequip(EquipmentContext ctx);
string GetEffectDescription();
}
// EquipmentContext 依赖注入(无硬引用)
public class EquipmentContext
{
public PlayerStats Stats;
public MMF_Player Feedback;
public SkillModifierRegistry SkillMods;
public WeaponManager WeaponMgr;
}
意义:完美符合开闭原则 —— 新增装备效果只需实现 ICharmEffect,不修改 EquipmentManager。EquipmentContext 依赖注入避免了各 Effect 直接 GetComponent 或访问 ServiceLocator。[Serializable] 标记使效果可在 Inspector 中堆叠配置。
⭐ P4:PostProcessManager — 数组复用避免频繁分配
private Volume[] _managedVolumes;
private float[] _startWeights; // 与 _managedVolumes 等长,复用
private IEnumerator TransitionWeights(float[] targets, float duration)
{
for (float t = 0; t < duration; t += Time.deltaTime)
{
float f = t / duration;
for (int i = 0; i < _managedVolumes.Length; i++)
_managedVolumes[i].weight = Mathf.Lerp(_startWeights[i], targets[i], f);
yield return null;
}
}
意义:复用 _startWeights 浮点数组,避免每次过渡创建临时数组,在过场/死亡/Boss战触发时无额外分配。
⭐ P5:EventChainManager — 帧合并批量评估
// 不管同帧内触发多少事件,Update 中只执行一次 DoEvaluateAll()
private void EvaluateAll() => _evaluatePending = true;
private void Update()
{
if (!_evaluatePending) return;
_evaluatePending = false;
DoEvaluateAll();
}
意义:场景加载时可能同帧触发多个条件事件(OnRoomEntered + OnAbilityUnlocked等),_evaluatePending 标志位确保所有条件都在同一帧内设置完毕后,只做一次 O(n×m) 的链遍历,避免重复评估。
⭐ P6:EventChainManager — 编辑器静态调试事件
#if UNITY_EDITOR
public static event Action<string, string> OnChainExecutedInEditor;
#endif
// 执行完成时推送
#if UNITY_EDITOR
OnChainExecutedInEditor?.Invoke(chain.chainId, "执行完成");
#endif
意义:条件编译静态事件,零运行时开销,供 EditorWindow 实时展示链执行日志,是框架调试能力的优秀设计。
⭐ P7:CameraTriggerZone — ExecuteAlways + OnDrawGizmos
[ExecuteAlways]
[RequireComponent(typeof(BoxCollider2D))]
public class CameraTriggerZone : MonoBehaviour
{
private void OnDrawGizmos()
{
Gizmos.color = new Color(0.2f, 0.5f, 1f, 0.25f);
Gizmos.DrawCube(transform.position, GetComponent<BoxCollider2D>().size);
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(transform.position, GetComponent<BoxCollider2D>().size);
}
}
意义:场景编辑时实时可视化触发区域,[RequireComponent] 防止遗漏依赖,是 Level Designer 友好设计的典范。
⭐ P8:TutorialManager — O(1) HashSet 完成状态 + ISaveable
private readonly HashSet<string> _completedHints = new();
public void ShowHint(string hintId, string text, float duration = 4f)
{
if (_completedHints.Contains(hintId)) return; // O(1) 查找
...
}
意义:使用 HashSet<string> 替代 List<string>.Contains(O(n)→O(1))。提示状态通过 ISaveable 持久化,跨存档不重复触发,设计完整。ContextualHintTrigger 的能力门控(HasAbility(AbilityType))进一步防止提前触发。
⭐ P9:AssetReleaseTracker — Addressables 生命周期精准管理
public class AssetReleaseTracker : MonoBehaviour
{
private readonly List<AsyncOperationHandle> _handles = new();
public void Track<T>(AsyncOperationHandle<T> handle) => _handles.Add(handle);
private void OnDestroy()
{
foreach (var h in _handles)
if (h.IsValid()) Addressables.Release(h);
_handles.Clear();
}
}
意义:挂在场景根节点,OnDestroy 自动批量释放,与 AssetLoader 配合形成完整的 Addressables 生命周期管理方案,有效防止场景卸载后的内存泄漏。
⭐ P10:LocalizationManager — 双层缓存 + ISaveable 语言持久化
// 双层 Dictionary:language+table → (key → value)
private readonly Dictionary<string, Dictionary<string, string>> _cache = new();
// ISaveable 持久化语言偏好(不用 PlayerPrefs)
public void OnSave(SaveData data) { data.Settings.Language = _currentLanguage; }
public void OnLoad(SaveData data) { SetLanguage(data.Settings.Language); }
意义:语言切换时仅替换一层缓存,查找 O(1)。语言偏好归入统一存档系统而非 PlayerPrefs,保持数据存储一致性。
⭐ P11:OnHitEffect — 装备/卸下时的正确 RAII 订阅管理
public class OnHitEffect : ICharmEffect
{
private IDisposable _sub;
public void OnEquip(EquipmentContext ctx)
{
_sub = ctx.HitEvents?.Subscribe(OnHitConfirmed);
}
public void OnUnequip(EquipmentContext ctx)
{
_sub?.Dispose();
_sub = null;
}
}
意义:装备时订阅事件、卸下时 Dispose,完美的 RAII 模式。与 MonoBehaviour 生命周期无关的订阅管理,防止卸下装备后继续响应命中事件。
四、v9 发现的问题
🔴 v9-P-1(低):UnderwaterAudioController 缺失事件订阅
位置:Assets/Scripts/Audio/UnderwaterAudioController.cs
问题:
// 当前:public 方法等待外部直接调用,与框架事件驱动模式不一致
public void EnterWater() { _mixer?.FindSnapshot("Underwater")?.TransitionTo(_transitionDuration); }
public void ExitWater() { _mixer?.FindSnapshot("Default")?.TransitionTo(_transitionDuration); }
同类组件 WaterDangerState、UnderwaterPostProcessingController 均正确订阅 LiquidEventChannelSO,UnderwaterAudioController 却是例外,形成不一致。若外部调用方(PlayerController 等)被移除,音频切换将静默失效。
修复:添加 LiquidEventChannelSO 引用和 OnEnable/OnDisable 自订阅(详见第六节)。
🟡 v9-A-1(低):GlobalSFXPlayer 使用 static _instance 单例
位置:Assets/Scripts/Audio/GlobalSFXPlayer.cs
问题:
private static GlobalSFXPlayer _instance; // 与框架 ServiceLocator 约定不一致
框架内全服务应通过 ServiceLocator.Register/Get<T> 管理,static _instance 是第二个此类例外(AccessibilityManager 为第一)。两者都有合理的使用场景(全局静态调用 API),但这种模式若扩散会侵蚀框架一致性。
建议:标注为"已知框架约定例外,仅允许此两处使用",或将 Play 改为通过 ServiceLocator.GetOrDefault<IAudioService>() 代理。本次不强制修复,记录为 Technical Debt。
🟡 v9-DC-1(低):Collectible.Despawn 未归还对象池
位置:Assets/Scripts/World/Collectible.cs
问题:
private void Despawn()
{
gameObject.SetActive(false); // 仅禁用,未归还 GlobalObjectPool
}
对于 Geo 类型(货币)等由 EnemyBase.OnDeath 实例化的 Collectible,频繁战斗会积累大量禁用 GameObject。正确做法是通过 IObjectPoolService.Return() 归还。
注:HPOrb 和场景内静态 Item 型 Collectible 不受影响(非运行时创建),此问题仅影响动态生成的 Geo。
建议:Despawn 改为调用 ServiceLocator.GetOrDefault<IObjectPoolService>()?.Return(gameObject),并配合 GlobalObjectPool 预热 GeoCollectible。本次标注为 TD,后续优化时处理。
🟡 v9-DC-2(低):FloatingDamageText 假设 Screen Space - Overlay Canvas
位置:Assets/Scripts/UI/FloatingDamageText.cs
问题:
_rectTransform.anchoredPosition = (Vector2)_cam.WorldToScreenPoint(currentWorld);
Camera.WorldToScreenPoint 返回屏幕像素坐标,赋值给 anchoredPosition 仅在 Canvas 为 Screen Space - Overlay、且 Canvas Scaler 为 Constant Pixel Size / Scale = 1 时正确。若使用 Screen Space - Camera 或不同分辨率缩放,坐标将偏移。
建议:改为 RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, screenPos, cam, out localPoint),使其适配所有 Canvas 模式。本次标注为 TD。
五、各模块深度评估(v9 新增部分)
5.1 Dialogue 模块
| 项 | 评价 |
|---|---|
DialogueUI.TypeLine StringBuilder |
⭐⭐ 零分配,TMP 最佳实践 |
WaitForSecondsRealtime |
正确:不受暂停影响 |
SkipTyping() |
立即完成打字,体验友好 |
InteractableNPC.virtual GetCurrentDialogue() |
虚方法设计,子类可根据世界状态返回不同对话 |
| 评分 | 9.6 / 10 |
5.2 Equipment 模块
| 项 | 评价 |
|---|---|
ICharmEffect + 7 种效果 |
⭐⭐ 完美 OCP,可无限横向扩展 |
EquipmentContext 依赖注入 |
避免 Effect 直接访问全局状态 |
UsedNotches 缓存值 |
避免每帧 LINQ Sum,O(1) 查询 |
TryEquipCharm Result 模式(null/string) |
简洁实用的错误信息返回 |
ISaveable 序列化装备槽 |
完整存档集成 |
ToolSlotManager(未读) |
需在后续确认是否一致 |
| 评分 | 9.5 / 10 |
5.3 EventChain 模块
| 项 | 评价 |
|---|---|
| 条件-动作分离(ChainCondition / ChainAction) | ⭐ 解耦设计,链可由 Designer 配置 |
_evaluatePending 帧合并批评估 |
⭐ 正确实现,Update + 标志位 |
cond.Register(this) 绑定中继事件 |
链条件自主订阅,无需外部注册 |
OnEnable cond.ResetState() |
防跨 PlayMode 状态残留 |
_completedChains HashSet |
O(1) 重复链保护 |
| 编辑器静态事件 | ⭐ 零运行时开销的调试能力 |
| 评分 | 9.4 / 10 |
5.4 Progression / Achievement 模块
| 项 | 评价 |
|---|---|
AchievementManager ISaveable |
进度持久化完整 |
Progress 0-1 浮点进度 |
UI 进度条友好 |
AchievementRuntimeState 运行时分离 |
不污染 ScriptableObject 资产 |
HPContainerPickup 事件驱动 |
零耦合,正确解耦 SaveSystem 操作 |
| 评分 | 9.1 / 10 |
5.5 Tutorial 模块
| 项 | 评价 |
|---|---|
HashSet<string> O(1) 完成查询 |
⭐ 正确数据结构选择 |
ISaveable 提示完成状态持久化 |
完整 |
ContextualHintTrigger 能力门控 |
优雅,避免提前触发 |
ShowHint + CompleteHint 分离 |
API 清晰,职责单一 |
| 评分 | 9.2 / 10 |
5.6 VFX 模块
| 项 | 评价 |
|---|---|
HurtFlashController MaterialPropertyBlock |
⭐⭐ 零 GC,生产级 |
PostProcessManager _startWeights 复用 |
⭐ 无额外分配 |
RegionLightController 双协程独立 tween |
优雅,可独立中断颜色/强度 |
VFXCatalogSO 延迟初始化 + Debug.Assert |
防止未初始化调用 |
| 评分 | 9.3 / 10 |
5.7 World 模块(新增部分)
| 项 | 评价 |
|---|---|
PuzzleReceiver 虚方法设计 |
⭐ 子类安全覆写,MMFeedbacks 集成 |
SavePoint ISaveable + IInteractable |
双接口实现,完整 |
DeathShade.Interact → 事件 → Destroy |
零耦合 Geo 回收 |
Collectible isPersistent 存档控制 |
设计清晰 |
HazardZone IsInstantKill MaxHP×2 |
功能正确,方案略显 hack |
| 评分 | 9.0 / 10 |
六、v9 修复项(本次执行)
Fix v9-P-1:UnderwaterAudioController 添加事件自订阅
修复前:仅有 public 方法,依赖外部直接调用
修复后:添加 LiquidEventChannelSO 订阅,与 WaterDangerState 保持一致的模式
七、历史修复复核(v7 + v8)
| 版本 | 修复项 | 状态 |
|---|---|---|
| v7-P-1 | HitStopManager _freezeEndTime + max 语义 |
✅ 已验证 |
| v7-A-1 | WallJumpState !Move.IsRising |
✅ 已验证 |
| v7-A-2 | PlayerController 删除 TryTransitionState 别名 | ✅ 已验证 |
| v7-U-2 | AttackState 删除重复事件取消订阅 | ✅ 已验证 |
| v7-P-2 | EnemyQuotaManager swap-and-pop + _indexMap | ✅ 已验证 |
| v7-S-1 | EnemyBase.SetAggroTickRate Debug.LogWarning | ✅ 已验证 |
| v8-P-1 | BreadcrumbTracker 事件频道订阅替换 FindWithTag | ✅ 已验证 |
| v8-P-2 | GlobalObjectPool.OnDestroy Addressables.Release | ✅ 已验证 |
八、完整框架技术债清单(截至 v9)
| ID | 优先 | 文件 | 描述 | 状态 |
|---|---|---|---|---|
| TD-01 | 低 | Audio/GlobalSFXPlayer.cs |
static _instance 与 ServiceLocator 约定不一致 | 已记录,暂缓 |
| TD-02 | 低 | Accessibility/AccessibilityManager.cs |
同 TD-01(v8 已记录) | 已记录,暂缓 |
| TD-03 | 低 | World/Collectible.cs |
Despawn 未归还 GlobalObjectPool(Geo 类型) |
已记录,暂缓 |
| TD-04 | 低 | UI/FloatingDamageText.cs |
WorldToScreenPoint 假设 Screen Space - Overlay Canvas |
已记录,暂缓 |
| TD-05 | 低 | Cutscene/CutsceneTrigger.cs |
flag 在 PlayCutscene 前写入(v9-DC-3) | 已记录,暂缓 |
九、框架整体架构评估(v9 最终版)
全局架构健康度:★★★★★ 9/10
┌─────────────────────────────────────────────────────────────────┐
│ 数据层 │
│ ┌──────────┐ ┌────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │SaveData │ │WorldState │ │AchievementSO │ │EventChainSO │ │
│ │(ISaveable│ │Registry │ │ProgressState │ │ Conditions │ │
│ │ pattern) │ │(纯 SO) │ │(Runtime分离) │ │ Actions(SO) │ │
│ └──────────┘ └────────────┘ └──────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ 服务层(ServiceLocator) │
│ IAudioService / ICameraService / IDialogueService │
│ ISaveService / ITutorialService / IAchievementService │
│ IObjectPoolService / ILocalizationService │
├─────────────────────────────────────────────────────────────────┤
│ 事件层(BaseEventChannelSO<T>) │
│ PlayerDied / BossDefeated / LiquidEntered / RegionEntered │
│ HitConfirmed / AbilityUnlocked / DialogueCompleted ... │
├─────────────────────────────────────────────────────────────────┤
│ 表现层 │
│ Equipment(ICharmEffect OCP) / VFX(MaterialPropertyBlock) │
│ Dialogue(StringBuilder TMP) / Tutorial(HashSet ISaveable) │
│ EventChain(批评估+编辑器调试) / Achievement(Progress 0-1) │
└─────────────────────────────────────────────────────────────────┘
架构核心优势:
- 单向依赖图:
Core.Events → Core.Save → Core → 业务模块,零循环引用 - 三层解耦:数据(SO/SaveData)→ 服务(ServiceLocator)→ 表现(MonoBehaviour),各层边界清晰
- 扩展点设计完整:
ICharmEffect(装备效果)、ChainCondition/Action(叙事链)、IActivatable(谜题接收器)、虚方法(PuzzleReceiver/InteractableNPC) - 内存意识高:全框架对高频路径(对话打字、命中闪烁、音量过渡)均有零分配实现
- 存档系统统一:语言偏好、成就进度、教程状态、装备槽、世界状态均通过
ISaveable统一管理,零 PlayerPrefs 散落
架构局限(已知 TD):
- 两处 static singleton 例外(GlobalSFXPlayer / AccessibilityManager)
Collectible.Despawn未使用对象池(Geo 动态生成场景)FloatingDamageTextCanvas 模式硬假设
十、v9 总结
v9 评审完成全量代码覆盖(v1-v9 累计约 120 个文件)。
本轮最重要发现:
- Equipment ICharmEffect 设计是整个框架最优雅的扩展点设计之一,7 种效果通过 12 行接口实现完全解耦,EquipmentContext 注入替代 GetComponent 是教科书级的依赖倒置
- DialogueUI StringBuilder 零分配打字机:体现了框架作者在看似简单的 UI 动画中的性能意识
- EventChainManager 帧合并评估 + Editor 调试事件:在同一文件中同时体现了运行时性能优化和开发工具设计能力
本次实际修复:1 项(UnderwaterAudioController 事件订阅一致性) 技术债记录:5 项,均为低优先级
综合评分:9.08 / 10(修复后预计 9.12)