# BaseGames Framework — 全量深度评审 > 评审时间:2026-05-12(基于上轮修复后的最新代码) > 评审范围:`Assets/Scripts/` 全目录(28 个子系统,全量阅读) > 评审标准:成熟商业动作 RPG(Unity 2022.3 LTS / C#) > 框架定位:新框架,不需要兼容,不需要兜底;追求纯净、统一、数据逻辑一致性 --- ## 目录 1. [总体评分](#1-总体评分) 2. [架构设计:系统级深析](#2-架构设计系统级深析) - [2.1 全局架构层次 & 依赖图](#21-全局架构层次--依赖图) - [2.2 事件系统](#22-事件系统) - [2.3 ServiceLocator 服务模型](#23-servicelocator-服务模型) - [2.4 存档系统](#24-存档系统) - [2.5 战斗系统](#25-战斗系统) - [2.6 玩家状态机](#26-玩家状态机) - [2.7 敌人系统](#27-敌人系统) - [2.8 任务 & 事件链](#28-任务--事件链) - [2.9 装备 & 护符](#29-装备--护符) - [2.10 世界系统](#210-世界系统) - [2.11 本地化系统(问题)](#211-本地化系统问题) - [2.12 Persistent 场景服务注册(问题)](#212-persistent-场景服务注册问题) 3. [性能](#3-性能) 4. [可扩展性](#4-可扩展性) 5. [编辑器友好性](#5-编辑器友好性) 6. [使用便利性](#6-使用便利性) 7. [问题清单(优先级排序)](#7-问题清单优先级排序) 8. [修复方案](#8-修复方案) 9. [综合结论](#9-综合结论) --- ## 1. 总体评分 | 维度 | 评分 | 说明 | |----------------|----------|------------------------------------------------------------------| | 架构设计 | ★★★★☆ | 系统设计精良,服务模型局部仍有接口覆盖缺口,本地化为异类设计 | | 性能 | ★★★★★ | 热路径全面优化;BatchLOS、SpeedrunTimer、EventChain 具备商业顶级水准 | | 可扩展性 | ★★★★☆ | SO 驱动覆盖率极高;StatusEffect 工厂、Boss 分阶段均为标准商业实践 | | 编辑器友好性 | ★★★★★ | 工具链丰富度超过绝大多数同体量商业框架 | | 使用便利性 | ★★★★☆ | 统一度高;本地化 & 部分 Manager 静态/具体类注册是体验洼地 | --- ## 2. 架构设计:系统级深析 ### 2.1 全局架构层次 & 依赖图 程序集依赖方向完全符合洁净架构原则: ``` ┌────────────────────────────────────────────────────────────┐ │ Editor (仅编辑器,无运行时引用) │ └───────────────────────────┬────────────────────────────────┘ │ ┌────────┬────────┬──────────┼──────┬─────────────────────────┐ │ UI │ World │ Quest │ ... │ 表现/业务层 │ └────┬───┴───┬────┴────┬─────┘ ... └──────────┬──────────────┘ │ │ │ │ ┌────┴───────┴─────────┴────────────────────────┴────────────┐ │ Combat / Player / Enemies / Audio / Skills / Equipment ... │ │ 游戏系统层 │ └─────────────────────┬───────────────────────────────────────┘ │ ┌──────────────────────┴──────────────────────────────────────┐ │ Core / Core.Save / Core.Events (服务 & 事件层) │ └─────────────────────────────────────────────────────────────┘ ``` 所有 `.asmdef` 依赖均单向向下,无循环。`BaseGames.Core.Events` 是纯净基础层,完全不依赖任何游戏程序集。 --- ### 2.2 事件系统 #### ✅ RAII 订阅模式 — 商业顶级 ```csharp // 全框架统一(除下述问题外已 100% 覆盖) private readonly CompositeDisposable _subs = new(); private void OnEnable() => _channel?.Subscribe(Handler).AddTo(_subs); private void OnDisable() => _subs.Clear(); ``` `EventSubscription` 为 `readonly struct`,订阅/取消订阅零堆分配。`CompositeDisposable` 一行批量清理,在生产规模的 Unity 项目中这是严格正确的做法。 #### ✅ EventBusMonitor — 超越商业标准 固定大小环形缓冲区(256 条),Editor 下零 GC 记录全部 SO 事件调用,含 payload 类型、订阅者计数、帧号、过滤搜索,自动滚动。此工具在国内外独立 AA 游戏中均属罕见。 #### ✅ EventChainManager — 高效的帧内去重 ```csharp private bool _evaluatePending; private void OnEnable() { _onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateAll(); }).AddTo(_subs); // ... } private void EvaluateAll() => _evaluatePending = true; private void Update() { if (!_evaluatePending) return; _evaluatePending = false; DoEvaluateAll(); } ``` 同帧内无论触发几个事件,只执行一次全量条件评估。这是事件链系统的标准性能优化手段。 --- ### 2.3 ServiceLocator 服务模型 #### ✅ 接口注册已覆盖核心服务 | 接口 | 注册处 | 说明 | |--------------------|-------------------------|----------------------------| | `IAudioService` | `GameServiceRegistrar` | 先注册 NullAudioService,后被 AudioManager 覆盖 | | `IDeathRespawnService` | `GameServiceRegistrar` | 正确 | | `ISceneService` | `GameServiceRegistrar` | 正确 | | `IEventChannelRegistry` | `EventChannelRegistry` | 正确 | | `ISaveService` | `GameServiceRegistrar` | Adapter 模式,正确 | | `IHitStopService` | `HitStopManager` | 本轮修复完成 | | `IQuestManager` | `QuestManager` | 正确 | | `IObjectPoolService` | `GlobalObjectPool` | 正确 | #### ⚠️ 遗漏接口(中优先级) 以下 Manager 注册时使用具体类而非接口,导致调用方对实现类产生依赖: | 类 | 当前注册 | 应注册为 | |---------------------|-------------------------------------------|-------------------| | `DialogueManager` | `Register(this)` | `IDialogueService` | | `AchievementManager`| `Register(this)` | `IAchievementService` | | `ProjectileManager` | `Register(this)` | `IProjectileService` | | `TutorialManager` | `Register(this)` | `ITutorialService` | | `AnalyticsManager` | `Register(this)` + 静态 facade | 见下节 | --- ### 2.4 存档系统 #### ✅ SaveData 结构设计优秀 `SaveData` 字段覆盖完整:Player、Equipment、World、Map、Quests、Achievements、Tools、ChallengeRooms、EventChains、Shops、Stats、NGPlus、Tutorial、DLC,无遗漏。 ```csharp [JsonExtensionData] public Dictionary ExtensionData = new(); public Dictionary DLC = new(); // DLC 专用隔离字段 ``` 双重前向兼容设计(`ExtensionData` 保留未知字段 + `DLC` 专用命名空间),工程化程度达到 AAA 水准。 #### ✅ ISaveable 接口统一性 — 优秀 以下 Manager 均正确实现 `ISaveable`: `QuestManager` / `EquipmentManager` / `AchievementManager` / `SpeedrunTimer` / `TutorialManager` / `BossProgressTracker` / `ChallengeRoomManager`。 存档数据流完全统一:`SaveManager.SaveAsync()` → 遍历 `ISaveable` → 写入 `SaveData` → JSON 序列化。无任何 Manager 绕过此机制直接写文件。 --- ### 2.5 战斗系统 #### ✅ HurtBox 8 步流水线 — 架构精良 ``` ① IsInvincible 或 HurtBoxInvincible → return ② 弹反窗口消费(ParrySystem.ConsumeParry)→ return ③ 霸体等级 ≥ Break 等级且不含 ForceBreak → HitConfirmed 广播后 return ④ 护盾层(IShieldable.AbsorbDamage)→ passThrough = 0 时 return ⑤ 防御减免(max(1, amount - Defense)) ⑥ IDamageable.TakeDamage(info) ⑦ 全局广播 EVT_DamageDealt ⑧ IStatusEffectable.ApplyStatusEffect(type) ``` 8 步均通过接口隔离,无任何具体类型直接依赖。`ParrySystem` 仅暴露 `bool` 窗口状态,伤害数据留在 Combat 层,完整跨程序集解耦。 #### ✅ StatusEffectManager 双结构设计 — 高性能 ```csharp private readonly List _activeList = new(); private readonly Dictionary _activeIndex = new(); ``` - `_activeList`:Update 时逆序遍历(避免移除时索引错位) - `_activeIndex`:O(1) 类型查找(叠层 / 净化 / 状态查询) - 工厂字典 `_effectFactories`:运行时可注册自定义效果,Boss 可扩展 `MaterialPropertyBlock` 修改 Shader 参数,不影响共享材质——正确的性能实践。 #### ✅ DamageInfo Builder 模式 ```csharp // 90% 场景用工厂方法 var info = DamageInfo.From(_sourceSO, atkDir, sourcePos, layer); // 复杂场景用 Builder var info = new DamageInfo.Builder() .SetRaw(50).SetType(DamageType.Fire).SetFlags(DamageFlags.CanBeParried) .SetBreak(BreakLevel.Medium).Build(); ``` `struct` 值类型,热路径零 GC;Builder 以 `class` 持有临时字段,仅在复杂构造时分配,是合理权衡。 --- ### 2.6 玩家状态机 #### ✅ 状态为纯 C# 类,零 MonoBehaviour 开销 `PlayerStateBase` 子类不继承 `MonoBehaviour`,由 `PlayerController` 统一驱动 `OnStateUpdate()` / `OnStateFixedUpdate()`。状态实例在 `Awake` 时创建后常驻,无频繁分配。 ```csharp // 状态字典使用 Type 为 key,查询 O(1) private readonly Dictionary _states = new(); ``` #### ✅ 数据驱动连击与 Animancer 帧事件 ```csharp // AttackState — 连击段数从 SO 读取,无硬编码 int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1; // HitBox 时间点由配置决定 events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground)); events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes()); ``` 新增攻击段数仅需修改 SO 资产,代码零改动。Animancer 归一化时间点事件保证帧率无关的命中判定,是 2D 动作游戏的工业级实践。 #### ✅ 冲刺无敌帧通过状态对象表达 ```csharp // DashState public override bool IsInvincible => true; // PlayerController.TakeDamage if (_currentState?.IsInvincible == true) return; // 不进入受击硬直 ``` 无敌帧语义清晰:状态本身声明是否无敌,控制器按声明行事,无任何硬编码状态类型判断。 #### ✅ Editor 状态转换白名单 ```csharp #if UNITY_EDITOR public virtual IReadOnlyList ValidTransitions => Array.Empty(); #endif ``` 编辑器下每次转换可验证合法性,Release 构建零开销。这是大型状态机调试的标准辅助手段。 --- ### 2.7 敌人系统 #### ✅ BatchLOSSystem — 高性能帧分布视线检测 ```csharp // 每帧轮询部分请求者(均匀分配,避免单帧全量射线) int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count); // Swap-and-pop:O(1) 无搬移删除 int idx = _indexMap[requester]; int last = _requesters.Count - 1; if (idx != last) { var moved = _requesters[last]; _requesters[idx] = moved; _indexMap[moved] = idx; } _requesters.RemoveAt(last); ``` 双结构(`List` + `HashSet` + `Dictionary` 三件套)确保:注册 O(1)、Unregister O(1)、Contains O(1)、帧内迭代 O(k)(k = 每帧预算数量)。是视线检测系统的教科书实现。 注释中已记录 Unity 2022.3 中 `Physics2D.RaycastCommand` 未稳定的限制,并给出升级路径(Job System),工程判断与文档意识兼备。 #### ✅ Behavior Designer 任务层接口化 所有 `BD_*` 任务均通过 `EnemyBase` 公共虚方法访问敌人能力(`MoveTo` / `BeginAttack` / `FacePlayer` 等),不直接操作具体组件。子类可重写任意方法,AI 任务无需修改。 ```csharp // BD_Attack.cs — 不依赖任何具体组件,全部通过 EnemyBase 接口 if (_enemy == null || !_enemy.CanAttack()) return TaskStatus.Failure; _enemy.BeginAttack(AttackType.Melee); ``` #### ✅ EnemyQuotaManager 波次结算 `EnemyBase` 通过 `public event Action OnDied` 通知 `ChallengeRoomManager`,而非广播全局 SO 事件,因为这是局部的波次结算通知,不需要跨系统传播。正确判断了何时用 C# event,何时用 SO EventChannel。 --- ### 2.8 任务 & 事件链 #### ✅ QuestManager — 接口 + RAII + O(1) 查找,完全正确 ```csharp // Awake:接口注册 ServiceLocator.Register(this); // Awake:构建 O(1) 查找索引 _questIndex = new Dictionary(_allQuests?.Length ?? 0); foreach (var q in _allQuests) if (q != null && !string.IsNullOrEmpty(q.questId)) _questIndex[q.questId] = q; // OnEnable:RAII 订阅 _onEnemyDied?.Subscribe(HandleEnemyDefeated).AddTo(_subs); ``` 与 SaveManager 的注册/注销通过 ServiceLocator 动态获取,不依赖静态实例: ```csharp private void OnEnable() => ServiceLocator.GetOrDefault()?.Register(this); private void OnDisable() => ServiceLocator.GetOrDefault()?.Unregister(this); ``` 这是框架内对 ISaveable 自动注册最干净的写法。 #### ✅ EventChainSO 条件状态重置机制 `EventChainManager.OnEnable()` 在绑定中继事件前调用 `cond?.ResetState()`,显式处理了 SO 资产在 Editor 重新进入 Play Mode 时的状态残留问题。这说明开发者清楚 SO 运行时状态的限制,主动防御。 --- ### 2.9 装备 & 护符 #### ✅ EquipmentContext 依赖封装 — 优秀设计 ```csharp private EquipmentContext _ctx = new EquipmentContext { Stats = GetComponent(), Feedback = GetComponent(), Events = ServiceLocator.GetOrDefault(), SkillMods = GetComponent(), WeaponMgr = GetComponent(), }; ``` `ICharmEffect.OnEquip(EquipmentContext ctx)` 接收上下文对象,护符效果无需直接引用 Player 上的各组件,依赖通过注入传递。新增护符效果只需实现接口,开闭原则完美遵守。 #### ✅ `TryEquipCharm` 结果通过返回值表达 ```csharp public string TryEquipCharm(CharmSO charm) { if (charm == null) return "护符不存在"; if (_equipped.Contains(charm)) return "已经装备"; if (!_collected.Contains(charm)) return "尚未收集此护符"; int remaining = _currentNotchCapacity - UsedNotches; if (charm.notchCost > remaining) return $"笔记不足(需要 {charm.notchCost},剩余 {remaining})"; // ... return null; // null = 成功 } ``` 返回 `null` 代表成功,`string` 代表失败原因——UI 直接用于显示,无需额外枚举类型。简洁务实。 --- ### 2.10 世界系统 #### ✅ WorldStateRegistry — ScriptableObject 作运行时状态容器 ```csharp private void OnEnable() => _states.Clear(); // 每次 Play Mode 进入时清空 ``` 统一 API `Mark(category, id)` / `IsMarked(category, id)` + 语义化快捷方法,O(1) 查询。注释明确说明 `OnEnable` 的作用(Domain Reload 兼容性),是有意识的 SO 运行时状态模式。 通过 `[SerializeField]` 注入而非 ServiceLocator 是合理选择:SO 不需要运行时实例管理,场景内组件直接引用资产即可。 #### ✅ 谜题系统接口化 — `ISwitchable` + `IInteractable` ```csharp public class PuzzleSwitch : MonoBehaviour, ISwitchable, IInteractable ``` `PuzzleWire` / `PuzzleReceiver` / `PuzzleDoor` 均通过接口通信,无直接类型依赖。新增谜题元素只需实现接口,无需修改任何现有代码。 #### ✅ RoomTransition — 数据驱动场景切换 `_requiresKeyItem` + `_requiredItemId` 通过 `WorldStateRegistry.IsCollected()` 检查。传送需求写在配置中,无游戏逻辑代码。 --- ### 2.11 本地化系统(问题) #### ⚠️ 问题:LocalizationManager 是静态类,游离于框架服务模型之外 ```csharp // 当前:静态类,无法通过 ServiceLocator 访问 public static class LocalizationManager { private static Language _currentLanguage = Language.ChineseSimplified; // 语言持久化使用 PlayerPrefs,绕过了 SaveData public static void SetLanguage(Language language) { PlayerPrefs.SetString("Language", language.ToString()); OnLanguageChanged?.Invoke(language); } public static void LoadSavedLanguage() { string saved = PlayerPrefs.GetString("Language", string.Empty); // ... } } ``` **问题 1 — PlayerPrefs 绕过 SaveData**:语言偏好是用户设置的一部分,与其他设置(如 `SettingsManager`)不一致地存入 PlayerPrefs,而非存档数据的 `SettingsSaveData` 字段。 **问题 2 — 静态类无法测试替换**:无法通过 ServiceLocator 覆盖语言服务,Editor 工具、测试套件无法在运行时注入不同语言实现。 **问题 3 — `PlayerPrefs.GetString` 在 Awake 使用字符串参数**:当前 `LoadSavedLanguage()` 使用字符串 `"Language"`,应迁移到 `GameIds` 或专属常量,防止魔法字符串散落。 --- ### 2.12 Persistent 场景服务注册(问题) #### ⚠️ TutorialManager.DontDestroyOnLoad 与框架约定冲突 ```csharp // TutorialManager.Awake() — 当前代码 private void Awake() { if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } ServiceLocator.Register(this); DontDestroyOnLoad(gameObject); // ← 自己管理生命周期 } ``` 与上轮已修复的 `EventChannelRegistry` 完全相同的问题:各 Manager 不应自行调用 `DontDestroyOnLoad`,由 Persistent 场景的根 GameObject 统一保证。此处两次 DontDestroyOnLoad 可能导致场景层级混乱。 #### ⚠️ AnalyticsManager — 静态 Facade 混合 ServiceLocator ```csharp // AnalyticsManager.cs — 混合设计 public class AnalyticsManager : MonoBehaviour // 实例注册到 ServiceLocator { // 但对外通过静态方法暴露 API public static void Track(string eventName, ...) { var inst = ServiceLocator.GetOrDefault(); if (inst == null || !inst._enabled) return; inst.Enqueue(eventName, parameters); } } ``` 这种 "静态门面调用实例" 的模式: - 调用者无法感知是否已注册(失败无声) - 无法通过接口替换实现(测试时难以 Mock) - 与框架 `ServiceLocator.GetOrDefault().DoSomething()` 的统一模式不一致 --- ## 3. 性能 ### 3.1 热路径零 GC 全覆盖 ✅ | 系统 | 优化手段 | |--------------------|---------------------------------------------| | `DamageInfo` | `struct`,Builder 仅复杂场景分配 | | `EventSubscription`| `readonly struct`,订阅/取消零 GC | | `EventBusMonitor` | 固定 256 环形缓冲区,零动态分配 | | `PlayerStateBase` | 纯 C# 类,无 MonoBehaviour,常驻零 GC | | `BatchLOSSystem` | 固定 List + 帧预算,无每帧分配 | | `SkillManager` | 固定大小数组复用(本轮修复) | | `HitBox` | `HashSet` / `Dictionary` 预设 capacity(8)(本轮修复)| | `GlobalObjectPool` | Addressables 异步预热 + Queue 复用 | | `SpeedrunTimer` | 仅在整秒变化时重建 TMP 字符串,每帧零 GC | | `WorldStateRegistry`| `HashSet` O(1) 查询,常驻内存 | ### 3.2 待改进点 **⚠️ 问题 P-1(低)**:`EquipmentManager.OnLoad` 中 `_equipped.ToList()` 创建临时列表,仅在加载存档时触发,影响可接受,但可用 for 循环向后遍历避免: ```csharp // 当前:分配 List 副本 foreach (var c in _equipped.ToList()) foreach (var fx in c.effects) fx?.OnUnequip(_ctx); _equipped.Clear(); // 改进:从末尾向前遍历,避免副本分配 for (int i = _equipped.Count - 1; i >= 0; i--) foreach (var fx in _equipped[i].effects) fx?.OnUnequip(_ctx); _equipped.Clear(); ``` **⚠️ 问题 P-2(低)**:`GlobalObjectPool._alive` 使用 `LinkedList` 追踪活跃对象,每次 `Spawn` 时分配 `LinkedListNode`。如果 alive 追踪仅用于限制上限,可改为纯计数器(`int _aliveCount`)消除节点分配。 **⚠️ 问题 P-3(低)**:`SceneLoader.LoadSceneCoroutine` 在加载新场景失败时(`AsyncOperationStatus != Succeeded`),旧场景已被卸载但新场景未成功加载,游戏将处于无场景状态。需补充失败恢复路径。 --- ## 4. 可扩展性 ### 4.1 ScriptableObject 驱动层 ✅ 商业顶级 | 领域 | SO 驱动范围 | |-------------|----------------------------------------------------------| | 护符 | `CharmSO.effects[]` → `ICharmEffect` 接口,新增护符只需资产 | | Boss | `BossSkillSO` + `SkillSequenceSO` + `AttackPatternSO` 三层 | | 技能 | `FormSkillSO` → 形态绑定技能,数据驱动 | | 状态效果 | `_effectFactories` 运行时注册,Boss 可注入自定义效果 | | 连击动画 | `PlayerAnimationConfigSO.GroundAttacks[]` 决定连击段数 | | 任务 | `QuestSO.branches` 分支,`RewardSO.Apply(player)` 奖励 | | 事件链条件 | `ChainCondition` 子类,`Register/Unregister` 接口 | | 谜题元素 | `ISwitchable` + `IInteractable` 接口,SO 数据驱动触发方式 | ### 4.2 StatusEffect 工厂模式 ✅ 最佳实践 ```csharp // Awake 注册标准效果 RegisterEffectFactory(DamageType.Fire, () => new FireEffect()); RegisterEffectFactory(DamageType.Poison, () => new PoisonEffect()); // Boss 或外部模块运行时注册自定义效果 statusEffectMgr.RegisterEffectFactory(DamageType.Frost, () => new FrostEffect()); ``` 工厂方法而非 switch/if-else,符合开闭原则,对未来 DLC 效果扩展友好。 ### 4.3 存档版本迁移架构 ✅ 路径已就绪 `SaveMigrator` 经本轮修复后版本号对齐,架构已预留分版本迁移方法: ```csharp if (data.Meta.Version == "1.0") MigrateFrom_1_0(data); if (data.Meta.Version == "2.0") MigrateFrom_2_0(data); ``` `SaveData.DLC` 专用字段 + `[JsonExtensionData]` 双重前向兼容,支持 DLC 包在不修改主存档结构的情况下独立扩展数据。 --- ## 5. 编辑器友好性 ### 5.1 工具链清单 ✅ 超越商业标准 | 工具 | 功能 | |-------------------------------|-------------------------------------------------------------| | `EventBusMonitorWindow` | 实时事件监控,环形缓冲,过滤搜索,订阅者计数 | | `SceneScaffoldTools` | 一键生成 Persistent 场景 GameObject 层级 + 自动资产绑定 | | `EventChainEditorWindow` | 可视化事件链编辑器 | | `BossSkillSequenceWindow` | Boss 技能序列可视化 | | `CreateEventChannelAssets` | 批量创建 EventChannel SO 资产 | | `AddressReferenceGraphWindow` | Addressables 引用关系图 | | `NavSurfaceBakeShortcut` | 快捷 NavSurface Bake | | `ScriptExecutionOrderTools` | 执行顺序管理 | | `ValidationSystem` | `IValidatable` 批量校验,Editor 下一键检查资产引用完整性 | | Combat / Equipment / World Editor | 各领域专属 Inspector 扩展 | ### 5.2 运行时调试支持 ✅ - `HurtBox.OnDrawGizmos()` — 三色可视化(激活/无敌/非激活) - `PlayerController._debugValidateTransitions` — `#if UNITY_EDITOR` 状态转换白名单验证 - `HitBox.Awake()` — 运行时验证 IsTrigger,错误时日志警告 - 所有 `[DefaultExecutionOrder]` 均有注释说明原因(`-2000` GameServiceRegistrar → `-1000` 事件注册 → `-200` LOS 系统 → `-100` PlayerController → `+50` UIManager) - `GameServiceRegistrar._primaryListener` — Inspector 预绑定可跳过全场景 `FindObjectsOfType` ### 5.3 GameIds — 统一字符串常量 ✅ ```csharp // 全框架统一 ID 常量 public static class GameIds { public static class Boss { public const string ForestBoss = "Boss_Forest"; ... } public static class Chain { public const string BossForestDefeated = "Chain_BossForest_Defeated"; ... } public static class Quest { ... } public static class Ability { ... } public static class Scene { ... } public static class Flag { ... } public static class Npc { ... } public static class Collectible { ... } } ``` 本地化字符串 Key(`LocalizationManager.Get("ui_start")`)和谜题 ID(`_switchId`)是框架内尚未纳入 `GameIds` 的字符串,存在魔法字符串风险(见问题清单 L-5)。 --- ## 6. 使用便利性 ### 6.1 数据流路径唯一 ✅ | 操作 | 唯一路径 | |------------------------|-------------------------------------------| | 事件广播 | `_channel?.Raise(payload)` | | 事件订阅 | `_channel?.Subscribe(H).AddTo(_subs)` | | 服务获取 | `ServiceLocator.GetOrDefault()` | | 存档注册 | `SaveManager.Register(this)`(ISaveable)| | 世界状态查询 | `_worldState.IsMarked(category, id)` | | 本地化文本(例外) | `LocalizationManager.Get("key")` ← 静态类| 除本地化外,所有操作路径唯一,无方式混用。 ### 6.2 Input 混合订阅 ✅ 合理 框架内共存两套订阅机制: - **EventChannel(SO)**:跨场景/跨程序集通信 - **C# event(InputReaderSO)**:玩家状态机内部输入响应 这是合理的设计选择:InputReaderSO 的 `event Action` 是状态机内部信号,不需要全局 SO 资产。`AttackState` / `ParryState` 订阅 Input C# event,于 `OnStateExit` 取消订阅——语义清晰,无泄漏风险。**保持现状,无需统一**。 ### 6.3 PlayerController 属性代理 ✅ 状态类友好 ```csharp // PlayerController 将所有依赖以只读属性暴露,状态类通过 Owner.X 访问 public PlayerMovement Movement => _movement; public PlayerStats Stats => _stats; public AnimancerComponent Animancer => _animancer; // ... ``` 状态类通过 `Owner.X` 访问,无需向构造函数传入多个参数,新增状态只需 `new MyState(owner)` 一行。 ### 6.4 `NullAudioService` 缺省服务 ✅ ```csharp // GameServiceRegistrar ServiceLocator.RegisterIfAbsent(new NullAudioService()); // AudioManager.Awake 后覆盖为真实实现 ``` 空对象模式(Null Object Pattern)确保任何场景下 `ServiceLocator.GetOrDefault()` 都不为 null,调用方无需判空。这是缺省服务的标准实现。 --- ## 7. 问题清单(优先级排序) ### 🔴 高优先级(直接影响框架一致性 / 数据正确性) | # | 文件 | 问题描述 | |-----|-------------------------------|-----------------------------------------------------------------------| | H-1 | `Tutorial/TutorialManager.cs` | `Awake()` 中调用 `DontDestroyOnLoad(gameObject)`,违反 Persistent 场景统一生命周期原则(同类问题已在 `EventChannelRegistry` 修复)| | H-2 | `Localization/LocalizationManager.cs` | 静态类游离于 ServiceLocator 体系之外;语言持久化走 `PlayerPrefs` 而非 `SaveData`,违背数据统一一致性原则 | ### 🟡 中优先级(影响架构纯净度 / 可测试性) | # | 文件 | 问题描述 | |-----|-------------------------------|-----------------------------------------------------------------------| | M-1 | `Dialogue/DialogueManager.cs` | 以具体类 `DialogueManager` 注册,无 `IDialogueService` 接口,调用方对实现类产生依赖 | | M-2 | `Progression/AchievementManager.cs` | 以具体类注册,无 `IAchievementService` 接口 | | M-3 | `Combat/ProjectileManager.cs` | 以具体类注册,无 `IProjectileService` 接口 | | M-4 | `Support/Analytics/AnalyticsManager.cs` | 混合静态 Facade + ServiceLocator 实例,模式不一致;`static Track()` 失败无声,无法 Mock | | M-5 | `Tutorial/TutorialManager.cs` | 以具体类注册,无 `ITutorialService` 接口(附于 H-1) | ### 🟢 低优先级(性能小改进 / 健壮性) | # | 文件 | 问题描述 | |-----|-------------------------------|-----------------------------------------------------------------------| | L-1 | `Equipment/EquipmentManager.cs` | `OnLoad` 中 `_equipped.ToList()` 分配临时列表;可用逆序 for 循环消除 | | L-2 | `Core/Pool/GlobalObjectPool.cs` | `_alive` 用 `LinkedList` 追踪活跃对象,每次 Spawn 分配 `LinkedListNode`;如仅用于上限检查可改为 `int _aliveCount` | | L-3 | `Core/SceneLoader.cs` | 场景加载失败时(`Succeeded == false`)旧场景已卸载但新场景未加载,缺少失败恢复路径 | | L-4 | `Localization/LocalizationManager.cs` | `PlayerPrefs.GetString("Language")` 使用 magic string,应迁移至 `GameIds` 或专属常量 | | L-5 | 多处 | 谜题 ID(`_switchId`)、本地化 Key、NPC ID 等字段尚有 magic string 散落,应扩充 `GameIds` 覆盖 | --- ## 8. 修复方案 ### Fix H-1:TutorialManager 移除 DontDestroyOnLoad ```csharp // TutorialManager.Awake() — 删除以下行 // DontDestroyOnLoad(gameObject); ← 删除 ``` `TutorialManager` 应位于 Persistent 场景的 GameManagers GameObject 下,由场景生命周期自动保证跨场景存活,无需自行调用。 --- ### Fix H-2:LocalizationManager 迁移为实例服务 **步骤 1**:定义接口 ```csharp // Core 程序集(或 Localization 程序集) public interface ILocalizationService { string Get(string key, string table = "UI"); void SetLanguage(Language language); Language CurrentLanguage { get; } event System.Action OnLanguageChanged; } ``` **步骤 2**:`LocalizationManager` 实现接口 + 从 SaveData 读写语言设置 ```csharp // 语言偏好通过 SaveData 持久化(而非 PlayerPrefs) public class LocalizationManager : MonoBehaviour, ILocalizationService, ISaveable { private Language _currentLanguage = Language.ChineseSimplified; private void Awake() { if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } ServiceLocator.Register(this); } // ISaveable public void OnSave(SaveData data) => data.Settings.Language = _currentLanguage.ToString(); public void OnLoad(SaveData data) { if (!string.IsNullOrEmpty(data.Settings?.Language) && System.Enum.TryParse(data.Settings.Language, out var lang)) _currentLanguage = lang; } } ``` **步骤 3**:调用方迁移 ```csharp // 旧:静态调用 var text = LocalizationManager.Get("ui_start"); // 新:通过服务接口 var loc = ServiceLocator.GetOrDefault(); var text = loc?.Get("ui_start") ?? "ui_start"; ``` > ⚠️ 若迁移成本过高(大量现有调用),可保留静态 `Get()` 作为 Facade,内部委托到服务实例,但语言持久化必须从 PlayerPrefs 迁移至 SaveData。 --- ### Fix M-1:DialogueManager 添加接口 ```csharp // 新增接口 public interface IDialogueService { bool IsDialogueActive { get; } void StartDialogue(DialogueSequenceSO sequence, string npcId = ""); } // DialogueManager 实现接口 public class DialogueManager : MonoBehaviour, IDialogueService { private void Awake() { if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } ServiceLocator.Register(this); } private void OnDestroy() => ServiceLocator.Unregister(this); } // 调用方 var dialogue = ServiceLocator.GetOrDefault(); dialogue?.StartDialogue(sequence, npcId); ``` --- ### Fix M-2 & M-3:AchievementManager + ProjectileManager 添加接口 ```csharp // IAchievementService public interface IAchievementService { void CheckAll(SaveData saveData); bool IsUnlocked(string achievementId); float GetProgress(string achievementId); } // IProjectileService public interface IProjectileService { Transform PlayerTransform { get; } void LaunchHoming(HomingProjectile proj, Vector2 direction, ProjectileConfigSO config, DamageInfo damageInfo); } ``` 注册时改为接口类型: ```csharp ServiceLocator.Register(this); ServiceLocator.Register(this); ``` --- ### Fix M-4:AnalyticsManager 去除静态 Facade 将静态 `Track()` 改为实例方法,调用方通过 `ServiceLocator` 访问: ```csharp // AnalyticsManager — 移除所有 static 方法 public void Track(string eventName, Dictionary parameters = null) { ... } public void TrackBossKill(string bossId, float duration, int deathCount) { ... } // 新增接口(可选) public interface IAnalyticsService { void Track(string eventName, Dictionary parameters = null); void TrackBossKill(string bossId, float duration, int deathCount); } ``` 调用方: ```csharp ServiceLocator.GetOrDefault()?.TrackBossKill(bossId, duration, deaths); ``` --- ### Fix L-1:EquipmentManager.OnLoad 消除 .ToList() ```csharp public void OnLoad(SaveData data) { // 逆序遍历,无需 .ToList() 副本 for (int i = _equipped.Count - 1; i >= 0; i--) foreach (var fx in _equipped[i].effects) fx?.OnUnequip(_ctx); _equipped.Clear(); _usedNotches = 0; // ... 其余逻辑不变 } ``` --- ### Fix L-2:GlobalObjectPool 活跃计数改为 int ```csharp // 用简单计数器替代 LinkedList private readonly Dictionary _aliveCount = new(); // Spawn 时 _aliveCount[key] = _aliveCount.GetValueOrDefault(key, 0) + 1; // Despawn 时 _aliveCount[key] = Mathf.Max(0, _aliveCount.GetValueOrDefault(key, 0) - 1); // 上限检查 int maxCount = _maxCounts.GetValueOrDefault(key, 0); if (maxCount > 0 && _aliveCount.GetValueOrDefault(key, 0) >= maxCount) { ... } ``` --- ### Fix L-3:SceneLoader 加载失败恢复 ```csharp private IEnumerator LoadSceneCoroutine(SceneLoadRequest request) { // 加载新场景(先加载再卸载,防止加载失败后无场景) var loadOp = Addressables.LoadSceneAsync(request.SceneName, LoadSceneMode.Additive); yield return loadOp; if (loadOp.Status != AsyncOperationStatus.Succeeded) { Debug.LogError($"[SceneLoader] 加载场景失败:{request.SceneName}"); // 加载失败:不卸载旧场景,维持当前状态 yield break; } // 加载成功后再卸载旧场景 if (!string.IsNullOrEmpty(_currentRoomScene) && _currentHandle.IsValid()) { var unloadOp = Addressables.UnloadSceneAsync(_currentHandle); yield return unloadOp; } _currentHandle = loadOp; _currentRoomScene = request.SceneName; _onSceneLoaded?.Raise(request.SceneName); } ``` --- ## 9. 综合结论 ### 框架综合水平 经过深度全量评审,本框架的代码质量**超过大多数已发布商业独立 AA 游戏**,在以下方面达到或超越行业最高标准: | 亮点 | 具体体现 | |-------------------------------|-----------------------------------------------------------------------| | 事件系统 RAII 模式 | `CompositeDisposable` + `EventSubscription` (readonly struct),行业领先| | BatchLOS Swap-and-pop | O(1) 注册/注销,帧预算均分,含升级路径文档 | | EventChain 帧内去重 | `_evaluatePending` + Update 合并,同帧多事件单次评估 | | StatusEffect 双结构 + 工厂 | List + Dict 双结构兼顾遍历/查询,工厂运行时可扩展 | | 存档系统 | 原子写入 + HMAC + DLC 字段 + 版本迁移架构,工程化程度接近 AAA | | 数据驱动覆盖率 | 连击段数、Boss 阶段、状态效果、护符效果,全部通过 SO 配置 | | 编辑器工具链 | EventBusMonitor + ScaffoldTools + 多窗口,超出同体量框架标准 | | GameIds 统一常量 | 消除 magic string,IDE 补全 + 编译期校验 | | SpeedrunTimer 整秒更新 | 每帧跳过字符串重建,细节处理体现工程素养 | ### 剩余问题概览 | 优先级 | 数量 | 核心内容 | |--------|------|-------------------------------------------------------| | 🔴 高 | 2 | TutorialManager DontDestroyOnLoad;LocalizationManager 静态类 + PlayerPrefs | | 🟡 中 | 5 | DialogueManager / AchievementManager / ProjectileManager / TutorialManager 缺少接口;AnalyticsManager 混合静态模式 | | 🟢 低 | 5 | EquipmentManager OnLoad GC;GlobalObjectPool LinkedList;SceneLoader 失败恢复;magic string | 修复以上 7+5 个问题后,框架可达到**完全无设计混用、模式统一、商业可发布**的代码质量标准。 --- *本评审基于全量源码静态分析(28 个子系统,逐文件阅读)。* *建议优先修复 H-1 和 M-1~M-5(接口补完),然后合并至 L 级别优化,一次性收官。*