# 代码质量全面评估报告 > **评估日期**:2026-05 > **评估范围**:`Assets/Scripts/`(25 个程序集,覆盖全部 24 个功能模块) > **评估标准**:以商业独立/AA 级 2D 动作游戏(如 Hollow Knight、Ori 系列、Dead Cells 技术架构)为基准 > **评估维度**:架构设计、性能、可扩展性、编辑器友好性、使用便利性 --- ## 综合评分 | 评估维度 | 评分(5 分制) | 简评 | |--------------|:-----------:|------| | 架构设计 | ⭐⭐⭐⭐½ | 模块隔离清晰,模式选型专业,少量结构性问题 | | 性能 | ⭐⭐⭐⭐ | 热路径零 GC、批量 LOS、对象池均到位,有几处隐患 | | 可扩展性 | ⭐⭐⭐⭐ | 接口驱动+SO数据驱动,新增功能低耦合,主要短板在玩家状态机 | | 编辑器友好性 | ⭐⭐⭐⭐½ | EventBusMonitor、SO 数据资产、Header 属性完善 | | 使用便利性 | ⭐⭐⭐⭐ | 依赖注入清晰,部分 API 命名/一致性可改进 | | **综合** | **⭐⭐⭐⭐** | **接近商业品质,核心问题为单例泛滥和局部硬编码** | --- ## 一、架构设计 ### 1.1 程序集划分(Assembly Definition)✅ 优秀 项目将 25+ 个功能域拆分为独立 `.asmdef` 程序集: ``` BaseGames.Core → BaseGames.Core.Events → BaseGames.Core.Save BaseGames.Combat → BaseGames.Combat.StatusEffects BaseGames.Player → BaseGames.Player.States BaseGames.Enemies → BaseGames.Enemies.AI → BaseGames.Enemies.Navigation ... ``` **优势**: - 增量编译:修改 `BaseGames.Player.States` 不触发 `BaseGames.Core` 重新编译,加快迭代速度 - 强制接口边界:`EnemyBase` 不可直接访问 `PlayerController` 内部,只能通过 `IDamageable` - 符合《Hollow Knight》及同类商业项目的程序集组织标准 **不足**: - `BaseGames.Core.Events` 程序集中混入了 `DamageInfo`、`HitInfo` 等战斗领域对象(见 `Core/Events/DamageInfo.cs`),违反了"Core.Events 只存事件通道基础设施"的原则,应移至 `BaseGames.Combat` --- ### 1.2 ServiceLocator 模式 ✅ 专业 ```csharp // 接口类型注册,天然支持依赖倒置 ServiceLocator.Register(realAudioManager); ServiceLocator.RegisterIfAbsent(new NullAudioService()); // Null Object 兜底 ``` **优势**: - `RegisterIfAbsent` 防止多场景重复注册,符合 Persistent 场景架构 - `NullAudioService` 是教科书级 Null Object 模式,避免 null 检查污染业务代码 - Editor 下提供 `OverrideForTest` / `Reset` 方法,支持单元测试 - 轻量静态字典,无 DI 框架依赖,适合游戏运行时 **不足**: - 静态类在场景重载时不会自动清空,若忘记在 `GameServiceRegistrar.OnDestroy` 注销,可能持有失效引用(已通过 `DontDestroyOnLoad` 缓解但未根治) - 建议补充 `Unregister()` 方法,便于单元测试隔离 --- ### 1.3 ScriptableObject 事件频道系统(Event Channel SO)✅ 行业标准 基于 Unity Open Projects 推广的 SO 事件频道模式: ```csharp public abstract class BaseEventChannelSO : ScriptableObject { public EventSubscription Subscribe(Action callback) { ... } // 返回可 Dispose 句柄 } ``` **优势**: - 发布者/订阅者完全解耦,不需要互相持有引用 - `EventSubscription` + `CompositeDisposable` 构成 Rx-like 生命周期管理,防内存泄漏 - `EventBusMonitor` 在 Editor 下记录每次调用(频道名、payload、监听器数量、帧号),调试体验优异 - SO 作为 Inspector 资产,便于在 Prefab 中任意组合引用,无需代码修改 **不足**: - 缺少 **事件频道查找表**:25+ 个 SO 频道资产散落在 `Assets/Data/` 下,若多人协作或频道名称不统一,容易创建重复频道。`EventChannelRegistry` 已有雏形,建议强制所有频道注册其中 --- ### 1.4 游戏状态机(GameStateMachine)✅ 稳健 ```csharp public bool TransitionTo(GameStateId nextId, out string error) { if (!_current.ValidNextStates.Contains(nextId)) { error = ...; return false; } _current?.OnExit(nextId); _current = next; _current.OnEnter(prev); ... } ``` **优势**: - 转换合法性在状态机层面校验(`ValidNextStates`),而非散落在各处的 if-else - 状态对象不继承 `MonoBehaviour`,纯 C# 类,可单元测试 - `[DefaultExecutionOrder(-1000)]` 确保 GameManager 最先 Awake **不足**: - `IGameState.ValidNextStates` 如果用 `HashSet` 而非 `IEnumerable` 可避免 `Contains()` 的 O(n) 查找(当前状态数少影响不大,但属于最佳实践缺失) --- ### 1.5 玩家状态机(Player FSM)⚠️ 有结构性问题 `PlayerController` 声明了 16 个具体状态字段: ```csharp private IdleState _idleState; private RunState _runState; private JumpState _jumpState; // ... 共 16 个 ``` **优势**: - 状态为纯 POCO(Plain Old C# Object),无 MonoBehaviour 开销 - `PlayerStateBase` 持有 Controller 引用并暴露属性,状态类代码简洁 **不足(商业级对比)**: - **强耦合**:新增状态需修改 `PlayerController`,违反开闭原则。Dead Cells、Celeste 等同类项目通常用 `Dictionary` 或枚举映射表管理状态,`PlayerController` 只需调用 `_states[StateType.Idle]` - **状态切换 API 散落**:`Owner.TryTransitionState(Owner.IdleState)` 中 `Owner.IdleState` 是公开属性,但这要求 `PlayerController` 对外暴露所有状态实例,破坏封装 - **HitBox 时间点硬编码在状态类中**:`AttackState` 中 `events.Add(0.3f, ...)` 和 `events.Add(0.6f, ...)` 应配置在 `PlayerAnimationConfigSO` 而非状态代码中 --- ### 1.6 伤害流水线 ✅ 设计完善 ``` HitBox.OnTriggerEnter2D → DamageInfo.From(SO) → HurtBox.ReceiveDamage → [1] 无敌帧 → [2] 弹反 → [3] 霸体 → [4] 护盾 → [5] 防御减免 → [6] HP 扣除 → [7] 状态效果 → [8] 事件广播 ``` 8 步流水线文档清晰,`DamageInfo` struct 使用 Builder 模式和零 GC 工厂方法 `From(SO)`,接口驱动(`IDamageable`、`IShieldable`、`IPoiseSource`、`IStatusEffectable`)使各步骤可独立替换。 --- ### 1.7 单例模式滥用 ⚠️ 需关注 全项目存在以下单例: | 类名 | 必要性评估 | |------|-----------| | `GameManager.Instance` | 合理(全局生命周期协调者)| | `SaveManager.Instance` | 合理(跨场景持久)| | `GlobalObjectPool.Instance` | 合理(全局池)| | `ClashResolver.Instance` | ⚠️ 可通过 ServiceLocator 消除 | | `ProjectileManager.Instance` | ⚠️ 可通过 ServiceLocator 消除 | | `DifficultyManager.Instance` | ⚠️ 可通过 ServiceLocator 消除 | 商业项目通常将"功能性单例"注册到 ServiceLocator,仅保留 2-3 个真正的全局单例。当前 6 个单例增加了测试和多场景管理的难度。 --- ## 二、性能 ### 2.1 对象池 ✅ 专业实现 ```csharp public class GlobalObjectPool : MonoBehaviour { private readonly Dictionary> _pools = new(); private readonly Dictionary> _alive = new(); ... public T Spawn(string key, Vector3 position, Quaternion rotation) where T : Component } ``` **优势**: - 双数据结构(空闲队列 + 活跃列表)支持上限控制和活跃对象回收 - 同时提供 `IEnumerator WarmupCoroutine()` 和 `async Task WarmupAsync()`,适应 MonoBehaviour 和纯 C# 两种调用场景 - Addressables 驱动预热,支持异步加载资源 **不足**: - `GetComponentCached()` 是否做了组件缓存?若每次 Spawn 都 `GetComponent`,频繁 Spawn 时仍有 GC 压力。建议在 `PooledObject.Awake` 缓存各常用组件接口 --- ### 2.2 批量视线检测(BatchLOSSystem)✅ 优化到位 ```csharp // 每 FixedUpdate 只检测部分敌人(Round-Robin 分帧) int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count); for (int i = 0; i < count; i++) { int idx = (_currentOffset + i) % _requesters.Count; Physics2D.Raycast(...); } _currentOffset = (_currentOffset + count) % ...; ``` **优势**: - 分帧批处理,避免同帧对 20+ 敌人全部执行 Raycast - 接口驱动(`ILOSRequester`),与 `EnemyBase` 解耦 **不足**: - `_requesters.RemoveAt(idx)` 是 O(n) 操作,敌人频繁死亡/重生时有性能隐患。商业实现通常用标记-清除(null 标记 + 批量清理)代替直接移除 --- ### 2.3 DamageInfo 零 GC ✅ ```csharp // 热路径:struct 工厂,无堆分配 var info = DamageInfo.From(_currentSource); info.KnockbackDirection = knockDir; info.KnockbackForce = _currentSource.KnockbackForce; ``` `DamageInfo` 设计为非 readonly struct(便于 Builder 就地写入),同时提供 `From(SO)` 零 GC 工厂路径,在命中密集场景(群战)中显著减少 GC 压力,是商业级实现水准。 --- ### 2.4 潜在性能隐患 | 位置 | 问题 | 建议 | |------|------|------| | `HitBox.cs` | `_hitCooldownTimers: Dictionary` 每帧 `Mathf.Max` 遍历,高频场景有 GC | 改用固定大小数组 + 索引映射 | | `GameServiceRegistrar.EnsureSingleAudioListener()` | `FindObjectsOfType` 每次场景加载调用 | 可接受(非热路径),已限制场景加载时调用 | | `AttackState.PlayAttackClip()` | 每次进入攻击状态都 `state.Events(this)` 注册 OnEnd 和帧事件 | Animancer 内部有缓存,影响有限,但帧事件时间点应提取到 SO | | `EnemyQuotaManager` | 未见实现细节,需确认波次结算时无 LINQ 热路径 | - | --- ## 三、可扩展性 ### 3.1 接口驱动设计 ✅ 充分 | 接口 | 用途 | 实现类 | |------|------|--------| | `IDamageable` | 可受击 | `PlayerController`, `EnemyBase` | | `IShieldable` | 护盾拦截 | `ShieldComponent` | | `IPoiseSource` | 霸体来源 | `PlayerController`, `EnemyBase` | | `IBreakable` | 可破坏机关 | 场景对象 | | `IStatusEffectable` | 状态效果 | `StatusEffectManager` | | `IPathAgent` | 导航代理 | `EnemyNavAgent`(Navigation 程序集)| | `ISaveStorage` | 存档存储后端 | `LocalFileStorage`(可替换为云存储)| | `ILOSRequester` | 视线检测请求者 | `EnemyBase` 实现 | **跨程序集依赖反转做得好**:`HurtBox`(Combat 程序集)通过 `IStatusEffectable` 接口调用 `StatusEffectManager`(StatusEffects 程序集),避免反向依赖。 --- ### 3.2 ScriptableObject 数据资产驱动 ✅ - `DamageSourceSO`:伤害源(伤害值、类型、标记)独立资产,策划可直接编辑 - `PlayerMovementConfigSO`:所有移动参数集中配置 - `EnemyStatsSO`:敌人属性数据分离 - `BossSkillSO` + `AttackPatternSO`:Boss 技能和攻击模式纯数据化 - `DifficultyScalerSO`:难度缩放系数可独立调节 这是 Hollow Knight 同类项目的标准实践,大幅降低策划-程序沟通成本。 --- ### 3.3 存档迁移(SaveMigrator)✅ 生产级 ```csharp case "1.0": data = MigrateFrom1_0(data); goto case "1.1"; case "1.1": data = MigrateFrom1_1(data); goto case "2.0"; case "2.0": data = MigrateFrom2_0(data); goto case "2.1"; ``` 版本链式迁移(fall-through),支持从任意旧版本升级到最新版,加上 checksum 校验,是 AA 级游戏存档系统的标准实现。 --- ### 3.4 扩展瓶颈 **1. 玩家状态扩展成本高** 新增一个玩家状态(如游泳攻击状态)需要: 1. 新建 `SwimAttackState.cs` 2. 在 `PlayerController` 中声明字段 `private SwimAttackState _swimAttackState;` 3. 在 `PlayerController.Awake()` 中初始化 4. 在相关状态的 `OnStateUpdate()` 中添加跳转判断 对比 Dead Cells 等商业项目的状态字典方案,每次新增需修改 Controller 核心类,违反开闭原则。 **2. Boss 技能扩展** `BossSkillSO` + `AttackPatternSO` + `SkillSequenceSO` 组合提供了良好的数据驱动扩展能力,但 `BossSkillExecutor` 的具体执行逻辑需要查看是否支持新技能类型不修改代码。 --- ## 四、编辑器友好性 ### 4.1 EventBusMonitor ✅ 出色 ```csharp #if UNITY_EDITOR EventBusMonitor.Record(name, value?.ToString() ?? "null", OnEventRaised?.GetInvocationList().Length ?? 0, Time.frameCount); #endif ``` 每次 EventChannel 触发均记录:频道名、payload 字符串表示、当前监听器数量、帧号、时间戳。配合自定义 EditorWindow 可实现完整事件流可视化,是商业项目必备的调试工具。`#if UNITY_EDITOR` 包裹确保零 Release 开销。 --- ### 4.2 Inspector 标注 ✅ - `[Header(...)]` 分组清晰:`PlayerController` 中 "核心组件"、"配置"、"战斗"、"事件频道" 分组一目了然 - `[DefaultExecutionOrder]` 在 5 个关键类上正确标注,执行顺序有保证 - `[RequireComponent(typeof(...))]` 在 `HitBox`、`HurtBox`、`PlayerMovement` 等关键组件上使用,防止配置错误 - `[Multiline]` 用于 EventChannel 的 description 字段,方便编辑器中多行文字说明 --- ### 4.3 `IValidatable` 接口 ⚠️ 未完全落地 `Core/Validation/IValidatable.cs` 定义了验证接口,但未见对应的 Editor 自动扫描器(如 `OnValidate()` 批量调用),导致验证逻辑无法在编辑器保存时自动触发。建议补充: ```csharp // Editor/ValidatorEditor.cs [MenuItem("BaseGames/Validate All ScriptableObjects")] static void ValidateAll() { foreach (var so in Resources.FindObjectsOfTypeAll()) if (so is IValidatable v) v.Validate(); } ``` --- ### 4.4 SO 资产菜单 ✅ 关键数据类均有 `[CreateAssetMenu]`,策划可直接右键创建,无需找程序,符合现代 Unity 工作流。 --- ## 五、使用便利性(API 设计) ### 5.1 EventChannel 订阅 API ✅ 流畅 ```csharp // 推荐用法:链式订阅,自动生命周期管理 _onPlayerDied.Subscribe(HandlePlayerDied).AddTo(_subscriptions); // OnDisable 一行清理 _subscriptions.Clear(); ``` `EventSubscription.AddTo()` 扩展方法是良好的 Fluent API 设计,使用体验接近 UniRx。 --- ### 5.2 对象池 API ✅ ```csharp // 泛型获取,类型安全 var proj = GlobalObjectPool.Instance.Spawn("Proj_Arrow", pos, rot); ``` API 简洁,类型安全,无需 `GetComponent` 手动转型。 --- ### 5.3 存档 API ✅ ```csharp // async/await 风格,UI 层友好 await SaveManager.Instance.SaveAsync(slot: 0); bool ok = await SaveManager.Instance.LoadAsync(slot: 0); ``` 现代 async/await 模式,配合存档指示器事件频道,完整覆盖 UI 反馈需求。 --- ### 5.4 InputReaderSO 输入 API ⚠️ 轻微不足 ```csharp // 事件驱动(✅ 推荐用法) _inputReader.AttackEvent += OnAttack; // 轮询(✅ 也支持) Vector2 move = _inputReader.MoveInput; ``` **不足**:`InputReaderSO` 是 ScriptableObject,跨场景加载时 `OnEnable/OnDisable` 行为依赖 Unity 的 SO 生命周期,容易在热重载或 Play-mode 重新进入时出现"Map must be contained in state"错误(代码注释中已提及,但解决方案(`EnsureInitialized` + Reset)属于防御性修复而非根治)。建议将 Input 初始化移至专用 MonoBehaviour 组件(如现有的 `InputReaderBootstrap.cs`)中管理。 --- ### 5.5 硬编码问题汇总 | 位置 | 硬编码内容 | 建议 | |------|-----------|------| | `HitBox.cs:42-43` | `PlayerHitBoxLayer = 13`, `EnemyHitBoxLayer = 16` | 改用 `LayerMask` 字段或 `Physics2D.GetLayerCollisionMask` | | `AttackState.cs:38-40` | `events.Add(0.3f, ...)` 命中盒激活时间 | 移至 `PlayerAnimationConfigSO` 的 `HitBoxActiveStart/End` 字段 | | `PlayerMovement.cs:58` | `CoyoteTime` 默认值 `0.12f` 内联 fallback | SO 必填,移除内联 fallback 以强制配置 | | `SaveManager.cs:13` | `QuickSaveSlot = 98` | 移至 `GlobalSettingsSO` | --- ## 六、与商业项目对比 ### 对比基准:Hollow Knight / Dead Cells 技术架构(公开分析资料) | 对比项 | 本项目 | 商业基准 | 差距 | |--------|--------|---------|------| | 程序集隔离 | 25 个 asmdef | 通常 15-30 个 | ✅ 持平 | | 事件系统 | SO EventChannel + CompositeDisposable | 通常 SO Channel 或自研消息总线 | ✅ 持平 | | 玩家状态机 | POCO 状态 + Controller 字段 | 通常状态字典/工厂,更低耦合 | ⚠️ 有差距 | | 对象池 | Addressables + Queue,支持上限 | 商业项目通常更完整(类型缓存、统计面板) | ✅ 基本持平 | | 存档系统 | JSON + checksum + 版本迁移链 | ✅ 已达商业标准 | ✅ 持平 | | 伤害流水线 | 8 步,接口驱动 | 商业项目通常 6-10 步 | ✅ 持平 | | 难度系统 | SO Scaler + ISaveable | 商业标准实现 | ✅ 持平 | | 输入系统 | Unity Input System + Buffer | 商业标准实现 | ✅ 持平 | | 单例数量 | 6 个 | 建议 ≤ 3 个,其余用 ServiceLocator | ⚠️ 偏多 | | 测试支持 | ServiceLocator 支持 Mock,但无测试用例 | 商业项目通常有核心系统单元测试 | 🔴 缺失 | | CI/自动验证 | 无 | 商业项目通常有 | 🔴 缺失 | --- ## 七、优先级改进建议 ### 🔴 高优先级(影响开发效率或潜在 Bug) 1. **PlayerController 状态管理重构** 将 16 个状态字段改为 `Dictionary` + 工厂方法,新增状态无需修改 Controller 2. **AttackState 帧事件时间点提取** `events.Add(0.3f, ...)` 移至 `PlayerAnimationConfigSO`,支持策划在 Inspector 调节命中盒窗口而无需修改代码 3. **HitBox 层级硬编码消除** `PlayerHitBoxLayer = 13` 改为 `[SerializeField] LayerMask _rivalHitBoxMask`,配置驱动,层级重排时不破坏逻辑 ### 🟠 中优先级(提升可维护性) 4. **ServiceLocator 替换功能性单例** `ClashResolver`、`ProjectileManager`、`DifficultyManager` 注册到 `ServiceLocator`,消除直接 `Instance` 引用 5. **EventChannelRegistry 强制注册** 要求所有 EventChannel SO 在 `GameServiceRegistrar.Awake` 时注册到 `IEventChannelRegistry`,防止孤立频道 6. **核心类迁移 DamageInfo / HitInfo** 从 `Core/Events/` 移至 `Combat/`,保持程序集领域边界清晰 ### 🟡 低优先级(质量提升) 7. **补充 IValidatable Editor 扫描器** 批量验证 SO 配置,减少运行时 `null` 警告 8. **BatchLOSSystem 移除优化** 改用标记清除替代 `RemoveAt`,消除 O(n) 开销 9. **补充核心系统单元测试** `GameStateMachine`、`DamageInfo.Builder`、`SaveMigrator` 逻辑简单,非常适合作为第一批测试用例 --- ## 八、总结 本项目代码质量在国内独立游戏中属于**偏上水准**,核心架构模式(SO 事件系统、程序集隔离、接口驱动伤害流水线、存档迁移)均达到或接近商业标准。 主要差距集中在两类问题: 1. **结构性**:玩家状态机扩展成本偏高,单例过多 2. **规范性**:少量硬编码、未落地的验证基础设施、缺少自动化测试 这两类问题不影响当前功能运作,但随项目规模增大(尤其玩家状态和 Boss 数量增加)会造成明显的维护负担。建议在 Phase 3-4 内容开发前完成高优先级改进。