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

26 KiB
Raw Blame History

zeling_v2 高级代码评审报告

评审标准:参照成熟商业 2D 动作游戏代码品质Team Cherry《Hollow Knight》/ Extremely OK Games《Celeste》/ DeNA 商业项目级别)。 版本Post-Improvement v1 第一轮5项改进完成后 日期2025-07


执行摘要

zeling_v2 是一个具备相当架构意识的 Unity 2D 动作游戏项目程序集分离、ScriptableObject 事件频道、POCO 玩家状态机、接口驱动的战斗管道——这些均超越了大多数同类个人项目。然而,与真正成熟的商业产品相比,在一致性、性能细节、可测试性、编辑器工具化等方面仍存在可察觉的差距。

评审维度 评分(/10 简评
架构设计 7.5 理念先进,但关键一致性漏洞
性能 6.5 战斗层 GC 友好全局有5+个性能热点未处理
可扩展性 7.0 主干可扩展;多处硬编码/魔法数字限制扩展
编辑器友好性 6.0 有 EventBusMonitor缺少 Custom Editor/Gizmo
使用便利性 6.5 API 设计清晰;错误处理不完善,异步陷阱明显
综合 6.7 远超原型质量,距离商业发行质量尚有差距

一、架构设计

1.1 亮点

程序集分离Assembly Definition

25 个 .asmdef 文件构成清晰的依赖图Core ← Combat ← Player ← …),在大型项目中能显著缩短增量编译时间,并强制接口边界。这是正确的工程实践。

ScriptableObject 事件频道 + CompositeDisposable

BaseEventChannelSO<T> + EventSubscription + CompositeDisposable 提供了可 Inspector 连线、零运行时依赖的事件总线,并通过 AddTo() 扩展方法实现生命周期安全的订阅。设计参考了 ReactiveX 风格,是目前团队规模下的优良选择。

POCO 玩家状态机

PlayerStateBase 继承链 + PlayerControllerDictionary<Type, PlayerStateBase> 注册表实现了真正的无 MonoBehaviour 状态机,可以充分利用 C# 类型系统,比 Animator State Machine 更可测试。

战斗管道接口隔离

HurtBox 通过 SetShieldable() / SetParrySystem() / SetPoiseSource() 注入接口,而非直接依赖具体类;HitBox 通过 IBreakable 接口处理可破坏物体——这消除了跨程序集硬耦合。

ServiceLocator + 注册器分离

GameServiceRegistrarDefaultExecutionOrder(-2000))统一在最早阶段注册服务,ServiceLocator.RegisterIfAbsent<IAudioService>(new NullAudioService()) 的空对象模式Null Object Pattern值得称赞。


1.2 关键问题

【严重】ServiceLocator 一致性漏洞

代码库中同时存在两套服务定位机制:

组件 访问方式 问题
ClashResolver ServiceLocator.Register<ClashResolver> 正确
DifficultyManager ServiceLocator.Register<DifficultyManager> 正确
GameManager public static Instance 与理念相悖
SaveManager public static Instance 与理念相悖
GlobalObjectPool public static Instance 与理念相悖

三个核心 Manager 保留了传统 Singleton导致测试时无法注入 Mock 实现,也使 ServiceLocator 的价值大打折扣。商业项目通常要求风格统一,否则维护者永远不确定"该用哪种方式查找服务"。

建议:将三者改为通过 ServiceLocator.Register<ISaveManager> / IGameManager / IObjectPool 注册,并在 GameServiceRegistrar.Awake() 统一入口注册。


【严重】EnemyBase.Awake() 使用 FindWithTag("Player")

// EnemyBase.cs:162
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;

场景中有 N 个敌人时,Awake() 被调用 N 次每次执行全场景标签扫描O(n) 场景对象遍历)。在 Hollow Knight / Celeste 这类商业项目中,此类查找统一通过事件频道推送或注册表注入。

