多轮审查和修复

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,503 @@
# BaseGames Framework — 第七轮完整评审
**日期**: 2026 年 5 月
**评审轮次**: v7v6 九项修复落地后全量复审 + 深度扩展模块阅读)
**版本基线**: Unity 2022.3 LTS / C# 9 / 28+ .asmdef 程序集
**上轮评分**: v6 加权 8.93 / 10
---
## 一、评审说明
### 1.1 v6 问题修复确认
本轮评审首先验证 v6 全部 9 项修复均已成功落地,无编译错误:
| ID | 文件 | 修复内容 | 验证结果 |
|----|------|---------|---------|
| U-1 | AchievementManager.cs | `Awake` 补充 `GetOrDefault != null → Destroy` 防护 | ✅ |
| A-2 | DeathRespawnService.cs | `Get<ISceneService>``GetOrDefault<ISceneService>` | ✅ |
| A-1 | GameManager.cs | 增加 `_prePauseState`,暂停/恢复记忆正确状态 | ✅ |
| P-1 | PlayerMovement.cs | `OverlapBoxNonAlloc` + `_groundBuffer[4]` | ✅ |
| P-2 | ShopController.cs | `_isDirty` 缓存 + 三处写后置脏 | ✅ |
| P-3 | MapManager.cs | `OnSave` 改为 `Clear() + AddRange()` | ✅ |
| P-4 | SaveManager.cs | `_saveables` 改为 `HashSet<ISaveable>` | ✅ |
| U-2 | SpeedrunTimer.cs | 修正 `unscaledDeltaTime` 暂停行为误导注释 | ✅ |
| DC-1 | SceneLoader.cs | 分析为正确行为,不修改 | ✅ |
### 1.2 本轮新增阅读范围
在 v6 覆盖基础上,本轮深度新阅读以下模块:
| 模块 | 主要文件 |
|------|---------|
| Player.States | PlayerController, PlayerStateBase, IdleState, RunState, JumpState, FallState, AttackState, DashState, AerialDashState, WallSlideState, WallJumpState, AirAttackState, DownAttackState, UpAttackState, HurtState, DeadState, SpringState, ParryState, SwimState |
| Input | InputReaderSO, InputBuffer |
| Enemies | EnemyBase, EnemyStats, EnemyMovement, EnemyQuotaManager, BossBase |
| Enemies.AI | BatchLOSSystem |
| Combat | ProjectileManager, HitStopManager, StatusEffectManager, ShieldComponent |
| World | RoomController, RoomTransition, BossProgressTracker |
| Progression | BossProgressTracker, ProgressLock, AchievementCondition |
---
## 二、各维度评审
### 2.1 架构设计 Architecture Design — 9.2 / 10
#### 亮点
**PlayerController — 数据驱动的组合式状态机**
`PlayerController` 使用 `Dictionary<Type, PlayerStateBase>` 而非 `switch/if-else` 链,所有状态实例在 `InitializeStates()` 统一创建,`GetState<T>()` 按类型 O(1) 取用;状态对象不继承 `MonoBehaviour`,生命周期完全由控制器驱动。这一设计使新增状态只需创建类 + 在字典注册,其余代码无需改动。
```csharp
// PlayerController — 扩展只需新增一行
_states[typeof(SwimState)] = new SwimState(this);
```
**PlayerStateBase — Editor 防护层**
`ValidTransitions` 白名单在 `#if UNITY_EDITOR` 内声明,生产构建无开销;`PlayerController.TransitionTo` 在 Editor 中检查并 `LogWarning`,调试阶段可精准发现非预期转换路径,不中断游戏但留下可溯源记录。
**依赖注入分层清晰**
- 同节点组件:`RequireComponent` 保证存在,`Awake``GetComponent` 自动获取
- 跨节点引用:`[SerializeField]` Inspector 绑定
- 跨系统访问:`ServiceLocator.GetOrDefault<IXxx>()` 接口
三层注入目标与获取方式严格对应,无任何 `FindObjectOfType` 调用(`DebugCheatSystem` 除外,受 `#if` 保护)。
**HurtBox 依赖运行时注入**
`PlayerController.Awake()` 通过 `_hurtBox.SetShieldable(_shield)` / `SetParrySystem(_parrySystem)` / `SetPoiseSource(this)` 在运行时将护盾、弹反、霸体系统注入到 HurtBox解耦方向正确HurtBox 不持有对 PlayerController 的引用,仅持有接口。
**BossBase — 最小化扩展基类**
`BossBase` 只增加阶段切换 (`EnterPhase`) 和 `_onBossFightEnded` 广播,`Die()` 重写仅追加事件广播后调用 `base.Die()`——Boss 特有行为与通用敌人行为边界清晰,继承层次扁平(`EnemyBase → BossBase → 具体Boss`),未出现"God Boss"类。
**EnemyQuotaManager — BehaviorTree LOD 管理**
双结构 `HashSet<EnemyBase> + List<EnemyBase>` 实现 O(1) 重复检测 + 顺序排序;每 10 帧按距离平方排序后仅激活最近的 `_maxActiveBehaviorTrees` 个行为树,对远处敌人降低 AI 计算频率,与 `BatchLOSSystem` 的分帧射线检测配合形成完整的敌人 AI LOD 体系,设计目标明确。
#### 不足
**A-1`WallJumpState.OnStateUpdate` 直接访问 `Move.Rb.velocity.y`**
```csharp
// WallJumpState.cs — 绕过 PlayerMovement 运动抽象
if (Move.Rb.velocity.y <= 0f)
Owner.TransitionTo(Owner.GetState<FallState>());
```
`PlayerMovement` 公开 `Rb` 属性是为了极少数需要 Rigidbody2D 直接操作的场景,但在状态转换条件中直接读取 `velocity.y` 与框架约定(状态只调用 `Move.IsXxx` 属性)矛盾。若后续将 Rigidbody2D 替换或引入预测物理,此处会产生意外。建议 `PlayerMovement` 新增 `IsRising / IsFalling` 只读属性,状态层查询语义属性而非物理原始值。
**A-2`TryTransitionState` 与 `TransitionTo` 完全等价**
```csharp
// PlayerController — 两个方法完全等价,命名暗示不同语义
public void TransitionTo(PlayerStateBase newState) { ... }
public void TryTransitionState(PlayerStateBase newState) => TransitionTo(newState);
```
`TryTransitionState` 名称暗示"可能失败/有条件",但当前实现是直接转发,调用方无法区分语义。建议:若需区分"无条件切换"与"带守卫切换"`TryTransitionState` 应返回 `bool` 并实际执行条件检查(如是否在 `ValidTransitions` 内);否则应删除别名,统一用 `TransitionTo`
---
### 2.2 性能 Performance — 8.8 / 10
#### 亮点
**BatchLOSSystem — 分帧射线 + O(1) Swap-and-pop 注销**
核心设计:轮询索引 `_currentOffset` 每帧只处理 `_maxRequestersPerFrame`(默认 8个请求者的射线检测均匀分摊到多帧避免大量敌人同帧全量射线的帧峰注销使用 `_indexMap` + swap-and-popO(1) 完成,无 O(n) 数组搬移。
```csharp
// O(1) 注销核心
int idx = _indexMap[requester];
int last = _requesters.Count - 1;
if (idx != last)
{
var moved = _requesters[last];
_requesters[idx] = moved;
_indexMap[moved] = idx;
}
_requesters.RemoveAt(last);
_indexMap.Remove(requester);
```
**EnemyQuotaManager — 按距离 LOD 激活 BehaviorTree**
10 帧间隔的 Rebalance + 距离平方排序(`sqrMagnitude`,避免开方),仅激活最近 N 个行为树——在有大量远程敌人的大型关卡中,可将 AI 计算量恒定在 O(N×_maxActive) 而非 O(N²)。
**DashState — FixedUpdate 保速 + 协程替代**
`FixedUpdate` 中持续调用 `Move.Dash()` 维持冲刺速度,防止地面摩擦力在 `Update` 间隙衰减。相比协程冻帧方案,无协程 GC`yield return` 开销。
**EnemyStats — HP 比例恢复难度调整**
`HandleDifficultyChanged` 保留 HP 比例(`float hpRatio = CurrentHP / MaxHP`),而非直接重置为新 MaxHP避免玩家在打 Boss 时切换难度导致 Boss 满血复活——数值设计与技术实现紧密配合。
**StatusEffectManager — 逆序遍历零移位**
`Update` 中使用 `for (int i = _activeList.Count - 1; i >= 0; i--)` 逆序遍历并 `RemoveAt(i)`,避免正序移除时索引位移导致跳过元素的经典 bug同时无额外分配。
#### 不足
**P-1`HitStopManager.FreezeDuration` 未实现"取最大值"语义**
注释(中英两处)均声明"若已有冻帧进行中,取两者中持续时间较长的(避免短请求截断较长的冻帧)",但实际代码:
```csharp
// 注释称"取最大",实为"直接覆盖"
if (_activeRoutine != null)
StopCoroutine(_activeRoutine); // 停止旧冻帧(不管剩余时长)
_activeRoutine = StartCoroutine(FreezeRoutine(unscaledSeconds)); // 以新时长重启
```
若 Boss 死亡触发 10 帧冻帧2 帧后普通命中再触发 2 帧冻帧,结果是原来 8 帧被截断为 2 帧,打击感大幅削弱。建议增加剩余时间追踪:
```csharp
private float _freezeEndTime;
public void FreezeDuration(float unscaledSeconds)
{
if (unscaledSeconds <= 0f) return;
float newEndTime = Time.unscaledTime + unscaledSeconds;
if (_activeRoutine != null && newEndTime <= _freezeEndTime) return; // 新请求更短,不截断
_freezeEndTime = newEndTime;
if (_activeRoutine != null) StopCoroutine(_activeRoutine);
_activeRoutine = StartCoroutine(FreezeRoutine(unscaledSeconds));
}
```
**P-2`EnemyQuotaManager.Unregister` 仍用 O(n) `List.Remove`**
`Register` 时同时维护 `_registeredSet`HashSet`_registered`List`Unregister` 只用 `_registeredSet.Remove` 做 O(1) 检测,`_registered.Remove(enemy)` 仍是 O(n) 线性扫描。与 `BatchLOSSystem` 的 swap-and-pop 模式不一致,在频繁进出房间且敌人数量多时有轻微开销。建议对齐 `BatchLOSSystem``_indexMap + swap-and-pop`
---
### 2.3 可扩展性 Scalability — 9.1 / 10
#### 亮点
**EnemyBase._stateObjs — 子类可覆盖的状态字典**
```csharp
// EnemyBase.Awake — 默认注册
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
// 子类可在 base.Awake() 后替换:
_stateObjs[EnemyStateType.Hurt] = new EliteHurtState(); // 精英怪专用受击逻辑
```
这一开放/封闭设计允许具体敌人精确替换单个状态行为而无需重写整个状态机,扩展颗粒度极细。
**AttackState — 连击段数完全数据驱动**
```csharp
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
```
连击段数从配置 SO 的动画数组长度动态读取,设计者只需在 `PlayerAnimationConfigSO` 中增减动画 clip无需修改 `AttackState` 代码即可实现 2 段、3 段、4 段连击切换。
**HitBox 时机配置 — `GroundAttackTimings[]` SO 驱动**
攻击判定框的激活时间点 (`HitBoxEnter` / `HitBoxExit`) 配置于 `PlayerAnimationConfigSO`,状态代码通过索引读取,完全解耦;更改判定时机不需要修改代码,只改 SO 数据——利于动作设计师独立迭代。
**ProjectileManager — 瘦服务层 + 追踪目标代理**
`ProjectileManager` 只做一件事:缓存玩家 Transform 供追踪弹使用,并提供 `LaunchHoming()` 封装。这种瘦服务设计避免服务层过度膨胀;各类具体弹幕(`LinearProjectile` / `ArcProjectile` / `HomingProjectile` / `ParryableProjectile`)独立实现,通过组合使用 `ProjectileConfigSO` 配置,新增弹幕类型零侵入。
**BossBase.EnterPhase — 扩展点已留**
```csharp
public virtual void EnterPhase(int phase)
{
_currentPhase = phase;
_onBossPhaseChanged?.Raise(new BossPhaseEvent { BossId = _bossId, Phase = phase });
}
```
子类 override 只需增加阶段特有逻辑事件广播在基类处理UI / 音乐 / 摄像机等系统通过频道响应Boss 代码与表现层零耦合。
#### 不足
**S-1`EnemyBase.SetAggroTickRate` 完全空 Stub**
```csharp
public void SetAggroTickRate(bool isAggro)
{
#if GRAPH_DESIGNER
_ = isAggro; // ← 实际功能未实现,注释说明等 Opsive 包升级
#endif
}
```
此方法存在于框架公共 API 中但不执行任何操作。Stub 有内联注释说明原因Opsive 包当前版本未暴露 frameInterval属于透明技术债`BD_SetAlert` 任务调用此方法时得不到任何效果,调试时容易困惑。建议增加 `#if UNITY_EDITOR` 内的 `Debug.LogWarning("SetAggroTickRate: 等待 Opsive 包升级,当前无效")` 主动提示。
**S-2`RoomTransition.HasItem` 语义错位**
```csharp
private bool HasItem(string itemId)
{
...
return _worldState.IsCollected(itemId); // ← IsCollected = "世界收藏品已拾取"
}
```
`WorldStateRegistry.IsCollected` 的语义是"世界对象Collectible已被拾取",与"玩家背包中有某道具"是不同概念。钥匙物品通常存放于背包/装备槽,用 `IsCollected` 检查相当于把钥匙道具当作一次性世界收藏品处理——若钥匙是可消耗物品或需要多次使用,此逻辑将产生语义错误。建议通过 `IInventoryService.HasItem(itemId)` 或专用接口检查,保持 `WorldStateRegistry` 的语义纯净。
---
### 2.4 编辑器友好 Editor-Friendliness — 9.3 / 10
#### 亮点(延续 v6
**Editor 状态转换守卫**
`PlayerStateBase.ValidTransitions` + `PlayerController` 中的 `_debugValidateTransitions` 开关,为玩家状态机提供运行时转换路径白名单验证,非预期转换在 Console 留下可溯源的 Warning不中断游戏——既不影响 QA 流程,又精准捕获状态机设计错误。
**BossSkillSequenceWindow / EventBusMonitorWindow / AddressReferenceGraphWindow**
(详见 v6本轮无新变化仍为显著亮点
**EnemyBase `Debug.Assert` 关键依赖**
```csharp
Debug.Assert(_statsSO != null, "[EnemyBase] _statsSO 未赋值...", this);
Debug.Assert(_stats != null, "[EnemyBase] _stats 未绑定...", this);
```
`Debug.Assert` 在 Release 构建中被完全剥离(`UNITY_ASSERTIONS`Editor / Development Build 中给出精确的 context 对象(`this`),点击 Console 可直接定位问题预制体,比 `NullReferenceException` 调用栈更直接。
**ShieldConfigSO — 护盾参数集中配置**
护盾的 HP / 吸收比例 / 充能延迟 / 充能速率 / 破碎惩罚时长 / 弹反恢复比例 全部配置于 `ShieldConfigSO``ShieldComponent.Update` 只读 SO 属性,美术 / 策划可在不动代码的情况下调整整套护盾手感。
#### 不足(延续 v6
**E-1、E-2**(见 v6BossSkillSequenceWindow 无 PNG 导出DebugCheatSystem 无 Tab 补全)
---
### 2.5 使用便利性 Developer UX — 9.0 / 10
#### 亮点
**ParryState — Animancer 帧事件兜底**
```csharp
var state = Anim?.Play(AnimCfg.ParryStart);
if (state != null)
{
state.Events(this).OnEnd = OnParryEnd;
return;
}
OnParryEnd(); // 无动画时立即退出,不挂死
```
状态永远不会因为动画 clip 未配置而"挂死"——无动画时安全降级到立即结束。同样模式在多个状态中一致使用,防御性设计扎实。
**InputBuffer — 消耗式设计**
```csharp
public bool ConsumeJump() { if (_jumpBuffer <= 0f) return false; _jumpBuffer = 0f; return true; }
```
读取即消耗Consume 模式),状态机中只需 `if (Buffer.ConsumeJump())` 一行无需手动清空API 表面积极小,难以误用。
**`DashState.CanDash` — 冷却状态外放**
`PlayerController` 不持有冲刺冷却状态,所有冷却逻辑封装在 `DashState` 内;`PlayerController.Update` 调用 `GetState<DashState>()?.TickCooldown(dt)`,其余代码通过 `GetState<DashState>()?.CanDash` 查询——单一数据源,不存在双重维护。
**`EnemyStats.SqrDistanceToPlayer` — 注释约定**
```csharp
/// 使用方请与 range*range 比较,而非直接与 range 比较。
public float SqrDistanceToPlayer { get; set; }
```
字段名 + XML 注释明确标注"平方距离"约定,`IsPlayerInRange()` 内部也用 `range * range` 比较调用方不会误用开根号版本API 约定自文档化。
#### 不足
**U-1`TryTransitionState` 命名暗示有条件但行为无条件**
(详见架构 A-2对 API 使用者有认知摩擦)
**U-2`AttackState` 在 `OnStateExit` 和 `OnClipEnd` 双重解绑**
```csharp
public override void OnStateExit()
{
Input.AttackEvent -= OnAttackInput; // 解绑 1
Owner.Combat?.DisableAllWeaponHitBoxes();
}
private void OnClipEnd()
{
Input.AttackEvent -= OnAttackInput; // 解绑 2正常流程
Owner.TryTransitionState(Owner.GetState<IdleState>());
}
```
若外部(如 `HurtState` 中断)在 `OnClipEnd` 前触发 `OnStateExit``OnAttackInput` 已经解绑,之后 `OnClipEnd` 再次解绑是无害的C# event 解绑不存在的委托不抛异常),但逻辑上暗示存在两种可能路径,可读性略差。建议统一在 `OnStateExit` 解绑,`OnClipEnd` 只负责请求状态转换,不再重复解绑。
---
### 2.6 框架纯净度 Framework Purity — 9.3 / 10
#### 亮点(延续 v6 + 本轮验证)
**PlayerController 完全无 FindObjectOfType**
全部 17 个玩家状态均通过 `_owner.GetState<T>()``_owner.Movement / .Input / .Combat` 等属性访问依赖,无任何全局对象搜索。`_onPlayerSpawned.Raise(transform)``Start()` 中以事件形式把 Transform 广播出去EnemyBase / ProjectileManager 等系统通过订阅接收——彻底消除 N 个敌人独立 `FindWithTag` 的 O(N) 全场景扫描。
**ParrySystem 解耦边界**
`PlayerController` 订阅 `ParrySystem` 的两个 C# 事件(`OnParryActivated` / `OnParryConsumed`)并在 `OnDestroy` 对称解绑,`ParrySystem` 不持有任何 `PlayerController` 引用——战斗子系统与主控制器单向依赖,边界清晰。
**`#if GRAPH_DESIGNER` 行为树守卫**
`EnemyBase.BehaviorTree` 属性、`EnemyQuotaManager` 的 BT 激活/停用逻辑、`SetAggroTickRate` 均在 `#if GRAPH_DESIGNER` 内,剥离彻底,无第三方 SDK 污染生产构建。
#### 不足(延续 v6 PU-1
`DebugCheatSystem.CmdHeal``FindFirstObjectByType<PlayerController>()` 仍存在,属 v6 遗留低优先级问题。
---
### 2.7 数据逻辑一致性 Data Consistency — 9.0 / 10
#### 亮点
**EnemyStats — 难度切换保 HP 比例**
```csharp
float hpRatio = MaxHP > 0 ? (float)CurrentHP / MaxHP : 1f;
ApplyHPScaler();
CurrentHP = Mathf.Clamp(Mathf.RoundToInt(MaxHP * hpRatio), 1, MaxHP);
```
难度实时变更时 HP 以比例而非绝对值重算,保证"玩家打了半血后切换难度Boss 仍是半血状态"——游戏感一致性优先于实现简单性的正确选择。
**BossProgressTracker — 事件中继零耦合**
Boss 击败 → `_onBossDefeated(bossId)``BossProgressTracker` 过滤 ID → `_onBossDefeatedForSave.Raise(bossId)``SaveManager`。两个频道隔离"战斗感知"与"持久化写入"职责,战斗模块不知道存档格式,存档模块不知道战斗流程——双向不知情设计。
**ShieldComponent.AbsorbDamage — 比例吸收设计**
```csharp
int toAbsorb = Mathf.FloorToInt(amount * AbsorptionRatio);
toAbsorb = Mathf.Min(toAbsorb, CurrentShieldHP);
int passthrough = amount - toAbsorb;
```
护盾只按配置比例吸收(而非全部吸收),穿透量继续走 `TakeDamage` 流程;护盾破碎时惩罚计时器激活,破碎期间无法再次吸收——护盾状态转换有完整的状态机语义,数据流路径清晰且无歧义。
#### 不足
**DC-1`HitStopManager` 注释与行为不一致**
此问题跨越"性能"与"数据一致性"两个维度:注释文档(两处)声明"取最大值"语义,但代码实现为"直接覆盖"。这不仅是逻辑 BugP-1也是文档一致性问题——任何基于注释实现调用方的开发者都会被误导认为小冻帧请求不会截断大冻帧实则会。修复见 P-1 建议方案。
**DC-2`RoomTransition._worldState` 注入方式存隐患**
`_worldState: WorldStateRegistry` 通过 `[SerializeField]` 直接注入,而非通过 `ServiceLocator.GetOrDefault<IWorldStateRegistry>()`。若未来 WorldStateRegistry 跨场景唯一化(改为 Persistent GameObject所有 RoomTransition 的 Inspector 绑定将全部失效,维护成本高。其余系统访问 WorldStateRegistry 的方式不统一(部分用 `[SerializeField]`,部分用 ServiceLocator建议全框架统一接入方式。
---
### 2.8 可测试性 Testability — 7.9 / 10
#### 亮点(延续 v6
**ServiceLocator.OverrideForTest / Reset、IValidatable、接口隔离**
(详见 v6
**PlayerStateBase — 无 MonoBehaviour 依赖**
所有 17 个玩家状态均为纯 C# 类,可在 Test Runner 中实例化 Mock `PlayerController` 后直接调用 `OnStateEnter() / OnStateUpdate()`,无需 `LoadScene`,单测成本极低——**这是本轮发现的新亮点**,前几轮未深入阅读 Player.States 程序集。
#### 不足(延续 v6
**T-1GameManager 死亡流程协程 + bool 标志**
**T-2StatusEffectManager Awake 内联工厂初始化顺序敏感**
(详见 v6
---
## 三、综合问题清单
### 需修复Bugs / 一致性破坏)
| ID | 严重度 | 模块 | 描述 |
|----|--------|------|------|
| P-1 | 中 | HitStopManager | `FreezeDuration` 直接覆盖旧协程,注释"取最大时长"的承诺无法兑现,短请求截断长冻帧 |
| A-1 | 低 | WallJumpState | `Move.Rb.velocity.y` 绕过 `PlayerMovement` 运动抽象层,应改用语义属性 |
| S-2 | 低 | RoomTransition | `HasItem``WorldStateRegistry.IsCollected` 检查钥匙物品,概念错位 |
### 建议优化(设计一致性 / 架构改进)
| ID | 优先级 | 模块 | 描述 |
|----|--------|------|------|
| A-2 | 低 | PlayerController | `TryTransitionState``TransitionTo` 等价,命名暗示语义不同,建议删除别名或实现真正的守卫逻辑 |
| P-2 | 低 | EnemyQuotaManager | `Unregister``_registered.Remove(enemy)` 仍为 O(n),建议对齐 BatchLOSSystem 用 swap-and-pop |
| S-1 | 低 | EnemyBase | `SetAggroTickRate` 是空 Stub应增加 `Debug.LogWarning` 提示调用方当前无效 |
| U-2 | 低 | AttackState | `OnStateExit``OnClipEnd` 双重解绑,统一在 `OnStateExit` 解绑即可 |
| DC-2 | 低 | RoomTransition/全框架 | `WorldStateRegistry` 部分用 `[SerializeField]` 部分用 ServiceLocator 注入,建议统一 |
---
## 四、各维度评分
| 维度 | 权重 | 本轮得分 | v6 得分 | 变化 |
|------|------|---------|---------|------|
| 架构设计 | 20% | **9.2** | 9.1 | +0.1 |
| 性能 | 18% | **8.8** | 8.6 | +0.2 |
| 可扩展性 | 15% | **9.1** | 9.0 | +0.1 |
| 编辑器友好 | 12% | **9.3** | 9.3 | — |
| 使用便利性 | 12% | **9.0** | 8.9 | +0.1 |
| 框架纯净度 | 8% | **9.3** | 9.3 | — |
| 数据一致性 | 8% | **9.0** | 9.1 | -0.1 |
| 可测试性 | 7% | **7.9** | 7.9 | — |
### 加权总分
$$
Score = 9.2 \times 0.20 + 8.8 \times 0.18 + 9.1 \times 0.15 + 9.3 \times 0.12 + 9.0 \times 0.12 + 9.3 \times 0.08 + 9.0 \times 0.08 + 7.9 \times 0.07
$$
$$
= 1.840 + 1.584 + 1.365 + 1.116 + 1.080 + 0.744 + 0.720 + 0.553 = \mathbf{9.00 / 10}
$$
**较 v68.93)提升 +0.07**,首次突破 9.00 整数分。
主要驱动因素:
- v6 修复的 P-1~P-4 性能问题带动性能维度 +0.2
- A-1暂停恢复记忆修复带动架构维度 +0.1
- 深度阅读 Player.States 程序集发现 `PlayerStateBase` 纯 C# 设计提升可扩展性与可测试性评估
- HitStopManager 注释/行为不一致P-1使数据一致性小降 -0.1
---
## 五、深度新发现Player 状态机设计亮点
本轮首次完整阅读 `BaseGames.Player.States` 程序集17 个状态文件),发现该子模块整体质量高于框架平均水准,单独列出:
### 5.1 纯 C# 状态 — 可测试性最强设计
全部 17 个状态类(`IdleState` ~ `SwimState`)均不继承 `MonoBehaviour`,通过构造函数注入 `PlayerController` 引用,`Update / FixedUpdate / Enter / Exit` 全部由 `PlayerController` 主动调用——状态对象可在 Test Runner 中独立实例化并断言状态转换逻辑,无场景加载开销。
### 5.2 `AttackState` 的 Animancer 帧事件集成
HitBox 激活时机通过 Animancer 归一化时间事件绑定(而非 `Update` 轮询计时器),确保"逻辑时机"与"动画帧"严格同步,不受帧率波动影响:
```csharp
events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes());
```
同时连击段数从 `AnimCfg.GroundAttacks.Length` 动态读取,段数完全由动画数据决定,代码零硬编码。
### 5.3 `DashState.IsInvincible` 多态无敌帧
```csharp
public override bool IsInvincible => true;
// PlayerController.TakeDamage 中:
if (_currentState?.IsInvincible == true) return; // 冲刺无敌,跳过受击
```
无敌帧由状态自声明,而非 `PlayerStats.IsInvincible` 标志位——避免了状态结束时"忘记清除无敌标志"的经典 bug无敌语义与状态生命周期强绑定。
### 5.4 `ParryState` 的防御性动画降级
见 2.5 亮点,始终不挂死是框架鲁棒性的体现。
---
## 六、与 v6 的横向差异分析
| 问题类别 | v6 发现 | v7 发现 | 趋势 |
|---------|---------|---------|------|
| 严重度"中"Bug | 2U-1 重复注册、A-1 暂停恢复) | 1P-1 HitStop 截断) | ↓ 减少 |
| 低优先级建议 | 7 | 5 | ↓ 减少 |
| 新发现亮点 | — | PlayerStateBase 纯 C# 可测、EnemyQuotaManager LOD | 新增 |
| 遗留技术债 | SetAggroTickRate stub | 同上(低优先级未修复) | 持平 |
框架整体处于"打磨收尾阶段"——核心错误稀少,大部分剩余问题属于"设计一致性"和"文档/代码对齐"而非功能 Bug。
---
## 七、下一轮修复建议优先级
```
[必须修复] P-1 HitStopManager — FreezeDuration 实现真正的"取最大时长"语义
[建议修复] A-1 WallJumpState — 改用 PlayerMovement.IsFalling 等语义属性,封装 Rb.velocity
[建议修复] S-2 RoomTransition — HasItem 改用 IInventoryService 接口
[低优先级] A-2 删除 TryTransitionState 别名,或赋予真正的守卫语义
[低优先级] P-2 EnemyQuotaManager.Unregister 对齐 swap-and-pop 实现
[低优先级] S-1 SetAggroTickRate 增加 Debug.LogWarning
[低优先级] U-2 AttackState 双重解绑整理
[低优先级] DC-2 WorldStateRegistry 注入方式全框架统一
```
---
## 八、评分历史
| 版本 | 加权总分 | 主要驱动变化 |
|------|---------|------------|
| v5 | 8.73 | 基准 |
| v6 | 8.93 | 编辑器工具套件 +0.86 项 Bug 修复 |
| **v7** | **9.00** | 4 项性能修复 +0.2,深度阅读 Player.States 补正评估HitStop Bug -0.1 |
---
*评审人GitHub CopilotClaude Sonnet 4.6*
*上一版:`FrameworkReview_2026_May_v6.md`(加权 8.93*