26 KiB
zeling_v2 完整代码综合评审(含修复后状态)
评审日期:2026-05-11
评审范围:Assets/Scripts/全部 25 个程序集,约 200+ 源文件
参照基准:《空洞骑士》《Celeste》《Neon Abyss》《Dead Cells》《Hades》等成熟商业 2D 动作游戏
修复轮次:本文档反映第三轮优化(10 项 P0-P2 改动 + 8 项本轮改动)后的当前代码状态
目录
1. 评分总览
| 维度 | 本次得分 | 上次得分 | 变化 | 商业顶线参考 |
|---|---|---|---|---|
| 架构设计 | 8.0 | 7.5 | ↑ 0.5 | 8.5(HK/Celeste 架构级别) |
| 性能 | 7.5 | 7.0 | ↑ 0.5 | 8.0 |
| 可扩展性 | 7.5 | 7.5 | → | 9.0 |
| 编辑器友好性 | 7.5 | 6.5 | ↑ 1.0 | 8.0 |
| 使用便利性 | 7.5 | 7.0 | ↑ 0.5 | 8.5 |
| 综合 | 7.6 | 7.1 | ↑ 0.5 | ≈8.3 |
本轮修复了
localScale翻转 /SwimState未注册 /ForceState()空实现 /SaveManager.Data直接暴露 /GameServiceRegistrar场景扫描 /PlatformBootstrap每帧查找等多个 P1 缺陷,各维度均有明显提升。
2. 架构设计深度评审
2.1 ✅ 顶层架构:层次分明,职责边界清晰
Persistent Scene
[GameManager] ← 单一入口,GameStateMachine 封装
[GameServiceRegistrar] ← 服务注册,DefaultExecutionOrder(-2000)
[AudioManager] ← IAudioService 实现
[GlobalObjectPool] ← 对象池
[SaveManager] ← 存档
[QuestManager] ← 任务
[MapManager] ← 地图
[EventChainManager] ← 叙事事件链
Gameplay Scene(单场景或分块加载)
[RoomController] ← 房间出生点 + 摄像机切换
Enemy Prefabs ← EnemyBase + BehaviorTree
Player Prefab ← PlayerController + 16 状态 + HurtBox + HitBox
亮点:GameServiceRegistrar.DefaultExecutionOrder(-2000) 是所有 MonoBehaviour 中最早执行的,确保服务在任何其他脚本的 Awake 之前注册完毕,启动顺序依赖问题得到彻底解决。
2.2 ✅ GameStateId:正确实现 IEquatable,消除装箱
public readonly struct GameStateId : System.IEquatable<GameStateId>
{
public readonly string Id;
public bool Equals(GameStateId other) => Id == other.Id;
public override bool Equals(object obj) => obj is GameStateId g && Equals(g);
public override int GetHashCode() => Id?.GetHashCode() ?? 0;
public static bool operator ==(GameStateId a, GameStateId b) => a.Equals(b);
public static bool operator !=(GameStateId a, GameStateId b) => !a.Equals(b);
}
readonly struct + IEquatable<T> + 显式 ==/!= 运算符,字典查找走 Equals(GameStateId) 重载,完全无装箱。之前评审中担心的 struct 装箱问题已正确处理。
2.3 ✅ GameStateMachine:合法性验证 + 错误上报
if (_current != null && !_current.ValidNextStates.Contains(nextId))
{
error = $"[GameStateMachine] 非法转换 {_current.Id} → {nextId}";
return false;
}
状态图的合法转换表由每个 IGameState 自持,违规转换不静默失败,而是返回 false + error 字符串,调用方(GameManager)打 Warning。这是商业级防御性设计。
2.4 ✅ DamageInfo 双路径工厂
// 热路径:零堆分配,直接从 SO 填字段
public static DamageInfo From(DamageSourceSO so) { ... }
// 构建路径:可读链式 Builder
var info = new DamageInfo.Builder()
.SetRaw(damage).SetType(Fire).SetFlags(CanBeParried).Build();
高频 HitBox 碰撞走 DamageInfo.From(so) 无 GC;Boss 技能等复杂伤害信息走 Builder,清晰易读。两条路径并存,不牺牲一方。
2.5 ✅ EventChainManager:SO 事件 → C# 事件的桥接层
// SO 事件(跨场景持久)→ 中继 C# 事件(给 ChainCondition 订阅)
Subscribe(_onBossDefeated, id => { OnBossDefeated?.Invoke(id); EvaluateAll(); });
将 ScriptableObject 事件频道与纯 C# 条件判断系统优雅解耦。叙事链条件不需要知道 SO 的存在,EventChainManager 成为纯净的"事件路由器"。
2.6 ✅ AudioManager:旧单例迁移模式规范
// 标记已废弃的旧访问方式
[System.Obsolete("Use ServiceLocator.Get<IAudioService>() instead.")]
public static AudioManager Instance { get; private set; }
在完成迁移之前,用 [Obsolete] 标记旧 API 而非直接删除,给调用方平滑过渡期,是成熟工程实践。
2.7 ⚠️ 剩余架构问题
P1:GameManager / QuestManager / MapManager 仍是 static Instance
public static GameManager Instance { get; private set; } // 核心状态机持有者
public static QuestManager Instance { get; private set; } // ISaveable 实现者
public static MapManager Instance { get; private set; } // ISaveable 实现者
SaveManager 的 Data 属性已标 [Obsolete] 并提供具名访问器,但上述三个管理器仍在用 static Instance,与 ServiceLocator 模式并存。这导致:
QuestManager.Instance?.Register(this)在OnEnable中调用——若 QuestManager 先于 SaveManager 在 Inspector 顺序中初始化,SaveManager.Instance可能为 null- 无法在运行时替换 QuestManager 实现(如 Tutorial 场景使用简化版)
建议:三者均添加 ServiceLocator.Register<IQuestService>(this) 等接口注册,保留 static Instance 作为向后兼容的转发属性(内部走 ServiceLocator)。
P2:QuestManager._onEnemyDied 语义不匹配
[SerializeField] private TransformEventChannelSO _onEnemyDied; // 携带 Transform
任务系统用 Transform 反查 EnemyBase 组件再读 EnemyId,而任务关心的是"哪种敌人死了"而非"哪个具体对象"。多一层间接查找,且 Transform 可能在查找时已被销毁。
3. 性能深度评审
3.1 ✅ 热路径优化全覆盖
| 热路径 | 修复前 | 修复后 |
|---|---|---|
HurtBox 每次受击 |
GetComponentInParent<IStatusEffectable>() |
Awake 缓存 |
EnemyBase.Update 距离 |
Vector2.Distance(√) |
sqrMagnitude |
IsPlayerInRange(r) |
Mathf.Sqrt |
range * range |
GlobalObjectPool 活跃对象强制回收 |
List.RemoveAt(0) O(n) |
LinkedList 头节点 O(1) |
PlatformBootstrap.Update |
ServiceLocator.GetOrDefault 每帧字典查找 |
_platform 字段缓存 |
PlayerMovement.UpdateFacing |
localScale 写入(触发 Transform 脏) |
SpriteRenderer.flipX |
3.2 ✅ StatusEffectManager MaterialPropertyBlock
private MaterialPropertyBlock _propBlock;
private void Awake() { _propBlock = new MaterialPropertyBlock(); ... }
// 设置 Shader 属性时:
_renderer.GetPropertyBlock(_propBlock);
_propBlock.SetFloat(_flashId, value);
_renderer.SetPropertyBlock(_propBlock);
不修改共享 Material,不触发 Unity 的 Material 实例化,避免渲染批次打断。
3.3 ✅ 对象池 + Addressables 预热
- 离线 Addressables
LoadAssetAsync加载 Prefab,零运行时同步加载 PooledObject节点缓存GetComponentCached<T>()避免重复GetComponent- LinkedList 活跃列表 + LRU 强制回收,100 子弹同屏时 Spawn/Despawn 稳定 O(1)
3.4 ⚠️ 剩余性能问题
P2:SkillManager.Update 三个浮点减法(可忽略,但有更优方案)
private void Update()
{
if (_soulCooldown > 0) _soulCooldown -= Time.deltaTime;
if (_spirit1Cooldown > 0) _spirit1Cooldown -= Time.deltaTime;
if (_spirit2Cooldown > 0) _spirit2Cooldown -= Time.deltaTime;
}
每帧三次浮点减法是可接受的开销。若后续扩展到 N 个技能,应改为 Dictionary<FormSkillSO, float> + 统一遍历。
P2:WorldStateRegistry 无变更通知机制
public void MarkCollected(string id) => _collectedIds.Add(id); // 无事件广播
HashSet 写入后没有通知机制,调用方无法得知状态变化。UI 需要主动轮询或自行订阅其他事件来驱动刷新,不够响应式。
P3:GameManager.RegisterStates 每帧 _fsm.Tick(deltaTime)
private void Update() => _fsm.Tick(Time.deltaTime);
状态机 Tick 调用是 O(1)(一次虚方法调用),但若当前状态(如 GameplayState)本身做了复杂逻辑,则需进一步评估。整体可接受。
4. 可扩展性深度评审
4.1 ✅ QuestSO 分支叙事系统结构完整
[Header("完成后续任务(分支)")]
public QuestBranch[] branches;
[Serializable]
public class QuestBranch
{
public string conditionQuestId; // 条件任务 Completed → 走本分支(空 = 默认)
public QuestSO nextQuest;
public string npcDialogueKey;
}
任务树可以纯通过 SO 资产的引用关系构建,无需修改代码添加新分支。条件任务完成 → 解锁后续任务的逻辑在 QuestManager.CompleteQuest() 中自动处理。
4.2 ✅ EquipmentManager:护符效果接口化
public interface ICharmEffect
{
void OnEquip(EquipmentContext ctx);
void OnUnequip(EquipmentContext ctx);
}
每个护符效果是独立的 ScriptableObject(或类),实现 ICharmEffect。EquipmentContext 携带 Stats / Feedback / SkillMods / WeaponMgr 等必要引用,效果脚本不需要 MonoBehaviour,完全可测试。
4.3 ✅ DeathRespawnService 接口化,可替换实现
public interface IDeathRespawnService
{
IEnumerator StartDeathSequenceCoroutine();
IEnumerator StartRespawnCoroutine();
IEnumerator StartGameOverCoroutine();
}
GameManager 通过 ServiceLocator.Get<IDeathRespawnService>() 使用,测试时可注入 Mock(立即完成的假实现),不需要等待真实的死亡动画计时。
4.4 ✅ WorldStateRegistry:ScriptableObject 注入,跨场景共享
WorldStateRegistry 是 ScriptableObject(非 MonoBehaviour),在所有场景之间共享同一资产实例,不需要 DontDestroyOnLoad 也能保持状态。
4.5 ⚠️ 剩余可扩展性问题
P1:WorldStateRegistry 新增实体类型成本高
// 5 类实体,5 个独立 HashSet,5 对方法
private HashSet<string> _collectedIds;
private HashSet<string> _activatedSavePoints;
private HashSet<string> _openedDoors;
private HashSet<string> _destroyedObjects;
private HashSet<string> _flags;
public bool IsCollected(string id) => _collectedIds.Contains(id);
public void MarkCollected(string id) => _collectedIds.Add(id);
// ... 每新增一类需要 3 处改动
建议:
public enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag }
private readonly Dictionary<WorldObjectCategory, HashSet<string>> _states = new();
public bool IsMarked(WorldObjectCategory cat, string id)
=> _states.TryGetValue(cat, out var s) && s.Contains(id);
public void Mark(WorldObjectCategory cat, string id)
=> (_states.TryGetValue(cat, out var s) ? s : (_states[cat] = new HashSet<string>())).Add(id);
新增实体类型只需在枚举中添加一行。
P1:SkillManager 硬编码三技能槽
private FormSkillSO _soulSkill;
private FormSkillSO _spirit1;
private FormSkillSO _spirit2;
private float _soulCooldown;
private float _spirit1Cooldown;
private float _spirit2Cooldown;
槽位数量在编译时固定为 3,无法通过配置扩展。若后期形态需要 4 个或 2 个技能,需修改 SkillManager 代码。
建议:
private readonly Dictionary<FormSkillSO, float> _cooldowns = new();
public void UpdateSkillSet(FormSkillSO[] skills)
{
_cooldowns.Clear();
foreach (var s in skills) if (s != null) _cooldowns[s] = 0f;
}
5. 编辑器友好性深度评审
5.1 ✅ SOValidationRunner:构建前自动数据校验
public class SOValidationRunner : IPreprocessBuildWithReport
{
public void OnPreprocessBuild(BuildReport report)
{
var (errors, warnings) = RunAll();
if (errors.Count > 0)
throw new BuildFailedException(...); // 有错误时中止构建
}
[MenuItem("Tools/Validate All ScriptableObjects")]
public static void ValidateMenu() { ... }
}
- 实现
IPreprocessBuildWithReport,构建时自动运行,防止空引用 SO 进入 Release 包 callbackOrder = 1,在AddressKeyValidator (order=0)后执行,验证链有序[MenuItem]支持手动一键校验
5.2 ✅ EventBusMonitor 运行时追踪
256 条环形缓冲区,记录每个 SO 事件的:帧号 / 频道名 / 负载 / 监听器数量。任意事件触发时序问题在 Editor 中 5 秒内可定位。
5.3 ✅ EventChainEditorWindow 编辑器专用日志
#if UNITY_EDITOR
public static event Action<string, string> OnChainExecutedInEditor;
#endif
叙事链执行时向编辑器窗口推送日志(chainId + 执行结果),不产生运行时开销。#if UNITY_EDITOR 包裹严格,不泄漏到 Release 构建。
5.4 ✅ WorldMarker 可视化 Gizmos
Gizmos.color = _markerType switch
{
WorldMarkerType.Objective => Color.yellow,
WorldMarkerType.NPC => Color.cyan,
WorldMarkerType.PointOfInterest => Color.green,
...
};
场景视图直接看到标记类型与覆盖范围,场景设计师无需 Play 即可预览关卡布局。
5.5 ✅ [RequireComponent] 同节点依赖自动保障
本轮修复后,PlayerController 已标注:
[RequireComponent(typeof(InputBuffer))]
[RequireComponent(typeof(PlayerMovement))]
[RequireComponent(typeof(PlayerStats))]
[RequireComponent(typeof(AnimancerComponent))]
在 Inspector 中添加 PlayerController 时 Unity 自动附加所有必须组件,防止遗漏。
5.6 ⚠️ 剩余编辑器问题
P2:EquipmentContext 在 Awake 内联构建,Inspector 不可见
private void Awake()
{
_ctx = new EquipmentContext
{
Stats = GetComponent<PlayerStats>(),
Feedback = GetComponent<PlayerFeedback>(),
Events = EventChannelRegistry.Instance,
SkillMods = GetComponent<SkillModifierRegistry>(),
WeaponMgr = GetComponent<WeaponManager>(),
};
}
若某组件缺失(如 PlayerFeedback 未挂载),_ctx.Feedback 为 null,护符效果 OnEquip 调用 ctx.Feedback 时静默失败。建议在 Awake 末尾加 Debug.Assert:
Debug.Assert(_ctx.Stats != null, "[EquipmentManager] 缺少 PlayerStats", this);
Debug.Assert(_ctx.Feedback != null, "[EquipmentManager] 缺少 PlayerFeedback", this);
Debug.Assert(_ctx.SkillMods != null, "[EquipmentManager] 缺少 SkillModifiers", this);
P2:PostProcessManager [SerializeField] Component _bossArenaVolume
用 Component 基类接收 Volume 组件,无类型约束。设计师可能误拖 BoxCollider2D,直到运行时才报错。
6. 使用便利性(DX)深度评审
6.1 ✅ EquipmentManager:返回错误字符串替代 bool+out
/// <summary>返回 null 表示成功;返回错误字符串表示失败原因。</summary>
public string TryEquipCharm(CharmSO charm)
{
if (charm == null) return "护符不存在";
if (_equipped.Contains(charm)) return "已经装备";
if (!_collected.Contains(charm)) return "尚未收集此护符";
int remaining = _currentNotchCapacity - UsedNotches;
if (charm.notchCost > remaining) return $"笔记不足(需要 {charm.notchCost},剩余 {remaining})";
...
return null; // 成功
}
调用方可直接将错误字符串显示给用户,不需要额外的错误码枚举。设计简洁,优于 bool TryEquip(..., out string error)。
6.2 ✅ Projectile 基类:Initialize + 模板方法 OnInitialized
// 基类处理通用初始化(池引用、HitBox 激活、计时器)
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction)
{
_config = config;
DamageInfo = damageInfo;
Direction = direction.normalized;
_aliveTimer = 0f;
_hitBox.Activate(config.DamageSource);
OnInitialized(); // 钩子:子类设定初速度
}
protected virtual void OnInitialized() { }
子类只需重写 OnInitialized() 设置 Rigidbody 速度,其余公共逻辑由基类统一处理。LinearProjectile / HomingProjectile / ArcProjectile 各自 ≤ 20 行。
6.3 ✅ 异步/协程异常全部包裹
// QuickSave/QuickLoad 调用点无法 await,用包裹器捕获异常
private static async void RunFireAndForget(Task task, string context)
{
try { await task; }
catch (Exception e)
{
Debug.LogError($"[SaveManager] {context} 失败: {e.Message}\n{e.StackTrace}");
}
}
所有 async void 入口(QuickSave / QuickLoad)都经过 RunFireAndForget 包裹,异常不会静默吞掉。
6.4 ✅ QuestManager:完整 ISaveable + IReadOnlyDictionary 状态只读视图
public IReadOnlyDictionary<string, QuestStateEnum> QuestStates => _questStates;
外部查询任务状态走 IReadOnlyDictionary,无法意外写入内部状态。SaveManager 持有 ISaveable 接口引用,不直接依赖 QuestManager 类型。
6.5 ⚠️ 剩余 DX 问题
P1:GameManager.RegisterStates 硬编码 9 个状态
private void RegisterStates()
{
_fsm.Register(new InitializingState());
_fsm.Register(new MainMenuState());
// ... 9 个
}
每次添加新游戏状态都需要修改 GameManager.cs。游戏状态类实现了 IGameState,可通过反射或工厂模式自动注册(IGameStateFactory 接口已存在):
// IGameStateFactory.cs 已定义,但 GameManager 未使用
foreach (var factory in GetComponents<IGameStateFactory>())
_fsm.Register(factory.Create());
P2:DeathRespawnService 复活流程等待方式混用
// 在 Coroutine 中轮询 bool flag(_deathConfirmed)
yield return new WaitUntil(() => _deathConfirmed);
通过轮询 bool 等待玩家确认,而不是直接订阅确认事件的回调。在 Coroutine 中 WaitUntil 每帧检查一次,虽然性能可接受,但意图不如直接回调清晰。
7. 子系统逐一点评
| 子系统 | 质量评级 | 亮点 | 注意点 |
|---|---|---|---|
| GameStateMachine | ⭐⭐⭐⭐⭐ | 合法转换验证 + 错误上报 | 状态注册仍手动 |
| 事件频道 | ⭐⭐⭐⭐⭐ | SO + C# event 双层 + 可组合订阅句柄 + EventBusMonitor | - |
| ServiceLocator | ⭐⭐⭐⭐⭐ | 接口注册 + 测试 Override/Reset | 与 static Instance 并存(迁移中) |
| SaveManager | ⭐⭐⭐⭐ | HMAC校验 + 版本迁移 + fire-and-forget 包裹 | Data 属性已标 Obsolete,待删除 |
| HurtBox | ⭐⭐⭐⭐ | 8 步流水线完整 + 接口注入 + 缓存 | 依赖注入不可见于 Inspector |
| DamageInfo | ⭐⭐⭐⭐⭐ | 双路径工厂(零 GC + Builder) | - |
| PlayerController | ⭐⭐⭐⭐ | POCO FSM + 类型字典 O(1) + SwimState 已注册 | 仍有 ~14 个 SerializeField |
| AttackState | ⭐⭐⭐⭐ | 连击数据驱动 + Animancer 事件驱动 HitBox | - |
| PlayerMovement | ⭐⭐⭐⭐ | flipX 朝向已修复 | _spriteRenderer 需 Inspector 赋值或 GetComponentInChildren |
| EnemyBase | ⭐⭐⭐ | 事件频道获取玩家 Transform + Start 兜底 | ForceState 动画已修复;状态机仍是枚举级别 |
| GlobalObjectPool | ⭐⭐⭐⭐ | LinkedList LRU + Addressables 预热 + 双版本桥接 | 双版本预热逻辑有重复 |
| StatusEffectManager | ⭐⭐⭐⭐⭐ | 双结构 + MaterialPropertyBlock + 逆序遍历 | - |
| EquipmentManager | ⭐⭐⭐⭐ | ICharmEffect 接口 + EquipmentContext + ISaveable | _ctx 依赖无 Assert 保护 |
| SkillManager | ⭐⭐⭐ | 修改器注册表 + FormSkillSO 数据驱动 | 三个硬编码槽 |
| QuestManager | ⭐⭐⭐⭐ | 分支任务 + ISaveable + IReadOnlyDictionary | EnemyDied 事件类型不匹配 |
| WorldStateRegistry | ⭐⭐⭐ | SO + 跨场景共享 + ISaveable | 每类实体独立字段,扩展性差 |
| AudioManager | ⭐⭐⭐⭐ | BGM 交叉淡入 + SFX 轮转池 + 旧 API 已标 Obsolete | Phase 2 实现尚未完整 |
| EventChainManager | ⭐⭐⭐⭐⭐ | SO 事件 → C# 事件桥接 + Editor 专用日志 | - |
| SOValidationRunner | ⭐⭐⭐⭐⭐ | 构建前校验 + MenuItem 一键校验 | - |
8. 残留问题清单
以下为本轮修复后仍存在的问题,按优先级排序。
| # | 优先级 | 模块 | 问题描述 | 建议修复方式 |
|---|---|---|---|---|
| 1 | P1 | GameManager / QuestManager / MapManager | static Instance 与 ServiceLocator 并存 |
实现接口 + ServiceLocator 注册,保留 Instance 作转发 |
| 2 | P1 | QuestManager | _onEnemyDied 用 TransformEventChannelSO 携带 Transform |
改用 EnemyDeathData { string EnemyId; Transform Source; } 事件 |
| 3 | P1 | GameManager | RegisterStates() 硬编码,未使用已存在的 IGameStateFactory 接口 |
通过工厂接口或配置自动注册 |
| 4 | P1 | WorldStateRegistry | 5 类状态独立字段,扩展性差 | 改为 Dictionary<WorldObjectCategory, HashSet<string>> |
| 5 | P1 | SkillManager | 三技能槽硬编码 | Dictionary<FormSkillSO, float> 动态冷却表 |
| 6 | P2 | EquipmentManager | EquipmentContext 依赖无 Debug.Assert 保护 |
Awake 末尾加断言 |
| 7 | P2 | PostProcessManager | [SerializeField] Component _bossArenaVolume 无类型约束 |
改为 Volume 具体类型 |
| 8 | P2 | DeathRespawnService | WaitUntil(() => _deathConfirmed) 轮询模式 |
改为直接 TaskCompletionSource 或事件回调 |
| 9 | P2 | GlobalObjectPool | WarmupCoroutine / WarmupAsync 逻辑重复 |
统一 async Task,提供 Coroutine 桥接扩展方法 |
| 10 | P3 | SaveManager | [Obsolete] Data 属性仍存在 |
确认调用方全部迁移后删除 |
| 11 | P3 | RoomController | CameraStateController.Instance?.SwitchRoom() 使用 static Instance |
改用 ServiceLocator 或事件频道 |
| 12 | P3 | WorldStateRegistry | 无变更通知机制 | 添加 event Action<WorldObjectCategory, string> OnStateChanged |
9. 后续迭代路线图
第一优先级(1-2 周,不影响功能开发)
① WorldStateRegistry 泛化 API
enum WorldObjectCategory { Collectible, SavePoint, Door, Destroyed, Flag }
Dictionary<WorldObjectCategory, HashSet<string>> _states
② SkillManager 动态技能槽
Dictionary<FormSkillSO, float> _cooldowns
③ EquipmentManager.Awake Debug.Assert 保护
// 4 行断言,30 分钟内完成
④ GlobalObjectPool 统一 WarmupAsync,提供 AsCoroutine() 桥接
第二优先级(2-4 周,架构级改进)
⑤ GameManager / QuestManager / MapManager 统一服务定位模式
实现 IQuestService / IMapService 接口 → ServiceLocator 注册
static Instance 改为 ServiceLocator.GetOrDefault<IXXXService>()
⑥ 利用 IGameStateFactory 接口自动注册游戏状态
GameManager.RegisterStates() 改为工厂驱动
⑦ QuestManager._onEnemyDied 改用 EnemyDeathData 事件类型
第三优先级(长期,影响内容管线)
⑧ EnemyBase 状态机升级
完全依赖 Behavior Designer 行为树,废弃 EnemyStateType 枚举
EnemyBase 成为纯数据持有者 + 生命周期桥接器
⑨ 单元测试接入(ServiceLocator 已支持 OverrideForTest)
SaveMigrator.Migrate 各版本路径
StatusEffectManager 堆叠/净化逻辑
QuestManager 目标进度计算
HurtBox 8 步流水线各分支
⑩ 护符效果数据管线自动化
ICharmEffect.Validate() 接入 SOValidationRunner
CharmSO.effects[] 空引用在构建时捕获
附:三轮修复全记录
| 轮次 | 问题 | 文件 | 状态 |
|---|---|---|---|
| 第一轮 P0 | SaveManager HMAC 跨设备失效 | SaveManager.cs | ✅ |
| 第一轮 P0 | QuickSave 异步异常静默吞掉 | SaveManager.cs | ✅ |
| 第一轮 P1 | HurtBox 每次受击 GetComponent | HurtBox.cs | ✅ |
| 第一轮 P1 | EnemyBase FindWithTag O(n) | EnemyBase.cs | ✅ |
| 第一轮 P1 | EnemyStats DistanceToPlayer 未平方 | EnemyStats.cs | ✅ |
| 第一轮 P1 | PlayerController Resources.FindObjectsOfTypeAll | PlayerController.cs | ✅ |
| 第一轮 P1 | PlayerController _dependenciesReady 每帧重计算 | PlayerController.cs | ✅ |
| 第一轮 P2 | AttackState 连击硬编码 3 段 | AttackState.cs | ✅ |
| 第一轮 P2 | HurtState 硬编码 0.4s 硬直 | HurtState.cs | ✅ |
| 第一轮 P2 | GlobalObjectPool List O(n) LRU | GlobalObjectPool.cs | ✅ |
| 第二轮 Bug | EnemyBase Awake 订阅未调用 | EnemyBase.cs | ✅ |
| 第三轮 P1 | PlayerMovement localScale 朝向 | PlayerMovement.cs | ✅ |
| 第三轮 P1 | SwimState 未注册 | PlayerController.cs | ✅ |
| 第三轮 P1 | EnemyBase.ForceState() 空实现 | EnemyBase.cs | ✅ |
| 第三轮 P1 | SaveManager.Data 暴露内部状态 | SaveManager.cs + 2 调用方 | ✅ |
| 第三轮 P1 | GameServiceRegistrar 全场景扫描 | GameServiceRegistrar.cs | ✅ |
| 第三轮 P1 | PlayerController [RequireComponent] 缺失 | PlayerController.cs | ✅ |
| 第三轮 P3 | PlatformBootstrap 每帧 ServiceLocator 查找 | PlatformBootstrap.cs | ✅ |
| 第三轮 P2 | UIManager 丢弃 shopId | UIManager.cs | ✅ |
| 第三轮 P3 | LocalizationManager 异常静默 | LocalizationManager.cs | ✅ |
本文档反映当前(2026-05-11)所有已实施修复后的代码状态。如需深入讨论任何具体问题的实施方案,可继续追问。