Files
zeling_v2/Docs/Review/CodeReview_2025_Full.md
2026-05-12 15:34:08 +08:00

23 KiB
Raw Blame History

泽灵 v2 — 全面代码评估报告

评估基准Unity 2022.3 LTS / C# / 2D 动作平台游戏
评估标准:商业高性能游戏代码实践(参考 Hollow Knight、Celeste、Hades 等同类产品)
评估范围:Assets/Scripts 全量代码(约 330 个 .cs 文件)
评估日期2025/2026


评分总览

评估维度 得分 备注
架构设计 8.5 模块化扎实,少量遗留单例
性能 8.0 热路径零分配,批量 LOS 优秀
可扩展性 8.5 数据驱动完善,少量硬编码管道
编辑器友好 9.0 工具链完整,为同类 Indie 最佳水平
使用便利性 8.0 契约清晰,命名偶有不一致
综合 8.4 接近商业发行水准

一、架构设计8.5 / 10

1.1 程序集隔离( 优秀)

项目共 25+ 个程序集定义(.asmdef,粒度精确到子模块层级:

BaseGames.Core          → 全局服务基础设施
BaseGames.Core.Save     → 持久化(单向依赖 Core
BaseGames.Core.Events   → SO 事件频道(无外部依赖)
BaseGames.Combat        → 战斗管道(无 Player/Parry 直接依赖)
BaseGames.Parry         → 弹反(依赖 Combat 接口,不依赖 Player
BaseGames.Player        → 玩家逻辑(处于依赖图顶层)
BaseGames.Enemies.AI    → BD 任务节点
BaseGames.Editor        → 仅编辑器工具
...

编译时强制模块边界,跨模块引用必须使用接口或事件频道,与商业游戏标准一致。

依赖方向控制

Core → Core.Save单向
       ↑
GameServiceRegistrar桥接使用 SaveServiceAdapter 适配器,
避免 Core.Save → Core 的反向引用,保持有向无环图。

1.2 服务定位器( 优秀)

ServiceLocatorBaseGames.Core

ServiceLocator.Register<IAudioService>(this);          // 覆盖注册
ServiceLocator.RegisterIfAbsent<IAudioService>(null);  // 防重复
ServiceLocator.GetOrDefault<ICameraService>()          // 安全取用
  • 接口隔离:调用方依赖 IAudioService,不依赖 AudioManager 具体类
  • Null-Object 兜底:GameServiceRegistrar.Awake() 先注册 NullAudioService,避免 AudioManager 未初始化时的空引用崩溃
  • 测试支持:OverrideForTest<T> / Reset() 仅在 #if UNITY_EDITOR 暴露

已注册接口 IAudioService / ISaveService / ISceneService / IDeathRespawnService / IEventChannelRegistry / IObjectPoolService / ICameraService / IPlatformService

1.3 游戏状态机( 优秀)

GameStateMachine 为纯 C# 类,不继承 MonoBehaviour

// 非法转换 → 返回 false + 错误字符串(不抛出异常)
bool ok = _fsm.TransitionTo(nextId, out string error);
  • 每个状态声明 ValidNextStates(合法出口白名单),状态图文档化在代码中
  • GameManager 持有 GameStateMachine 实例而非继承它(组合优于继承)
  • 与 Celeste 等游戏的 StateMachine 模式完全吻合

1.4 玩家状态机( 优秀)

PlayerController 维护 Dictionary<Type, PlayerStateBase> 状态字典; PlayerStateBase POCO 类,不继承 MonoBehaviour生命周期由 PlayerController 驱动:

OnStateEnter → OnStateUpdate → OnStateFixedUpdate → OnStateExit

12 个具体状态Idle / Run / Jump / Fall / Dash / AerialDash / Attack / AirAttack / UpAttack / DownAttack / Parry / Hurt / Dead / Spring / WallSlide / WallJump / Swim各自独立新增状态只需实现 PlayerStateBase 并在 InitializeStates() 中注册。

#if UNITY_EDITOR ValidTransitions 仅在编辑期验证,不增加运行时开销。

1.5 敌人状态系统( 优秀 + 尚有空间)

Phase 1 双轨实现:EnemyStateType 枚举保持对外 APIIEnemyState POCO 对象承载逻辑:

_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
// 子类可在 base.Awake() 后替换:
_stateObjs[EnemyStateType.Hurt] = new BossSpecialHurtState();

ForceState() 完成三步 Exit → 赋值 → Enter干净无副作用。

缺陷EnemyBase.TakeDamage() 中 TODO

// TODO: 根据霸体结果选 Stagger / Hurt
ForceState(EnemyStateType.Hurt);  // 霸体判断尚未走 POCO 路径

实际 Stagger 触发仍绕过 _stateObjs 字典——Phase 1 双轨不完整。

1.6 SO 事件频道( 优秀)

BaseEventChannelSO<T> / VoidBaseEventChannelSO 双泛型基类:

  • Subscribe() 返回 EventSubscriptionIDisposable),配合 CompositeDisposable.AddTo() 零泄漏
  • Editor 模式下 EventBusMonitor.Record() 记录所有事件(帧号 + 订阅数),供 EventBusMonitorWindow 运行时调试
  • description 字段:设计师在 Inspector 中注释频道用途

1.7 遗留问题(⚠️

问题 文件 影响
GameManager.Instance 静态单例与 ServiceLocator 并存 GameManager.cs 双入口访问模式
AudioManager.Instance 标注 [Obsolete] 但仍存在 AudioManager.cs 新代码可能误用旧入口
VFXPool.Instance 未注册到 ServiceLocator VFXPool.cs 无法 Mock / 测试
GlobalObjectPool.Instance 保留(已注册 IObjectPoolService但静态 Instance 共存) GlobalObjectPool.cs 同上
SaveManager.Instance 保留(由 DeathRespawnService 直接调用) DeathRespawnService.cs 依赖具体类
AntiSoftlockSystem 在全局命名空间(无 namespace AntiSoftlockSystem.cs 命名空间污染
EquipmentManager 使用 EventChannelRegistry.Instance(非 ServiceLocator EquipmentManager.cs 不一致

二、性能8.0 / 10

2.1 战斗热路径——零堆分配( 优秀)

DamageInfostruct(非 class通过两种方式构造

// 热路径:零堆分配,直接从 SO 初始化
DamageInfo info = DamageInfo.From(damageSourceSO);
info.KnockbackDirection = ...;

// 复杂情况Builder 模式(分配一个 Builder 对象,可接受)
DamageInfo info = new DamageInfo.Builder()
    .SetRaw(50).SetType(DamageType.Slash).SetFlags(DamageFlags.CanBeParried)
    .Build();

HurtBox.ReceiveDamage() 8步流水线中无任何 LINQ / Alloc 调用,性能关键路径完全符合商业标准。

2.2 批量视线检测( 优秀)

BatchLOSSystem 实现时间切片策略:

每 FixedUpdate 仅处理 min(_maxRequestersPerFrame=8, total) 个请求者
_currentOffset 轮询偏移,均匀分配负载

对比朴素实现(每敌人每帧一次 Raycast2D20 个敌人 → 减少 60%+ 射线调用。 _results[idx] 写入后未被读取(结果已通过 requester.ReceiveLOSResult() 直接回调),_results List 是冗余字段,可删除。

2.3 对象池( 优秀)

GlobalObjectPool 特性:

特性 实现
Addressables 预热 WarmupAsync() 异步批量实例化
LRU 回收 MaxCount 达限时回收 LinkedList 头节点O(1)
后台补池 同步 Instantiate 后触发协程异步补充
接口隔离 实现 IObjectPoolService,可 Mock 测试

VFXPoolParticleSystem 专用池独立维护合理的关注点分离ParticleSystem 生命周期与普通 GameObject 不同)。

可改进VFXPool.Play() 每次都通过协程启动,即使对象已在池中可同步取出,协程调度有 1-2 帧延迟。

2.4 音频池( 良好)

AudioManager 使用 6 源轮转 SFX 池,双 AudioSource 交叉淡入淡出 BGM避免频繁 AudioSource.Stop/Play 切换产生的爆音与内存分配。

2.5 状态效果双结构( 优秀)

private readonly List<StatusEffect>                         _activeList  = new(); // Update 遍历 O(n)
private readonly Dictionary<StatusEffectType, StatusEffect> _activeIndex = new(); // 类型查找 O(1)

MaterialPropertyBlock 修改 Shader 属性(不修改共享材质),符合 Unity 性能最佳实践。

2.6 序列化性能( 良好)

SaveManager.SaveAsync() 使用 Formatting.None(减少 JSON 体积),SemaphoreSlim(1,1) 防止并发写入损坏。

2.7 性能风险点(⚠️

风险 位置 说明
FindObjectsOfType<AudioListener> GameServiceRegistrar.EnsureSingleAudioListener() 已有 _primaryListener 绑定后可绕过,但未绑定时仍全场景扫描
_equipped.Sum(c => c.notchCost) LINQ EquipmentManager.UsedNotches 每次 UI 查询触发 LINQ列表小<8时可接受建议缓存
BatchLOSSystem._results 冗余写入 BatchLOSSystem.cs L68 每帧写入后不读取,微小 GC 风险
VFXPool.PlayCoroutine() VFXPool.cs 即使池命中,仍需协程恢复一帧
PlayerController._states[typeof(T)] PlayerController.cs Type 键查找无 boxing引用比较但每次 TransitionTo 需哈希查找

三、可扩展性8.5 / 10

3.1 护符系统( 优秀)

ICharmEffect 策略模式,完全数据驱动:

CharmSO → ICharmEffect[]
           ├── StatModifierEffect     (+攻击力/防御)
           ├── AttackSpeedEffect      (攻速修改)
           ├── OnHitEffect            (命中触发)
           ├── SkillNumericModifierEffect (技能数值)
           ├── SkillSlotOverrideEffect    (技能槽替换)
           ├── WeaponOverrideEffect       (武器替换)
           └── SoulSpellEffect            (灵力法术)

新增效果只需实现 ICharmEffect,无需修改 EquipmentManager——完美的开闭原则。

3.2 成就系统( 优秀)

AchievementCondition 抽象类10 个具体实现:

CollectedItemCondition / DefeatedBossCondition / EnteredRegionCondition /
ParryCountCondition / TimedBossKillCondition / MapExplorationCondition ...

设计师可在 AchievementSO Inspector 中组合条件,不需要代码介入。

3.3 Boss 技能系统( 良好)

BossBase → EnterPhase(int) [virtual]
         → BossSkillExecutor → BossSkillSO[]
                             → SkillSequenceSO (有序技能序列)
                             → AttackPatternSO (技能模式 SO)

TelegraphSystem 独立为组件,可复用于不同 Boss。

3.4 EventChain 系统( 良好)

EventChainSO 顺序事件链,配合 EventChainManager 执行:设计师可在 SO 中定义剧情触发序列,无需写代码。

3.5 IValidatable + SOValidationRunner 优秀)

任何 SO 实现 IValidatable,自动纳入构建前扫描:

public IEnumerable<string> Validate()
{
    if (BaseDamage <= 0) yield return "❌ BaseDamage 必须 > 0";
}

SOValidationRunner 作为 IPreprocessBuildWithReport构建中止防止错误配置上线。

3.6 可扩展性缺陷(⚠️

问题 说明 建议
HurtBox.ReceiveDamage() 8步流水线硬编码 新增拦截步骤须修改 HurtBox 引入 IDamageInterceptor[] 责任链
StatusEffectManager.CreateEffect() DamageType→StatusEffect 极可能是 switch 改为 SO 配置映射 Dictionary<DamageType, StatusEffect>
无通用属性计算器 护符/Buff 效果各自修改 PlayerStats 字段 考虑 StatCalculator 优先级栈(参考 Hades 设计)
AudioManager.PlayBGM/SFX(string) 为桩 Phase 2 未完成 优先实现 AudioEventSO 集成
Spells 模块仅有 _Placeholder.cs 施法系统留空 按现有 Skill 模式扩展

四、编辑器友好9.0 / 10

编辑器工具链是本项目最突出的优势,接近 AA 商业游戏水准。

4.1 工具总览

工具 位置 功能
SOValidationRunner Editor/Validation/ 全量 SO 数据验证,构建前自动执行
AddressKeyValidator Editor/ Addressable Key 有效性验证,防止引用失效
AddressReferenceGraphWindow Editor/ Addressable 引用图可视化
HurtBoxEditor Editor/Combat/ PlayMode 受击盒注入状态可视化(绿色/橙色)
EventBusMonitorWindow Editor/ 运行时事件总线监控(频道名 + 订阅数 + 帧号)
EventChannelEditor Editor/ 事件频道 Inspector 中一键 Raise 按钮
BossSkillSequenceWindow Editor/ Boss 技能序列可视化设计器
EventChainEditorWindow Editor/ EventChain 可视化编辑器
CharmEffectDrawer Editor/Equipment/ 护符效果自定义 PropertyDrawer
MapRoomDataEditor Editor/Map/ 地图房间数据编辑器
SceneScaffoldTools Editor/ 场景脚手架快捷工具
NavSurfaceBakeShortcut Editor/ 导航网格一键烘焙快捷键
CreateEventChannelAssets Editor/ 一键创建事件频道 SO 资产菜单
ScriptExecutionOrderTools Editor/ 执行顺序可视化管理
DestructibleTileEditor Editor/World/ 可破坏瓦片编辑器
AchievementSOEditor Editor/Achievements/ 成就 SO 自定义编辑器

4.2 Inspector 设计( 优秀)

  • 所有配置使用 [Header] 分组,字段有 [Tooltip]
  • 所有事件频道 SO 有 description 字段(设计师可见注释)
  • [DefaultExecutionOrder] 系统范围覆盖(-2000 到 +50执行顺序文档化在代码中
  • [RequireComponent] 保证依赖完整性,避免配置错误

4.3 潜在改进(⚠️

问题 说明
HurtBoxEditor 用反射读取私有字段 字段重命名后 Editor 静默失效,建议改用 SerializedProperty 或公开只读属性
SOValidationRunner 错误检测靠关键字 "必须" / "" 语言切换后可能失效,建议改为 ValidationResult 枚举Error / Warning / Info
BatchLOSSystem 无 Editor Gizmo 调试时无法可视化射线,建议添加 OnDrawGizmos
EventChainEditorWindow 无截图/文档 新成员上手曲线较高

五、使用便利性8.0 / 10

5.1 订阅模式( 优秀)

// 组合式OnDisable 一行清理,零泄漏
private readonly CompositeDisposable _subs = new();

private void OnEnable()
{
    _onPlayerSpawned.Subscribe(OnPlayerSpawned).AddTo(_subs);
    _onBossFightEnded.Subscribe(OnBossEnded).AddTo(_subs);
}

private void OnDisable() => _subs.Clear();

相比裸 +=/-= 订阅,极大降低事件泄漏风险,与商业级 Rx 风格一致。

5.2 伤害构造( 优秀)

// 热路径首选:零分配
DamageInfo info = DamageInfo.From(source);

// 复杂流水线Builder
DamageInfo info = new DamageInfo.Builder()
    .SetRaw(100).SetFlags(DamageFlags.CanBeParried | DamageFlags.CanClash)
    .SetKnockback(Vector2.right, 10f).Build();

双模式清晰区分使用场景,符合 API 设计最佳实践。

5.3 状态类便捷属性( 良好)

PlayerStateBase 提供所有常用属性的简称:

// 在任意状态中直接使用
Anim.Play(AnimCfg.Run);
Move.Jump();
Stats.TakeDamage(info);
Buffer.Consume(InputType.Jump);

避免重复的 _owner. 链式访问,代码可读性接近 Celeste 的 Player.cs

5.4 Null-Object 模式( 良好)

NullAudioService   // IAudioService 空实现Log 警告,不崩溃)
NullPlatformService // IPlatformService 空实现PC 非 Steam 环境)
NullPathAgent      // IPathAgent 空实现(无导航组件时使用)

三个 NullObject 防御了三类常见 NullReferenceException符合商业游戏的防御性编程要求。

5.5 命名一致性问题(⚠️

问题 对比
玩家用 TransitionTo() 转换状态 敌人用 ForceState()
玩家用 GetState<T>() 取状态对象 敌人用 _stateObjs[enumKey]
ServiceLocator.Get<T>() 失败抛异常 GetOrDefault<T>() 失败返回 null
Register() 覆盖已有注册 RegisterIfAbsent() 不覆盖

5.6 ISaveable 手动注册(⚠️

// SavePoint.cs, EquipmentManager.cs, AchievementManager.cs ... 各自手动调用
SaveManager.Instance.Register(this);    // OnEnable
SaveManager.Instance.Unregister(this);  // OnDisable

约有 8+ 个 ISaveable 实现重复此样板代码。商业实践(如 Unity 官方 Open Project通常用 SaveManager.FindAndRegister<ISaveable>() 或 ScriptableObject 驱动的注册表统一管理。


六、专项模块深度评估

6.1 存档系统8.5/10

优势

  • Newtonsoft.Json 序列化(完整类型支持,无反射限制)
  • SaveMigrator.Migrate() 版本迁移管道(向前兼容)
  • Checksum 完整性验证(防止文件损坏导致存档不可用)
  • SemaphoreSlim 防并发写入
  • CrashReporter + EmergencySaveService 崩溃保护
  • LocalFileStorage 通过 ISaveStorage 接口可替换(云存档、主机平台扩展点)

不足

  • SaveManager.Instance 仍被 DeathRespawnService 直接引用(应通过 ISaveService
  • SaveData 结构若需新字段,SaveMigrator 需手动更新(无自动 Schema 演化)
  • 无存档文件加密(对 PC 存档修改作弊无防御,可接受)

6.2 输入系统8.0/10

优势

  • InputReaderSO 封装 Unity InputSystem作为 ScriptableObject 可在不同场景共享
  • OnEnable 重置防止 Play Mode 再进入时状态残留(工程实践亮点)
  • EnableGameplayInput() / EnableUIInput() 提供明确的上下文切换
  • InputBuffer 缓冲近端输入解决游戏手柄操作的时序问题Coyote Time 协同)
  • ConflictDetector 检测按键冲突(重映射安全保障)

不足

  • InputReaderSO C# 事件(非 SO 事件频道)与其他模块的 SO Channel 模式不一致——需订阅 C# event 而非通过 SO 引用
  • MoveInput 轮询属性与事件订阅模式并存(两种取值方式)

6.3 摄像机系统7.5/10

优势

  • ICameraService 接口隔离,RoomCamera / CameraStateController 通过服务层解耦
  • CameraBlendProfileSO 数据驱动过渡曲线

不足

  • Camera/_Placeholder.cs 说明摄像机系统尚未完整实现
  • CameraStateController 骨架代码较多,实际行为有限

6.4 AI 系统8.5/10

优势

  • BD 自定义任务节点18 个)覆盖完整 AI 行为集
  • #if GRAPH_DESIGNER 编译守卫,无 Behavior Designer 时代码仍可编译
  • SharedString TargetStateNamevs 原 SharedInt枚举字符串绑定BD 图重排枚举不破坏
  • BatchLOSSystem + ILOSRequester 接口:视线检测完全与敌人类型解耦
  • BD_WaitForAnimation 使用 Animancer State 轮询而非硬编码等待时间

不足

  • BD 图中的 BlackboardVariableEnemyBase 属性之间的映射文档化不足
  • SetAggroTickRate() 为空方法Opsive 运行时 API 变更留存了兼容桩)

6.5 战斗系统9.0/10

优势

  • DamageInfo struct 流水线RawDamage → Amount护盾修改 → FinalDamage防御减免—— 清晰的三段式
  • DamageFlags / DamageTags 位域枚举单值携带多语义CanBeParried | IgnoreIFrame
  • HurtBox 8步流水线顺序固定无敌帧 → 弹反 → 霸体 → 护盾 → 防御减免 → TakeDamage → 广播 → DoT
  • ClashResolver 拼刀碰撞检测独立为组件,不污染 HitBox
  • ParrySystem 仅暴露窗口状态(ConsumeParry()),不引用玩家具体类型

不足

  • HitBox 无法限制同一帧对同一 HurtBox 的多次触发(需 _hitCooldown + HashSet 去重)——当前 _hitCooldown 仅是全局冷却,多目标情况下可能误伤
  • PoiseWindowConfig 存在但 PlayerController.GetCurrentPoiseLevel() 固定返回 PoiseLevel.None(未实现)

七、优先修复建议(按影响面排序)

P1影响正确性

  1. EnemyBase.TakeDamage() 霸体判断 TODO

    • 现状Stagger 触发 hardcodePOCO 路径不完整
    • 修复:在 TakeDamage() 中根据 DamageInfo.Break 和当前霸体等级选择 StaggerHurt
  2. HitBox 同目标重入保护

    • 现状:_hitCooldown 仅限制全局频率,多目标情况下可能一帧命中同一 HurtBox 多次
    • 修复:维护 HashSet<Collider2D> _hitThisActivationDeactivate() 时清空
  3. ISaveable 自动注册

    • 现状8+ 个实现类手动 Register/Unregister
    • 修复:在 SaveManager.Awake()FindObjectsOfType<ISaveable>() 批量注册(允许一次 FindObjects

P2影响质量

  1. 移除 AudioManager.Instance 单例

    • 仅通过 ServiceLocator.Get<IAudioService>() 访问
  2. StatusEffectManager.CreateEffect() 替换 switch

    • 改为 [SerializeField] private StatusEffectMappingSO _mapping,设计师可配置 DamageType→Effect
  3. AntiSoftlockSystem 加入命名空间

    • 当前无 namespace建议 namespace BaseGames.Support.AntiSoftlock
  4. BatchLOSSystem 删除冗余 _results List

    • 结果已通过回调传递,_results 字段仅占内存,删除即可

P3体验优化

  1. HurtBoxEditor 改用 SerializedProperty

    • 避免反射字段名依赖,防止重命名导致 Editor 静默失效
  2. SOValidationRunner 使用枚举结果

    • ValidationResult { Error, Warning } 代替关键字字符串匹配
  3. VFXPool.Play() 同步取池

    • 池命中时跳过协程,直接同步设置 Transform 并播放
  4. 完成 AudioEventSO Phase 2 集成

    • PlayBGM(string) / PlaySFX(string) 目前输出警告,应接入 AudioEventSO 资产查找

八、对标商业游戏评估

维度 Hollow Knight 类比 本项目水准
模块隔离 单 Assembly早期 25+ asmdef更现代
事件解耦 C# event 直连 SO EventChannel更可配置
存档系统 Binary 格式 JSON + 迁移器,更易维护
对象池 自定义池 Addressables + LRU更完整
编辑器工具 16 个专用工具,远超同类
AI 调试 EventBusMonitor + HurtBoxEditor
状态机 MonoBehaviour 继承 POCO 状态对象,更轻量
单元测试 ⚠️ ServiceLocator.Reset() 提供基础,但测试文件尚未建立

九、总结

本代码库在 Indie 游戏中属于顶层水准,在架构规范性、模块化程度和编辑器工具链方面已达到部分 AA 商业标准。核心机制(战斗流水线、状态机、存档系统、对象池)设计扎实,接口边界清晰,后续扩展成本低。

主要短板集中在三个方面

  1. 遗留单例模式7 处)与 ServiceLocator 并存——形成双入口访问隐患
  2. 数个模块处于 Phase 1 / Stub 阶段Audio Phase 2、霸体判断、IEnemyState Phase 2
  3. 缺乏自动化测试覆盖——ServiceLocator 测试基础设施已就绪,但测试文件数量为零

若在当前基础上补齐上述三点,该代码库完全达到独立发行商业游戏的代码质量要求。


本报告由 GitHub Copilot 自动分析生成,基于源代码静态阅读,不包含运行时 Profile 数据。