建议:在 GameServiceRegistrarPlayerController.Awake() 完成后,通过 TransformEventChannelSO 广播玩家 Transform所有敌人订阅该事件接收引用。


⚠️【中等】GameStateMachine 缺乏守卫条件Guard Condition

// GameStateMachine.cs:29
if (_current != null && !_current.ValidNextStates.Contains(nextId))

ValidNextStates 是运行时列表检查,属于防御式断言而非真正的状态转换守卫。商业质量的状态机通常为每个转换弧定义独立的 CanTransitionTo(context) 谓词,以便同一目标状态在不同条件下可以允许或拒绝转换,逻辑集中在转换定义上,而非分散在调用方。


⚠️【中等】EquipmentManager 直接使用 EventChannelRegistry.Instance

// EquipmentManager.cs:39
Events = EventChannelRegistry.Instance,

这是另一个静态 Singleton 访问点,绕过了 ServiceLocator,且破坏了 EquipmentManager 的可测试性。


1.3 依赖方向图(实际 vs 理想)

理想:
GameServiceRegistrar → ServiceLocator ← [所有使用方]

实际:
GameServiceRegistrar → ServiceLocator ← [部分使用方]
                                ↑
GameManager.Instance ←─────────┘ (绕过)
SaveManager.Instance ←─────────┘ (绕过)
EventChannelRegistry.Instance  ←┘ (绕过)

二、性能

2.1 亮点

DamageInfo 零 GC 设计

DamageInfostruct + DamageInfo.From() 工厂方法,在战斗最热路径上避免堆分配。在帧率敏感的动作游戏中,这是正确的优先选择。

StatusEffectManager 双结构List + Dictionary

_activeListO(1) Update 遍历)+ _activeIndexO(1) 类型查找)是标准的游戏引擎 ECS 风格优化,避免了在 Update 中进行字典遍历。

GlobalObjectPool LRU 回收 + Addressables 预热

池空时的 Background Refill Coroutine + Addressables 异步加载是成熟的生产级对象池模式。

Animancer 帧事件驱动 HitBox

帧事件(events.Add(enterTime, ...) / events.Add(exitTime, ...))比 Physics.OverlapCircle 轮询更精确,也避免了每帧碰撞查询。


2.2 性能问题

【高危】PlayerController.FindDefaultInputReader() 全资产扫描

// PlayerController.cs推测
Resources.FindObjectsOfTypeAll<InputReaderSO>()

