26 KiB
BaseGames Framework — 第七轮完整评审
日期: 2026 年 5 月
评审轮次: v7(v6 九项修复落地后全量复审 + 深度扩展模块阅读)
版本基线: 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,生命周期完全由控制器驱动。这一设计使新增状态只需创建类 + 在字典注册,其余代码无需改动。
// 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
// 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 完全等价
// 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-pop,O(1) 完成,无 O(n) 数组搬移。
// 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 未实现"取最大值"语义
注释(中英两处)均声明"若已有冻帧进行中,取两者中持续时间较长的(避免短请求截断较长的冻帧)",但实际代码:
// 注释称"取最大",实为"直接覆盖"
if (_activeRoutine != null)
StopCoroutine(_activeRoutine); // 停止旧冻帧(不管剩余时长)
_activeRoutine = StartCoroutine(FreezeRoutine(unscaledSeconds)); // 以新时长重启
若 Boss 死亡触发 10 帧冻帧,2 帧后普通命中再触发 2 帧冻帧,结果是原来 8 帧被截断为 2 帧,打击感大幅削弱。建议增加剩余时间追踪:
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 — 子类可覆盖的状态字典
// EnemyBase.Awake — 默认注册
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
// 子类可在 base.Awake() 后替换:
_stateObjs[EnemyStateType.Hurt] = new EliteHurtState(); // 精英怪专用受击逻辑
这一开放/封闭设计允许具体敌人精确替换单个状态行为而无需重写整个状态机,扩展颗粒度极细。
AttackState — 连击段数完全数据驱动
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 — 扩展点已留
public virtual void EnterPhase(int phase)
{
_currentPhase = phase;
_onBossPhaseChanged?.Raise(new BossPhaseEvent { BossId = _bossId, Phase = phase });
}
子类 override 只需增加阶段特有逻辑,事件广播在基类处理,UI / 音乐 / 摄像机等系统通过频道响应,Boss 代码与表现层零耦合。
不足
S-1(低)EnemyBase.SetAggroTickRate 完全空 Stub
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 语义错位
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 关键依赖
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(见 v6,BossSkillSequenceWindow 无 PNG 导出;DebugCheatSystem 无 Tab 补全)
2.5 使用便利性 Developer UX — 9.0 / 10
亮点
ParryState — Animancer 帧事件兜底
var state = Anim?.Play(AnimCfg.ParryStart);
if (state != null)
{
state.Events(this).OnEnd = OnParryEnd;
return;
}
OnParryEnd(); // 无动画时立即退出,不挂死
状态永远不会因为动画 clip 未配置而"挂死"——无动画时安全降级到立即结束。同样模式在多个状态中一致使用,防御性设计扎实。
InputBuffer — 消耗式设计
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 — 注释约定
/// 使用方请与 range*range 比较,而非直接与 range 比较。
public float SqrDistanceToPlayer { get; set; }
字段名 + XML 注释明确标注"平方距离"约定,IsPlayerInRange() 内部也用 range * range 比较,调用方不会误用开根号版本,API 约定自文档化。
不足
U-1(低)TryTransitionState 命名暗示有条件但行为无条件
(详见架构 A-2,对 API 使用者有认知摩擦)
U-2(低)AttackState 在 OnStateExit 和 OnClipEnd 双重解绑
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 比例
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 — 比例吸收设计
int toAbsorb = Mathf.FloorToInt(amount * AbsorptionRatio);
toAbsorb = Mathf.Min(toAbsorb, CurrentShieldHP);
int passthrough = amount - toAbsorb;
护盾只按配置比例吸收(而非全部吸收),穿透量继续走 TakeDamage 流程;护盾破碎时惩罚计时器激活,破碎期间无法再次吸收——护盾状态转换有完整的状态机语义,数据流路径清晰且无歧义。
不足
DC-1(中)HitStopManager 注释与行为不一致
此问题跨越"性能"与"数据一致性"两个维度:注释文档(两处)声明"取最大值"语义,但代码实现为"直接覆盖"。这不仅是逻辑 Bug(P-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-1(中)GameManager 死亡流程协程 + bool 标志
T-2(低)StatusEffectManager 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}
较 v6(8.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 轮询计时器),确保"逻辑时机"与"动画帧"严格同步,不受帧率波动影响:
events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes());
同时连击段数从 AnimCfg.GroundAttacks.Length 动态读取,段数完全由动画数据决定,代码零硬编码。
5.3 DashState.IsInvincible 多态无敌帧
public override bool IsInvincible => true;
// PlayerController.TakeDamage 中:
if (_currentState?.IsInvincible == true) return; // 冲刺无敌,跳过受击
无敌帧由状态自声明,而非 PlayerStats.IsInvincible 标志位——避免了状态结束时"忘记清除无敌标志"的经典 bug,无敌语义与状态生命周期强绑定。
5.4 ParryState 的防御性动画降级
见 2.5 亮点,始终不挂死是框架鲁棒性的体现。
六、与 v6 的横向差异分析
| 问题类别 | v6 发现 | v7 发现 | 趋势 |
|---|---|---|---|
| 严重度"中"Bug | 2(U-1 重复注册、A-1 暂停恢复) | 1(P-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.8,6 项 Bug 修复 |
| v7 | 9.00 | 4 项性能修复 +0.2,深度阅读 Player.States 补正评估,HitStop Bug -0.1 |
评审人:GitHub Copilot(Claude Sonnet 4.6)
上一版:FrameworkReview_2026_May_v6.md(加权 8.93)