多轮审查和修复

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,769 @@
# zeling_v2 代码全面评审
> 评审日期2026-05-11
> 评审范围:`Assets/Scripts/` 全部模块(约 25 个 Assembly Definition覆盖 Combat、Player、Enemies、Core、UI、World 等)
> 评审标准以《空洞骑士》《Celeste》《Neon Abyss》等成熟商业 2D 动作游戏代码质量为参照基准
> 说明本次评审在上一轮优化AdvancedCodeReview.md的基础上进行反映所有已实施改动后的当前代码状态。
---
## 目录
1. [评分总览](#1-评分总览)
2. [架构设计](#2-架构设计)
3. [性能](#3-性能)
4. [可扩展性](#4-可扩展性)
5. [编辑器友好性](#5-编辑器友好性)
6. [使用便利性DX](#6-使用便利性dx)
7. [问题优先级汇总表](#7-问题优先级汇总表)
8. [优化建议](#8-优化建议)
---
## 1. 评分总览
| 维度 | 得分(满分 10 | 与商业基准差距 |
|---|---|---|
| **架构设计** | 7.5 | 基础扎实,单例滥用是主要拖分项 |
| **性能** | 7.0 | 热路径已优化,若干 Update 开销尚存 |
| **可扩展性** | 7.5 | Assembly 划分优秀,配置驱动待完善 |
| **编辑器友好性** | 6.5 | 工具链有亮点SO 验证未落地 |
| **使用便利性** | 7.0 | API 设计清晰,异步一致性有缺口 |
| **综合** | **7.1** | 优于大多数独立游戏,接近中等商业质量 |
---
## 2. 架构设计
### 2.1 优点
#### ✅ 事件频道SO Event Channel模式落地质量高
`BaseEventChannelSO<T>` 泛型事件基类设计完善:
```csharp
// BaseEventChannelSO.cs
public EventSubscription Subscribe(Action<T> callback)
{
OnEventRaised += callback;
return new EventSubscription(() => OnEventRaised -= callback);
}
```
- 返回 `EventSubscription` 可组合到 `CompositeDisposable`,避免手动取消订阅遗漏
- `EventBusMonitor`(仅 Editor记录 256 条事件日志,帧号+监听器数量一应俱全
- 约 35+ 个具体频道类型覆盖全模块,系统间全部解耦
> 商业对比与《GDC 2017 - Unite Austin》推荐的 SO 架构一致,执行完整度超过大多数独立参考项目。
#### ✅ 25 个 Assembly Definition 依赖拓扑明确
```
BaseGames.Core.Events
BaseGames.Core
BaseGames.Combat BaseGames.Parry BaseGames.Player
↑ ↑
BaseGames.Enemies BaseGames.Player.States
```
- `BaseGames.Parry` 不依赖 `BaseGames.Combat``HurtBox.ConsumeParry()` 无跨程序集数据,干净
- `BaseGames.Player.States` 单独隔离16 个状态类不污染 Player 程序集
#### ✅ 玩家 FSMPOCO 状态类 + 类型字典派发
```csharp
// PlayerController.cs
private readonly Dictionary<Type, PlayerStateBase> _states = new();
// ...
_states[typeof(AttackState)] = new AttackState(this);
public T GetState<T>() where T : PlayerStateBase
=> _states.TryGetValue(typeof(T), out var s) ? (T)s : null;
```
- 状态不继承 `MonoBehaviour`,无 Unity 序列化负担
- `GetState<T>()` 零 GCO(1) 字典查找
- 状态基类 `PlayerStateBase` 提供 `Owner`/`Input`/`Anim`/`Cfg` 便捷属性,子类代码清晰
#### ✅ ServiceLocator 实现规范
```csharp
public static TInterface GetOrDefault<TInterface>(TInterface fallback = default) { ... }
#if UNITY_EDITOR
public static void OverrideForTest<TInterface>(TInterface mock) { ... }
public static void Reset() { ... }
#endif
```
- 接口类型注册,支持依赖倒置
- 提供 Editor 专用测试 override/reset测试友好
#### ✅ 对象池完整实现
- Addressables 异步预热(`WarmupAsync` / `WarmupCoroutine` 双版本)
- `LinkedList<PooledObject>` 活跃列表LRU O(1) 强制回收
- `GetComponentCached<T>()` 扩展方法避免重复反射
#### ✅ SaveManager HMAC 校验 + 版本迁移
```csharp
// SaveMigrator.Migrate(loaded) 确保旧存档平滑升级
loaded = SaveMigrator.Migrate(loaded);
```
- `RunFireAndForget` 正确捕获 async void 中的异常
- `Formatting.None` 减少序列化体积
---
### 2.2 问题
#### ⚠️ P1单例static Instance与 ServiceLocator 混用
项目同时使用两种全局访问模式:
| 类 | 访问方式 |
|---|---|
| `GameManager` | `static Instance` |
| `SaveManager` | `static Instance` |
| `MapManager` | `static Instance` |
| `QuestManager` | `static Instance` |
| `GlobalObjectPool` | `static Instance` |
| `IPlatformService` | `ServiceLocator.Get<>()` |
| `ISceneService` | `ServiceLocator.Get<>()` |
| `IAudioService` | `ServiceLocator.Get<>()` |
混用导致:
- `MapManager.Instance?.Register(this)``OnEnable` 中调用,若 MapManager 未激活则静默失败
- 无法对 `SaveManager``QuestManager` 进行单元测试
- 多场景重新加载时 Instance 生命周期难以追踪
**商业标准:** 《Hollow Knight》等成熟项目统一通过 DI 容器或 ServiceLocator 管理;绝不混用。
#### ⚠️ P1EnemyBase 仍保留 Awake 中 FindWithTag已修复但注释混乱
修复前 Awake 仍调用 `FindWithTag`,已在本次评审中同步修复(添加事件订阅,移除旧代码)。说明之前的优化不完整,留下了"Phase 1 注释"与"Phase 2 实现"共存的混乱状态:
```csharp
// Start() 的兜底查找与 Awake() 的 FindWithTag 同时存在(已修复)
// 这种 "Phase 注释" 模式在多次迭代后容易造成死代码残留
```
**建议:** 移除所有 `// Phase 1` / `// Phase 2` 标注,改用 `// TODO:` 或 git branch 管理迭代。
#### ⚠️ P2EnemyBase.ForceState() 是空实现
```csharp
public void ForceState(EnemyStateType newState)
{
_currentState = newState;
// Phase 2根据状态播放对应动画 / 触发硬直计时
}
```
只更新枚举值,无任何动画/计时副作用。`TakeDamage()` 调用 `ForceState(Hurt)` 后敌人没有实际硬直反馈,是一个静默的逻辑空洞。
#### ⚠️ P2UIManager 忽略商店 ID 参数
```csharp
private void OpenShop(string _) => OpenPanel(_shopRoot);
```
`_onShopOpen` 携带 `string shopId`(商店 ID`UIManager` 直接丢弃。若游戏后期出现多个商店,此处需要重构,届时影响面较大。
#### ⚠️ P2GameStateMachine 与 GameStateId 的 struct 滥用
`GameStateId` 被设计为 struct值类型但内部包含 string `Id`
```csharp
// 示例推断GameStateId 包含 string Id 字段
if (state == GameStates.Gameplay || state == GameStates.BossFight)
```
struct 值比较需要 `IEquatable<T>` 重写,否则 `==` 走装箱比较,每次 `HandleGameStateChanged` 触发都可能有 GC 分配。
---
## 3. 性能
### 3.1 优点
#### ✅ 热路径 GetComponent 全部缓存
```csharp
// HurtBox.cs - Awake 缓存,每次受击无 GetComponent
private IStatusEffectable _statusEffectable;
private void Awake() => _statusEffectable = GetComponentInParent<IStatusEffectable>();
```
#### ✅ 距离计算使用 sqrMagnitude
```csharp
// EnemyBase.Update()
_stats.SqrDistanceToPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).sqrMagnitude;
// IsPlayerInRange
return _stats.SqrDistanceToPlayer <= range * range;
```
避免每帧 `Mathf.Sqrt`100 个敌人同屏时节省约 100 次浮点开平方。
#### ✅ 对象池 LRU O(1) 回收
`LinkedList<PooledObject>` 头节点始终是最早 Spawn 的对象,强制回收 O(1),替换原 `List.RemoveAt(0)` 的 O(n) 移位。
#### ✅ 事件频道无 GC 分配C# event
`Action<T>` delegate 调用无堆分配,优于 `UnityEvent`(有对象封装)。
#### ✅ StatusEffectManager 双结构避免重复遍历
```csharp
private readonly List<StatusEffect> _activeList = new(); // Update 遍历
private readonly Dictionary<StatusEffectType, StatusEffect> _activeIndex = new(); // O(1) 类型查找
```
逆序遍历 List 安全移除,`Dictionary` 保证施加同类效果时 O(1) 查找堆叠。
---
### 3.2 问题
#### ⚠️ P1PlayerMovement.UpdateFacing() 使用 localScale 翻转
```csharp
transform.localScale = new Vector3(Mathf.Abs(s.x) * dir, s.y, s.z);
```
`localScale` 负值翻转会:
1. 影响子节点所有 Collider 的物理中心(尤其是圆形 Collider
2. 导致 UI Canvas World Space 子元素同步翻转
3. 与粒子系统、Shaderuv 方向)产生兼容问题
**商业标准:** Celeste / HK 均使用 `SpriteRenderer.flipX = true` 或旋转 `transform.rotation`,绝不翻转 Scale。
#### ⚠️ P1GameServiceRegistrar 使用 FindObjectsOfType场景加载时触发
```csharp
// GameServiceRegistrar.EnsureSingleAudioListener()
AudioListener[] listeners = FindObjectsOfType<AudioListener>(true);
```
`SceneManager.sceneLoaded` 回调中调用 `FindObjectsOfType`,每次场景加载全场景扫描。若场景中 GameObject 数量大,这是一个可测量的加载卡顿点。
**建议:** 改为每个场景相机自行注册 AudioListener或在场景 Bootstrap 时主动禁用多余监听器。
#### ⚠️ P2DashState 使用魔法数字兜底
```csharp
float force = _config != null ? _config.JumpForce : 18f; // PlayerMovement
Stats?.BeginInvincibility(Cfg != null ? Cfg.DashDuration + 0.05f : 0.23f); // DashState
Move?.SetGravityScale(Cfg != null ? Cfg.DefaultGravityScale : 3f);
```
每个状态类都有 `Cfg != null ? ... : hardcoded` 二元表达式,分散了数值配置源,调试时不知道实际值来自哪里。
**建议:**`PlayerStateBase` 基类或 `PlayerController` 中加 `[RequireComponent]``Awake` 断言,确保配置 SO 永远不为 null消除运行时兜底逻辑。
#### ⚠️ P2多处 Coroutine + async Task 并存GlobalObjectPool
```csharp
public IEnumerator WarmupCoroutine() { ... } // 相同逻辑
public async Task WarmupAsync() { ... } // 相同逻辑
```
同一预热逻辑实现了两遍,维护时需同步修改两个版本。
**建议:** 统一为 `async Task`,在需要 Coroutine 调用方处用 `StartCoroutine(WarmupAsync().AsCoroutine())` 桥接。
#### ⚠️ P3LocalizationManager.Get() 静默吞掉异常
```csharp
catch
{
return entryKey;
}
```
本地化读取失败时返回 Key 字符串并且不打 Log生产环境难以排查本地化数据缺失问题。
---
## 4. 可扩展性
### 4.1 优点
#### ✅ 配置数据与逻辑完全分离
- `PlayerAnimationConfigSO`:动画片段 + HitBox 时间点 + 硬直时长
- `PlayerMovementConfigSO`:加速度、跳跃力、土狼时间、冲刺参数
- `EnemyStatsSO`HP、攻击力、视野、寻路参数
设计师可在不修改代码的情况下独立调整全部数值。
#### ✅ 攻击连击段数动态读取
```csharp
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
if (_comboIndex < maxCombo - 1) { ... }
```
`GroundAttacks[]` 数组长度决定连击段数,添加新连击只需在 SO 中添加一个 AnimationClip。
#### ✅ IPathAgent 接口抽象导航层
```csharp
protected IPathAgent _nav; // EnemyBase
// 由子类/Inspector注入或 GetComponent<IPathAgent>()
```
PathBerserker2d / A* Pathfinding / NavMesh2D 均可通过实现 IPathAgent 无缝替换。
#### ✅ ISaveable / SaveMigrator 版本化存档
```csharp
loaded = SaveMigrator.Migrate(loaded);
```
版本迁移管道已建立,存档格式变更时可平滑升级旧存档。
---
### 4.2 问题
#### ⚠️ P1EnemyBase.ForceState() 无扩展点
敌人状态机是扁平的 `EnemyStateType` 枚举 + switch或 if/else没有对应玩家 FSM 的"POCO 状态类 + 字典"设计。添加新敌人状态需要:
1. 扩展 `EnemyStateType` 枚举
2.`ForceState()` / `Update()` 等多处手动添加分支
**与玩家 FSM 差距明显。** 建议将敌人也迁移到 POCO 状态类或完全依赖 Behavior Designer 行为树,二选一,避免两套系统共存。
#### ⚠️ P1WorldStateRegistry 没有泛化的持久化标记 API
```csharp
public bool IsCollected(string id) => _collectedIds.Contains(id);
public bool IsDoorOpened(string id) => _openedDoors.Contains(id);
public bool IsDestroyed(string id) => _destroyedObjects.Contains(id);
```
每增加一类世界实体状态都需要1添加新的 `HashSet<string>` 字段2添加两个方法3更新 `LoadFromSave`/`GetAllFlags`。可扩展性差。
**建议:** 统一为 `Dictionary<string, HashSet<string>>` 按"类别"键隔离,或引入 `enum WorldObjectCategory` 参数化访问。
#### ⚠️ P2技能系统SkillManager冷却计时硬编码三个槽
```csharp
private float _soulCooldown;
private float _spirit1Cooldown;
private float _spirit2Cooldown;
```
支持的技能槽数量在编译时固定为 3。若后续要求更多技能槽需要修改 SkillManager 代码。
**建议:** 改用 `Dictionary<FormSkillSO, float>` 动态冷却表,支持任意数量技能。
---
## 5. 编辑器友好性
### 5.1 优点
#### ✅ EventBusMonitor运行时事件日志
```csharp
#if UNITY_EDITOR
EventBusMonitor.Record(name, value?.ToString(), listenerCount, Time.frameCount);
#endif
```
Editor 模式下所有 SO 事件的触发都会被记录(频道名、负载、监听器数量、帧号),通过自定义窗口可以实时追踪事件流。这是商业游戏才有的调试基础设施。
#### ✅ WorldMarker.OnDrawGizmosSelected()
```csharp
Gizmos.color = _markerType switch
{
WorldMarkerType.Objective => Color.yellow,
WorldMarkerType.NPC => Color.cyan,
WorldMarkerType.PointOfInterest => Color.green,
...
};
```
场景中直接可视化标记类型,减少 playtest 调试成本。
#### ✅ IValidatable 接口
```csharp
public interface IValidatable
{
IEnumerable<string> Validate();
}
```
SO 可实现验证逻辑,但缺少配套的 Editor 工具ValidationRunner接口孤立存在。
#### ✅ CreateAssetMenu 全面覆盖
所有 SO 类均标注 `[CreateAssetMenu(menuName = "...")]`,层级清晰,设计师无需记住 SO 路径。
---
### 5.2 问题
#### ⚠️ P1IValidatable 没有 Runner/Window
接口定义完整,但没有对应的 `SOValidationRunner`Editor 菜单一键验证所有 SO`[MenuItem("Tools/Validate All SOs")]` 实现。目前 `Validate()` 方法从未被调用,等同于死代码。
**商业标准:** 《Hades》《Dead Cells》均有数据验证管道CI 中自动跑,防止设计师填写空/越界数据导致运行时崩溃。
#### ⚠️ P1PlayerController Inspector 字段过多(约 18 个 SerializeField
```csharp
[Header("核心组件")] // 3个
[Header("配置")] // 5个
[Header("战斗")] // 9个
[Header("事件频道")] // 3个
```
18 个字段全部需要在 Inspector 中手动拖拽赋值,其中 `_movement``_stats``_animancer``_inputBuffer` 均挂在同一 GameObject 上,理应通过 `[RequireComponent]` + `Awake` 自动获取。
**建议:** 同节点组件改为 `RequireComponent` + Awake GetComponent跨节点引用保留 `[SerializeField]`。可将约 18 个字段减至 810 个,降低出错概率。
#### ⚠️ P2PostProcessManager 使用 Component 类型代替具体 Volume 类型
```csharp
[SerializeField] private Component _bossArenaVolume; // Assign a Volume component
```
Inspector 中接受任意 `Component`,没有类型约束,设计师可能误拖 `Transform``Collider`。运行时通过类型转换才能发现错误。
**建议:** 改为 `UnityEngine.Rendering.Volume` 或具体后处理 Volume 类型。
#### ⚠️ P3HurtBox Inspector 字段在代码中注入而非序列化
```csharp
// HurtBox.cs
public void SetShieldable(IShieldable shieldable) => _shieldable = shieldable;
public void SetParrySystem(ParrySystem ps) => _parrySystem = ps;
public void SetPoiseSource(IPoiseSource src) => _poiseSource = src;
```
这些依赖在 `PlayerController.Awake()` 中手动注入Inspector 不可见,也不能提前验证是否遗漏配置。若在另一个 Prefab 上挂 HurtBox 而忘记注入,会静默运行(弹反/霸体失效)。
---
## 6. 使用便利性DX
### 6.1 优点
#### ✅ 8 步伤害流水线注释完整
```csharp
// 1. 无敌帧检查
// 2. 弹反检查
// 3. 霸体检查
// 4. 护盾层拦截
// 5. 计算 FinalDamage
// 6. 调用 _owner.TakeDamage
// 7. 全局广播
// 8. 状态效果触发
```
任何开发者接手都能在 10 秒内理解伤害全流程,维护成本低。
#### ✅ InputReaderSO 统一输入接口
```csharp
public event Action AttackEvent;
public event Action DashEvent;
public event Action ParryEvent;
public Vector2 MoveInput { get; private set; } // Polling 接口
```
状态类既可订阅事件(`Input.AttackEvent += ...`),也可每帧轮询 `Input.MoveInput`灵活性高。Gameplay / UI 两套 ActionMap 在 SO 内部切换,调用方无感知。
#### ✅ PlayerStateBase 便捷属性减少模板代码
```csharp
protected InputReaderSO Input => _owner.Input;
protected AnimancerComponent Anim => _owner.Animancer;
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
```
每个状态类中直接写 `Anim.Play(AnimCfg.Dash)` 即可,无需每次手动访问 Owner 链式属性。
#### ✅ FormController 形态切换三路广播
```csharp
_onFormChanged?.Raise(index); // SO 事件 → UI/Save
OnFormChanged?.Invoke(); // C# 事件 → WeaponManager
_onSkillSetChanged?.Raise(); // SO 事件 → SkillHUD
```
一次切换操作自动通知所有关心形态的子系统,无需调用方手动串联。
---
### 6.2 问题
#### ⚠️ P1SaveManager 公开 `public SaveData Data` 属性
```csharp
public SaveData Data => _current;
```
外部代码(`ProgressLock``HPContainerPickup`)可直接读写 `SaveManager.Instance.Data.XXX`,绕过 SaveManager 的所有访问控制和校验逻辑。`_current` 可能为 null游戏未读档状态导致 NullReferenceException。
**建议:** 提供具名访问方法(`GetPlayerData()` / `GetWorldData()`),内部做 null guard移除 `Data` 属性。
#### ⚠️ P1`_dependenciesReady` 标志存在但每帧仍调用检查
```csharp
private void Update()
{
if (!HasRequiredStateDependencies()) return; // 每帧调用
...
}
private bool HasRequiredStateDependencies()
{
if (_dependenciesReady) return true; // 快速返回
...
}
```
`_dependenciesReady` 快速 return 已经优化了大部分开销,但 `Update` 中每帧仍有方法调用栈开销。若依赖项永远在 `Awake` 期间就绪,该方法根本不应在 `Update` 中调用。
**建议:**`Awake``ResolveDependencies()` 后直接断言依赖完整,`Update` 中不再调用检查。仅在开发期 `Awake` 末尾用 `Debug.Assert` 保护。
#### ⚠️ P2QuestManager 的 `OnEnemyDied` 订阅类型不匹配
```csharp
[SerializeField] private TransformEventChannelSO _onEnemyDied; // 携带 Transform
```
敌人死亡事件携带 `Transform`(敌人的 Transform但任务系统关心的是"哪种敌人死亡"(通常是 EnemyId / EnemyType。通过 Transform 反向查找 `EnemyBase` 组件获取 ID多了一层不必要的间接层。
**建议:** 死亡事件频道改为携带 `EnemyDeathData { string EnemyId; Transform Source; }`,或改用 `StringEventChannelSO` 直接携带 `EnemyId`
#### ⚠️ P2SwimState 存在但未在 PlayerController.InitializeStates() 中注册
```
Assets/Scripts/Player/States/SwimState.cs ← 文件存在
```
```csharp
// PlayerController.InitializeStates()
_states[typeof(IdleState)] = new IdleState(this);
// ... 16 个状态,无 SwimState
```
`SwimState` 无法通过 `GetState<SwimState>()` 获取,即使被引用也会返回 null使游泳状态完全无效。
#### ⚠️ P3PlatformBootstrap `Update()` 每帧调用 `GetOrDefault`
```csharp
private void Update()
{
ServiceLocator.GetOrDefault<IPlatformService>()?.RunCallbacks();
}
```
每帧一次 Dictionary 查找。应在 `Awake` 缓存引用:
```csharp
private IPlatformService _platform;
private void Awake() { ...; _platform = ServiceLocator.Get<IPlatformService>(); }
private void Update() => _platform?.RunCallbacks();
```
---
## 7. 问题优先级汇总表
| # | 优先级 | 模块 | 描述 | 影响 |
|---|--------|------|------|------|
| 1 | **P1** | 全局 | 单例 vs ServiceLocator 混用 | 可测试性、生命周期管理 |
| 2 | **P1** | EnemyBase | ForceState() 空实现,受击无视觉/计时反馈 | 战斗手感 |
| 3 | **P1** | PlayerMovement | localScale 翻转朝向 | 物理/视觉副作用 |
| 4 | **P1** | GameServiceRegistrar | 场景加载时 FindObjectsOfType | 加载性能 |
| 5 | **P1** | Editor | IValidatable 无 Runner | 数据质量保障缺失 |
| 6 | **P1** | PlayerController | Inspector 18 个 SerializeField | 配置错误概率高 |
| 7 | **P1** | SaveManager | `Data` 属性绕过访问控制 | 潜在 NullReferenceException |
| 8 | **P2** | UIManager | 商店 ID 参数被丢弃 | 多商店扩展时需重构 |
| 9 | **P2** | WorldStateRegistry | 世界状态扩展性差(每类单独字段) | 添加新实体类型成本高 |
| 10 | **P2** | SkillManager | 固定三技能槽硬编码 | 技能系统扩展瓶颈 |
| 11 | **P2** | EnemyBase | "Phase" 注释残留,死代码风险 | 维护性 |
| 12 | **P2** | PlayerController | SwimState 未注册 | 游泳功能完全失效 |
| 13 | **P2** | QuestManager | EnemyDied 事件携带 Transform 而非 EnemyId | 语义歧义 |
| 14 | **P2** | GlobalObjectPool | Coroutine + async Task 双版本重复逻辑 | 维护成本翻倍 |
| 15 | **P3** | PlatformBootstrap | Update 中 ServiceLocator.GetOrDefault 每帧查找 | 微性能 |
| 16 | **P3** | PostProcessManager | Component 类型引用无约束 | Inspector 配置错误风险 |
| 17 | **P3** | LocalizationManager | 异常静默吞掉 | 本地化问题难排查 |
| 18 | **P3** | HurtBox | 依赖注入通过代码而非 Inspector | 编辑器不可见,遗漏静默失效 |
---
## 8. 优化建议
### 8.1 高价值、低成本13天
**① 注册 SwimState**
```csharp
// PlayerController.InitializeStates() 添加一行
_states[typeof(SwimState)] = new SwimState(this);
```
**② EnemyBase.ForceState() 补充动画与计时**
```csharp
public void ForceState(EnemyStateType newState)
{
_currentState = newState;
switch (newState)
{
case EnemyStateType.Hurt:
if (_animancer != null && _animConfig?.Hurt != null)
_animancer.Play(_animConfig.Hurt);
// TODO: 启动硬直计时器
break;
case EnemyStateType.Dead:
Die();
break;
}
}
```
**③ 修复 LocalizationManager 异常日志**
```csharp
catch (Exception e)
{
Debug.LogWarning($"[Localization] Key '{entryKey}' in table '{tableName}' failed: {e.Message}");
return entryKey;
}
```
**④ PlatformBootstrap 缓存服务引用**
```csharp
private IPlatformService _platform;
private void Awake() { /* ... init ... */ _platform = ServiceLocator.Get<IPlatformService>(); }
private void Update() => _platform?.RunCallbacks();
```
**⑤ UIManager 保存并使用商店 ID**
```csharp
private void OpenShop(string shopId)
{
// TODO: 根据 shopId 选择对应 ShopController
OpenPanel(_shopRoot);
}
```
---
### 8.2 中等成本12周
**⑥ PlayerMovement 朝向改为 SpriteRenderer.flipX**
```csharp
// 替换 localScale 翻转
private SpriteRenderer _sprite;
private void Awake() => _sprite = GetComponentInChildren<SpriteRenderer>();
public void UpdateFacing()
{
float vx = _rb.velocity.x;
if (Mathf.Abs(vx) < 0.1f) return;
int dir = vx > 0f ? 1 : -1;
if (dir == _facingDirection) return;
_facingDirection = dir;
if (_sprite != null) _sprite.flipX = dir < 0;
}
```
**⑦ PlayerController 减少 Inspector 字段**
为同节点组件添加 `[RequireComponent]` 并在 Awake 自动获取:
```csharp
[RequireComponent(typeof(PlayerMovement))]
[RequireComponent(typeof(PlayerStats))]
[RequireComponent(typeof(AnimancerComponent))]
[RequireComponent(typeof(InputBuffer))]
// Awake 中删除手动赋值,直接 GetComponent
```
**⑧ 实现 SOValidationRunner**
```csharp
// Editor/SOValidationRunner.cs
[MenuItem("Tools/Validate All SOs")]
public static void ValidateAll()
{
var assets = AssetDatabase.FindAssets("t:ScriptableObject");
foreach (var guid in assets)
{
var so = AssetDatabase.LoadAssetAtPath<ScriptableObject>(
AssetDatabase.GUIDToAssetPath(guid));
if (so is IValidatable v)
foreach (var msg in v.Validate())
Debug.LogWarning($"[Validation] {so.name}: {msg}", so);
}
}
```
**⑨ WorldStateRegistry 泛化 API**
```csharp
public enum WorldObjectCategory { Collectible, Door, Destroyed, SavePoint, Flag }
private readonly Dictionary<WorldObjectCategory, HashSet<string>> _states = new();
public bool IsMarked(WorldObjectCategory cat, string id)
=> _states.TryGetValue(cat, out var set) && set.Contains(id);
public void Mark(WorldObjectCategory cat, string id)
{
if (!_states.ContainsKey(cat)) _states[cat] = new HashSet<string>();
_states[cat].Add(id);
}
```
---
### 8.3 长期架构改进(迭代计划)
**⑩ 统一全局管理器访问模式**
选择一种模式,全面推广:
- **方案 A推荐**:将 `SaveManager``QuestManager``MapManager``static Instance` 替换为 `ServiceLocator.Register<ISaveManager>(this)`,所有调用方通过 `ServiceLocator.Get<ISaveManager>()` 访问
- **方案 B**:保留单例,但所有单例实现共同接口,提供 `ServiceLocator.OverrideForTest<>()` 的测试路径
**⑪ 敌人状态机升级**
基于现有 Behavior Designer 行为树,完全放弃 `EnemyStateType` 枚举状态机,敌人所有状态逻辑都在 BD Task 中实现。`EnemyBase` 仅提供数据接口HP、位置、视野成为纯粹的"数据持有者 + Unity 生命周期桥接器"。
**⑫ 单元测试接入**
ServiceLocator 已支持 `OverrideForTest` / `Reset`,可以对以下关键系统编写 NUnit 测试:
- `SaveMigrator.Migrate()` 各版本迁移路径
- `StatusEffectManager` 堆叠 / 净化逻辑
- `QuestManager` 目标进度计算
- `HurtBox.ReceiveDamage()` 8 步流水线各分支
---
## 附:本次评审同步修复的 Bug
> 以下问题在撰写本文档时发现并已直接修复:
| 文件 | 问题 | 修复 |
|------|------|------|
| `EnemyBase.cs` | `Awake()` 中缺少 `_onPlayerSpawned.OnEventRaised += SetPlayerTransform` 订阅(上轮优化遗漏);`OnDestroy` 有取消订阅但从未订阅,造成无效的 `-=` 调用 | 已添加订阅,移除 Phase 1 `FindWithTag` 残留代码 |
---
*文档结束。如需针对任何具体问题深入讨论实施方案,可继续追问。*