多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,583 @@
# 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` 继承链 + `PlayerController``Dictionary<Type, PlayerStateBase>` 注册表实现了真正的无 MonoBehaviour 状态机,可以充分利用 C# 类型系统,比 Animator State Machine 更可测试。
#### ✅ 战斗管道接口隔离
`HurtBox` 通过 `SetShieldable() / SetParrySystem() / SetPoiseSource()` 注入接口,而非直接依赖具体类;`HitBox` 通过 `IBreakable` 接口处理可破坏物体——这消除了跨程序集硬耦合。
#### ✅ ServiceLocator + 注册器分离
`GameServiceRegistrar``DefaultExecutionOrder(-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")`
```csharp
// EnemyBase.cs:162
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
```
场景中有 N 个敌人时,`Awake()` 被调用 N 次每次执行全场景标签扫描O(n) 场景对象遍历)。在 Hollow Knight / Celeste 这类商业项目中,此类查找统一通过事件频道推送或注册表注入。
**建议**:在 `GameServiceRegistrar``PlayerController.Awake()` 完成后,通过 `TransformEventChannelSO` 广播玩家 Transform所有敌人订阅该事件接收引用。
---
#### ⚠️【中等】`GameStateMachine` 缺乏守卫条件Guard Condition
```csharp
// GameStateMachine.cs:29
if (_current != null && !_current.ValidNextStates.Contains(nextId))
```
`ValidNextStates` 是运行时列表检查,属于防御式断言而非真正的状态转换守卫。商业质量的状态机通常为每个转换弧定义独立的 `CanTransitionTo(context)` 谓词,以便同一目标状态在不同条件下可以允许或拒绝转换,逻辑集中在转换定义上,而非分散在调用方。
---
#### ⚠️【中等】`EquipmentManager` 直接使用 `EventChannelRegistry.Instance`
```csharp
// 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 设计
`DamageInfo``struct` + `DamageInfo.From()` 工厂方法,在战斗最热路径上避免堆分配。在帧率敏感的动作游戏中,这是正确的优先选择。
#### ✅ `StatusEffectManager` 双结构List + Dictionary
`_activeList`O(1) Update 遍历)+ `_activeIndex`O(1) 类型查找)是标准的游戏引擎 ECS 风格优化,避免了在 Update 中进行字典遍历。
#### ✅ `GlobalObjectPool` LRU 回收 + Addressables 预热
池空时的 Background Refill Coroutine + Addressables 异步加载是成熟的生产级对象池模式。
#### ✅ Animancer 帧事件驱动 HitBox
帧事件(`events.Add(enterTime, ...)` / `events.Add(exitTime, ...)`)比 `Physics.OverlapCircle` 轮询更精确,也避免了每帧碰撞查询。
---
### 2.2 性能问题
#### ❌【高危】`PlayerController.FindDefaultInputReader()` 全资产扫描
```csharp
// PlayerController.cs推测
Resources.FindObjectsOfTypeAll<InputReaderSO>()
```
`Resources.FindObjectsOfTypeAll<T>()` 扫描内存中**所有已加载对象**(包括所有 Asset在大型项目的 `Start()` 期间可造成 10~50ms 卡顿(取决于资产数量)。
**建议**:通过 `[SerializeField]` 在 Inspector 直接赋值;若需运行时发现,改用 `ServiceLocator.Get<InputReaderSO>()` 或在 `GameServiceRegistrar` 中注册。
---
#### ❌【高危】`PlayerController` 每帧调用 `ResolveDependencies()`
```csharp
// 推测Update() 和 Start() 均调用 HasRequiredStateDependencies()
// HasRequiredStateDependencies() 内部调用 ResolveDependencies()
```
依赖解析在 `Start()` 完成后结果不会改变,但代码每帧 `Update()` 中重复执行。在玩家控制器(每帧必定执行)中,这是纯粹的冗余 CPU 周期。
**建议**`_dependenciesResolved` 布尔标志,仅在首次成功后设 `true`,后续 `Update()` 跳过检查。
---
#### ❌【高危】`HurtBox.ReceiveDamage()` 每次受击调用 `GetComponent<>()`
```csharp
// 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()` 每帧计算所有敌人到玩家距离
```csharp
// EnemyBase.cs:180
_stats.DistanceToPlayer = Vector2.Distance(transform.position, _playerTransform.position);
```
场景中 50 个敌人 = 每帧 50 次 `Vector2.Distance`(含 `sqrt`)。可优化为 `SqrMagnitude` 比较(避免 sqrt或通过 `BatchLOSSystem` 分帧分批处理。
---
#### ⚠️【中等】`SaveManager` 双重序列化 + HMAC 计算
```csharp
// 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.None`Checksum 可改为只计算关键字段而非全量 JSON。
---
#### ⚠️【中等】`GlobalObjectPool._alive` 列表头删除 O(n)
```csharp
// 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`
存档系统通过接口订阅而非硬编码字段,新增可存档组件只需实现 `ISaveable`SaveManager 不感知具体类型。
---
### 3.2 可扩展性问题
#### ❌【高危】`AttackState` 连击数硬编码
```csharp
// AttackState.cs:61
if (_comboIndex < 2) // 魔法数字:限制为 3 段连击
```
若需要不同武器/形态有不同连击数,必须修改 `AttackState` 源码。商业质量的连击系统通常把上限配置在 `PlayerAnimationConfigSO.GroundAttacks.Length` 中,动态读取。
**建议**
```csharp
if (_comboIndex < AnimCfg.GroundAttacks.Length - 1)
```
---
#### ❌【高危】`HurtState` 硬编码持续时间
```csharp
// HurtState.cs:15
private const float HurtDuration = 0.4f;
```
不同敌人攻击的硬直时长应可配置,但 `HurtState` 不接受外部参数。若特定攻击需要更长/更短的硬直,当前架构无法支持,只能修改常量或添加新状态。
**建议**`Initialize(DamageInfo info)` 已存在,将 `HurtDuration` 改为从 `info``PlayerAnimationConfigSO` 读取。
---
#### ⚠️【中等】`WorldStateRegistry` 多独立 HashSet 硬编码
```csharp
// 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()` 无完整状态回调
```csharp
// EnemyBase.cs:153
public void ForceState(EnemyStateType newState)
{
_currentState = newState;
// Phase 2根据状态播放对应动画 / 触发硬直计时
}
```
`ForceState` 没有 `OnExit` / `OnEnter` 回调。当前用 `// Phase 2` 注释占位但这意味着敌人的任何行为改变动画切换、计时器重置、Navigation 停止)都需要调用方自己处理,极易出现漏调。
**建议**:即使 Phase 1 简化实现,也应在 `ForceState` 内置虚方法调用框架:
```csharp
_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 编辑器友好性问题
#### ❌【高危】`PlayerAnimationConfigSO` 的 `float[]` 帧事件时间
```csharp
// AttackState.cs:42-46
float enterTime = AnimCfg?.GroundAttackHitBoxEnterTimes?[_comboIndex] ?? 0.3f;
float exitTime = AnimCfg?.GroundAttackHitBoxExitTimes?[_comboIndex] ?? 0.6f;
```
`GroundAttackHitBoxEnterTimes``GroundAttackHitBoxExitTimes``float[]`,在 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.CurrentState``EnemyStateType`)在运行时无法在 Inspector 中直接看到,只能靠断点或 Debug.Log。将 `[field: SerializeField, ReadOnly]` 或通过 Custom Inspector 暴露运行时状态,可大幅提升调试效率。
---
## 五、使用便利性(开发者体验)
### 5.1 亮点
#### ✅ `PlayerController.GetState<T>()` 泛型访问器
```csharp
Owner.TryTransitionState(Owner.GetState<IdleState>());
```
泛型状态访问器比枚举索引更安全(编译时类型检查),比 `as` 转型更简洁。
#### ✅ `ISaveable` + `ISaveStorage` 接口分离
`SaveManager` 不直接依赖文件系统,`ISaveStorage` 可注入 `InMemoryStorage`(测试用)/ `CloudStorage`(扩展用)。良好的 DI 设计。
#### ✅ `EquipmentManager.TryEquipCharm()` 返回错误字符串
```csharp
public string TryEquipCharm(CharmSO charm) // null=成功string=失败原因
```
返回错误消息而非 bool调用方UI可以直接将错误显示给玩家无需额外查询失败原因。
#### ✅ `CompositeDisposable` + `AddTo()` 链式订阅
```csharp
channel.Subscribe(Handler).AddTo(_subscriptions);
```
Rx 风格的订阅生命周期管理,在 `OnDisable` 一行 `Clear()` 即可安全取消所有订阅,不会出现常见的订阅泄漏。
---
### 5.2 使用便利性问题
#### ❌【高危】`SaveManager.QuickSave()` Fire-and-Forget 异常吞噬
```csharp
// SaveManager.cs:118
public void QuickSave() => _ = SaveAsync(QuickSaveSlot);
public void QuickLoad() => _ = LoadAsync(QuickSaveSlot);
```
`_ = SaveAsync(...)` 丢弃了 `Task` 对象,任何 `SaveAsync` 内部的异常(包括文件写入失败、序列化异常)都会被**静默吞掉**,不会触发任何错误提示,也不会调用 `Debug.LogError`
在生产环境中这意味着:玩家可能以为存档成功,实际上存档悄悄失败了。
**建议**
```csharp
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>()` 失败无异常栈
```csharp
// ServiceLocator.cs推测
Debug.LogError($"[ServiceLocator] 未找到服务 {typeof(T).Name}");
return default;
```
返回 `default`null而非抛出异常调用方通常不会检查返回值导致 `NullReferenceException` 在**完全不相关的位置**崩溃,调试时难以定位根本原因。
**建议**:提供两种方法:
```csharp
public static T Get<T>() // 未找到时抛 ServiceNotRegisteredException
public static T GetOrDefault<T>() // 未找到时返回 null已存在
```
---
#### ⚠️【中等】`InputReaderSO` 缺少统一"是否按下"轮询 API
`InputReaderSO` 目前只有 `MoveInput``Vector2`可轮询其他输入Jump、Attack、Dash 等)只能通过事件订阅。当状态机需要在 `OnStateUpdate()` 中检查"当前帧 Attack 是否被按住"时,无法通过 SO 轮询,只能通过外部缓冲标志(`InputBuffer`)。
设计上并无大错,但不一致性(部分支持轮询,部分不支持)会导致混淆。
---
#### ⚠️【中等】`PlayerController` 外部状态暴露粒度粗
`PlayerController` 通过 `GetState<T>()` 暴露所有状态对象,但外部代码(如 UI、Enemy AI通常只需要询问"玩家是否在攻击中",而不需要直接访问 `AttackState` 实例。这会导致外部代码对状态对象的直接依赖,破坏封装。
**建议**:在 `PlayerController` 上增加语义化查询属性:
```csharp
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>()` 每次受击都调用(应缓存)
- `DamageFlags``IgnoreIFrame` 设计良好,但缺少文档说明哪些攻击应设置此标志
---
### 6.2 存档系统SaveManager—— ⭐⭐⭐½
**优点**
- ISaveable 订阅模式Register/Unregister
- HMACSHA256 Checksum防篡改
- SaveMigrator版本迁移设计前瞻
- async Task 全异步 IO
**主要问题**
- 仍使用 `public static Instance`Singleton 不一致)
- `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 导致存档失败静默
- **平台 Bug**Checksum 设备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需另行评审。*