Resources.FindObjectsOfTypeAll<T>() 扫描内存中所有已加载对象(包括所有 Asset在大型项目的 Start() 期间可造成 10~50ms 卡顿(取决于资产数量)。

建议:通过 [SerializeField] 在 Inspector 直接赋值;若需运行时发现,改用 ServiceLocator.Get<InputReaderSO>() 或在 GameServiceRegistrar 中注册。


【高危】PlayerController 每帧调用 ResolveDependencies()

// 推测Update() 和 Start() 均调用 HasRequiredStateDependencies()
// HasRequiredStateDependencies() 内部调用 ResolveDependencies()

依赖解析在 Start() 完成后结果不会改变,但代码每帧 Update() 中重复执行。在玩家控制器(每帧必定执行)中,这是纯粹的冗余 CPU 周期。

建议_dependenciesResolved 布尔标志,仅在首次成功后设 true,后续 Update() 跳过检查。


【高危】HurtBox.ReceiveDamage() 每次受击调用 GetComponent<>()

// HurtBox.cs:105
if (_owner is MonoBehaviour mb)
    mb.GetComponent<IStatusEffectable>()?.ApplyStatusEffect(info.Type);

GetComponent<>() 代价比属性访问高约 10-50 倍(内部有 NativeArray 遍历)。在快节奏战斗中玩家/敌人每秒可能受击 5~10 次。

建议:在 Awake() 缓存 _statusEffectable = GetComponentInParent<IStatusEffectable>()


⚠️【中等】EnemyBase.Update() 每帧计算所有敌人到玩家距离

// EnemyBase.cs:180
_stats.DistanceToPlayer = Vector2.Distance(transform.position, _playerTransform.position);

场景中 50 个敌人 = 每帧 50 次 Vector2.Distance(含 sqrt)。可优化为 SqrMagnitude 比较(避免 sqrt或通过 BatchLOSSystem 分帧分批处理。


⚠️【中等】SaveManager 双重序列化 + HMAC 计算

// SaveManager.cs:62-67
string jsonForChecksum = JsonConvert.SerializeObject(_current, Formatting.Indented);
_current.Meta.Checksum = ComputeChecksum(jsonForChecksum);
string finalJson = JsonConvert.SerializeObject(_current, Formatting.Indented);

每次存档执行两次完整 JSON 序列化(第二次含 Checksum 字段)。Formatting.Indented 会产生更大的字符串对象(大量 GC。存档是低频操作主要问题是 Formatting.Indented 的 GC 压力。

建议:生产构建用 Formatting.NoneChecksum 可改为只计算关键字段而非全量 JSON。


⚠️【中等】GlobalObjectPool._alive 列表头删除 O(n)

// GlobalObjectPool.cs:120
po = aliveList[0];
aliveList.RemoveAt(0);   // O(n) — 移动后续所有元素

LRU 回收时 RemoveAt(0) 需要移动列表中所有后续元素。若活跃对象较多(>20累积开销明显。

建议:改用 LinkedList<PooledObject> 或循环队列(Queue<PooledObject>)实现 O(1) LRU。


⚠️【低危】StatusEffectManager.Update() RemoveAt vs _activeList.Remove

代码注释说"逆序遍历避免索引错位",但若 RemoveAt(i) 调用的是索引删除(而非 _activeList.Remove(effect)),则性能更优,逻辑也更清晰。需确认实际调用的是 _activeList.RemoveAt(i) 而非线性搜索的 _activeList.Remove(effect)


三、可扩展性

3.1 亮点

CharmSO + Effect 组合模式

护符效果通过 CharmEffectSO 抽象 + effects[] 数组实现组合Composite Pattern新增护符效果只需新建 ScriptableObject 子类,无需修改 EquipmentManager。这是 Hollow Knight 类游戏护符系统的正确实现方式。

IGameState + GameStateMachine 状态注册表

游戏状态通过接口注册新增游戏状态不需要修改状态机本身Open/Closed 原则)。

ISaveable + SaveManager.Register/Unregister

存档系统通过接口订阅而非硬编码字段,新增可存档组件只需实现 ISaveableSaveManager 不感知具体类型。


3.2 可扩展性问题

【高危】AttackState 连击数硬编码

// AttackState.cs:61
if (_comboIndex < 2)   // 魔法数字:限制为 3 段连击

若需要不同武器/形态有不同连击数,必须修改 AttackState 源码。商业质量的连击系统通常把上限配置在 PlayerAnimationConfigSO.GroundAttacks.Length 中,动态读取。

建议

if (_comboIndex < AnimCfg.GroundAttacks.Length - 1)

【高危】HurtState 硬编码持续时间

// HurtState.cs:15
private const float HurtDuration = 0.4f;

不同敌人攻击的硬直时长应可配置,但 HurtState 不接受外部参数。若特定攻击需要更长/更短的硬直,当前架构无法支持,只能修改常量或添加新状态。

建议Initialize(DamageInfo info) 已存在,将 HurtDuration 改为从 infoPlayerAnimationConfigSO 读取。


⚠️【中等】WorldStateRegistry 多独立 HashSet 硬编码

// WorldStateRegistry.cs
private HashSet<string> _collectedIds = new();
private HashSet<string> _activatedSavePoints = new();
private HashSet<string> _openedDoors = new();
private HashSet<string> _destroyedObjects = new();
private HashSet<string> _flags = new();

每种世界状态类型都需要修改 WorldStateRegistry 类本身。若新增"已解锁秘密房间"类别,需添加新字段、新方法、新序列化逻辑。

建议:统一用 Dictionary<WorldStateType, HashSet<string>> 或单一 _flags 加命名前缀约定("door:xxx" / "collect:xxx")。


⚠️【中等】InputReaderSO 无输入优先级/拦截机制

InputReaderSO 直接暴露 C# Action 事件,没有优先级层、拦截点或输入消费标记。当对话/UI 弹出时,需要手动调用 EnableUIInput() / DisableGameplayInput(),且整个 Gameplay map 被禁用。这在有多个输入消费方(技能、背包、对话)的游戏中容易产生竞争。

参考Celeste 使用 InputManager 层级 + 输入消费标记consumed flag防止事件穿透。


⚠️【中等】EnemyBase.ForceState() 无完整状态回调

// EnemyBase.cs:153
public void ForceState(EnemyStateType newState)
{
    _currentState = newState;
    // Phase 2根据状态播放对应动画 / 触发硬直计时
}

ForceState 没有 OnExit / OnEnter 回调。当前用 // Phase 2 注释占位但这意味着敌人的任何行为改变动画切换、计时器重置、Navigation 停止)都需要调用方自己处理,极易出现漏调。

建议:即使 Phase 1 简化实现,也应在 ForceState 内置虚方法调用框架:

_current?.OnExit();
_currentState = newState;
OnEnemyStateEnter(newState);

四、编辑器友好性

4.1 亮点

EventBusMonitor#if UNITY_EDITOR

BaseEventChannelSO.Raise() 中集成编辑器监控,可以追踪每个事件频道的触发历史和订阅者数量,是非常实用的调试工具。

[DefaultExecutionOrder] 明确声明

各组件明确声明执行顺序(-2000 / -1000 / -900 / -800 / -100避免了 Unity 脚本执行顺序的隐式依赖,易于维护。

[Header] / [SerializeField] 组织清晰

各 MonoBehaviour 使用 [Header] 分组序列化字段Inspector 布局清晰。

GlobalObjectPool.PoolConfig 可序列化结构

[Serializable] PoolConfig 使对象池配置可在 Inspector 中编辑,比硬编码 string 数组更安全。


4.2 编辑器友好性问题

【高危】PlayerAnimationConfigSOfloat[] 帧事件时间

// AttackState.cs:42-46
float enterTime = AnimCfg?.GroundAttackHitBoxEnterTimes?[_comboIndex] ?? 0.3f;
float exitTime  = AnimCfg?.GroundAttackHitBoxExitTimes?[_comboIndex]  ?? 0.6f;

GroundAttackHitBoxEnterTimesGroundAttackHitBoxExitTimesfloat[],在 Inspector 中显示为纯数字数组,与具体动画帧没有视觉对应关系。美术和设计师在调整 HitBox 时机时必须手动换算归一化时间0.0~1.0)。

这与 Hollow Knight / Celeste 的帧事件工作流有明显差距——通常这类数据通过专门的 Animancer 帧事件 Inspector 工具Timeline 预览 + Gizmo来可视化编辑。

建议:为 PlayerAnimationConfigSO 编写 CustomPropertyDrawer,在 Inspector 中显示当前 AnimationClip 帧对应的归一化时间;或集成 Animancer 的 AnimancerEvent.Sequence 并在 Inspector 中预览。


【中等】缺少关键 Gizmo

以下关键运行时数据在 Scene 视图中不可见:

  • HitBox 的实际碰撞范围(只有 Collider Gizmo没有攻击伤害信息提示
  • EnemyStatsSO.DetectionRange / AttackRange 的可视化圆
  • BatchLOSSystem(若存在)的视线射线可视化
  • GlobalObjectPool 的活跃/待机对象数量 HUD

商业项目(尤其是动作游戏)通常有丰富的 OnDrawGizmosSelected() 实现,辅助设计师在不运行游戏的情况下进行关卡设计。


⚠️【中等】SaveManager.cs 缺少存档调试工具

SaveManager 没有编辑器菜单 / Inspector 按钮用于:

  • 在 Inspector 中查看当前 _current SaveData 内容
  • 一键清除所有存档槽
  • 手动触发 SaveAsync / LoadAsync

对于快速迭代来说这些工具往往节省大量时间。Hollow Knight 据报道有完整的内部调试存档面板。


⚠️【低危】EnemyBase 缺少运行时状态可视化

EnemyBase.CurrentStateEnemyStateType)在运行时无法在 Inspector 中直接看到,只能靠断点或 Debug.Log。将 [field: SerializeField, ReadOnly] 或通过 Custom Inspector 暴露运行时状态,可大幅提升调试效率。


五、使用便利性(开发者体验)

5.1 亮点

PlayerController.GetState<T>() 泛型访问器

Owner.TryTransitionState(Owner.GetState<IdleState>());

泛型状态访问器比枚举索引更安全(编译时类型检查),比 as 转型更简洁。

ISaveable + ISaveStorage 接口分离

SaveManager 不直接依赖文件系统,ISaveStorage 可注入 InMemoryStorage(测试用)/ CloudStorage(扩展用)。良好的 DI 设计。

EquipmentManager.TryEquipCharm() 返回错误字符串

public string TryEquipCharm(CharmSO charm)  // null=成功string=失败原因

返回错误消息而非 bool调用方UI可以直接将错误显示给玩家无需额外查询失败原因。

CompositeDisposable + AddTo() 链式订阅

channel.Subscribe(Handler).AddTo(_subscriptions);

Rx 风格的订阅生命周期管理,在 OnDisable 一行 Clear() 即可安全取消所有订阅,不会出现常见的订阅泄漏。


5.2 使用便利性问题

【高危】SaveManager.QuickSave() Fire-and-Forget 异常吞噬

// SaveManager.cs:118
public void QuickSave() => _ = SaveAsync(QuickSaveSlot);
public void QuickLoad() => _ = LoadAsync(QuickSaveSlot);

_ = SaveAsync(...) 丢弃了 Task 对象,任何 SaveAsync 内部的异常(包括文件写入失败、序列化异常)都会被静默吞掉,不会触发任何错误提示,也不会调用 Debug.LogError

在生产环境中这意味着:玩家可能以为存档成功,实际上存档悄悄失败了。

建议

public void QuickSave() => SaveAsync(QuickSaveSlot).LogErrors("[SaveManager] QuickSave failed");
// 实现扩展方法:
public static async void LogErrors(this Task task, string msg)
{
    try { await task; }
    catch (Exception e) { Debug.LogError($"{msg}: {e}"); }
}

【高危】ServiceLocator.Get<T>() 失败无异常栈

// ServiceLocator.cs推测
Debug.LogError($"[ServiceLocator] 未找到服务 {typeof(T).Name}");
return default;

返回 defaultnull而非抛出异常调用方通常不会检查返回值导致 NullReferenceException完全不相关的位置崩溃,调试时难以定位根本原因。

建议:提供两种方法:

public static T Get<T>()          // 未找到时抛 ServiceNotRegisteredException
public static T GetOrDefault<T>() // 未找到时返回 null已存在

⚠️【中等】InputReaderSO 缺少统一"是否按下"轮询 API

InputReaderSO 目前只有 MoveInputVector2可轮询其他输入Jump、Attack、Dash 等)只能通过事件订阅。当状态机需要在 OnStateUpdate() 中检查"当前帧 Attack 是否被按住"时,无法通过 SO 轮询,只能通过外部缓冲标志(InputBuffer)。

设计上并无大错,但不一致性(部分支持轮询,部分不支持)会导致混淆。


⚠️【中等】PlayerController 外部状态暴露粒度粗

PlayerController 通过 GetState<T>() 暴露所有状态对象,但外部代码(如 UI、Enemy AI通常只需要询问"玩家是否在攻击中",而不需要直接访问 AttackState 实例。这会导致外部代码对状态对象的直接依赖,破坏封装。

建议:在 PlayerController 上增加语义化查询属性:

public bool IsAttacking => _currentState is AttackState;
public bool IsInvincible => Stats.IsInvincible;
public bool IsGrounded  => Move.IsGrounded;

⚠️【低危】EnemyBase.Die() 方法缺失(可见调用但未定义)

在读取的 EnemyBase.cs 中,TakeDamage() 调用了 Die(),但 Die() 方法体在阅读的代码段中未找到完整实现(可能在文件末尾)。若 Die() 为虚方法但基类无默认实现(只是抛异常或空实现),子类必须知道重写它,否则敌人死亡逻辑静默失败。


六、模块级评估

6.1 战斗系统HitBox / HurtBox——

优点:架构上已达商业级水准。

  • DamageInfo struct 零 GC
  • 8步伤害管道完整定义
  • Per-target 冷却(Dictionary<Collider2D, float>)避免同帧多次命中
  • _rivalHitBoxMask LayerMask 可配置

主要问题

  • HurtBox 第8步 GetComponent<IStatusEffectable>() 每次受击都调用(应缓存)
  • DamageFlagsIgnoreIFrame 设计良好,但缺少文档说明哪些攻击应设置此标志

6.2 存档系统SaveManager—— ½

优点

  • ISaveable 订阅模式Register/Unregister
  • HMACSHA256 Checksum防篡改
  • SaveMigrator版本迁移设计前瞻
  • async Task 全异步 IO

主要问题

  • 仍使用 public static InstanceSingleton 不一致)
  • QuickSave() fire-and-forget 异常吞噬
  • 双重 JSON 序列化(性能浪费)
  • Checksum 密钥用 SystemInfo.deviceUniqueIdentifier(设备唯一标识),玩家更换设备时存档将永远校验失败——这是逻辑 Bug

6.3 对象池GlobalObjectPool—— ½

优点

  • Addressables 异步预热
  • LRU 回收机制(有上限控制)
  • Background Refill Coroutine

主要问题

  • 仍使用 public static Instance
  • LRU RemoveAt(0) 是 O(n)
  • GetComponentCached<T>() 方法未在阅读代码中找到实现(若不存在则是命名不一致)

6.4 事件系统EventChannel—— ½

优点

  • 泛型基类 + 派生具体类型
  • EventSubscription + CompositeDisposable
  • EventBusMonitor 编辑器监控

主要问题

  • OnEventRaised?.GetInvocationList().Length 在 Editor 监控中:每次 Raise 都分配 GetInvocationList() 返回的 Delegate[] 数组(即使在 Editor 中)。可以改用 _listenerCount 计数器字段避免分配。

6.5 敌人系统EnemyBase——

优点

  • IPathAgent 接口隔离PathBerserker2d 依赖被包装)
  • ILOSRequester 接口支持批量 LOS 计算
  • BD Task 通过虚方法接口访问,避免直接依赖 BehaviorTree API

主要问题

  • FindWithTag("Player") 全场景扫描N个敌人 × 场景加载时)
  • ForceState() 无状态进入/退出回调
  • Update() 每帧 Vector2.Distance()

6.6 玩家状态机PlayerController + States——

优点

  • POCO 状态机(无 MonoBehaviour 开销)
  • Dictionary<Type, PlayerStateBase> 注册 + GetState<T>() 泛型访问
  • 构造函数注入PlayerController → PlayerStateBase

主要问题

  • IPoiseSource.GetCurrentPoiseLevel() 硬编码返回 PoiseLevel.None
  • AttackState 连击数硬编码 < 2
  • HurtState.HurtDuration 硬编码 0.4f
  • FindDefaultInputReader() 全资产扫描
  • DropThroughPlatform() 空实现stub未标注 [System.Obsolete]// TODO 跟踪

七、关键问题清单(优先级排序)

优先级 问题 影响范围 建议行动
P0 SaveManager Checksum 用设备ID——换设备必崩 存档完整性 改为游戏固定密钥或用户密码哈希
P0 QuickSave() fire-and-forget 异常吞噬 存档可靠性 添加 .LogErrors() 扩展
P1 EnemyBase FindWithTag("Player") N次全扫描 场景加载性能 事件频道推送 or ServiceLocator 注入
P1 HurtBox 每次受击 GetComponent<IStatusEffectable>() 战斗帧率 Awake 缓存
P1 PlayerController FindDefaultInputReader() 全资产扫描 游戏启动性能 SerializeField 直接赋值
P1 ServiceLocator / static Instance 不一致3处 架构一致性 统一注册入口
P2 AttackState 连击数硬编码 < 2 扩展性 从配置 SO 读取
P2 HurtState.HurtDuration 硬编码 扩展性 从 DamageInfo 或 SO 读取
P2 EnemyBase.ForceState() 无状态回调 敌人行为正确性 添加 OnEnter/OnExit 框架
P2 GlobalObjectPool LRU RemoveAt(0) O(n) 运行时性能 改用 LinkedList 或循环队列
P3 PlayerAnimationConfigSO float[] 帧事件无可视化 编辑器效率 自定义 PropertyDrawer
P3 缺少关键 Gizmo攻击范围、检测范围 关卡设计效率 OnDrawGizmosSelected()
P3 ServiceLocator.Get<T>() 失败无异常栈 调试效率 提供抛异常版本
P3 IPoiseSource.GetCurrentPoiseLevel() 硬编码返回 None 功能完整性 实现真实的霸体等级查询
P3 WorldStateRegistry 多独立 HashSet 扩展性差 可维护性 统一 Dictionary<WorldStateType, HashSet<string>>

八、与商业标准差距总结

已达商业水准

  • 程序集隔离 + 接口驱动的跨模块通信
  • ScriptableObject 事件频道 + 生命周期安全订阅
  • DamageInfo struct 零 GC 战斗管道
  • 8步伤害流水线弹反/霸体/护盾/减免)
  • ISaveable + async IO + SaveMigrator + Checksum
  • CharmSO Effect 组合模式
  • Animancer 帧事件驱动 HitBox 时机

尚未达商业水准

  • 一致性3处核心 Manager 绕过 ServiceLocator
  • 性能细节:多个热路径 GetComponent<>() 未缓存
  • 调试工具:缺少 Custom Inspector / Gizmo / Runtime Debug HUD
  • 配置数据验证float[] 帧事件时间无范围检查(可写入 NaN/负数)
  • 异步安全fire-and-forget 导致存档失败静默
  • 平台 BugChecksum 设备ID 跨设备失效
  • 状态机完整性Enemy ForceState 无回调、玩家 DropThroughPlatform 空 stub
  • 输入拦截:无优先级/消费标记机制

工作量估算(仅关键 P0~P1 修复)

任务 估算工时
Checksum 密钥修复 0.5h
QuickSave fire-and-forget 修复 1h
HurtBox GetComponent 缓存 0.5h
EnemyBase FindWithTag 替换(含事件频道设计) 3h
PlayerController InputReader SerializeField 0.5h
3处 Instance 迁移至 ServiceLocator 4h
合计 ~9.5h

本报告基于对 Assets/Scripts/ 下约 20 个关键源文件的直接阅读,覆盖 Core、Combat、Player、Enemies、Save、Equipment、World、Events、Pool 模块。未直接覆盖的模块Audio、UI、Quest、Cutscene、Localization需另行评审。