5.7 KiB
小怪 / Boss 共享底座:抽取决策记录
日期:2026-07-27 状态:已实施(Layer A),B/C 明确否决 背景:小怪走
EnemyAiBrain + PerceptionStateMachine + EnemyAbilityBase,Boss 走BossBase + BossSkillExecutor + BossSkillSO。两套决策/技能层并行演进,出现机制原语的平行实现。
1. 结论摘要
| 层 | 内容 | 决策 | 依据 |
|---|---|---|---|
| A | 加权随机取索引 | ✅ 已抽取 BaseGames.Core.WeightedPick |
4 份平行实现,且已出现分歧 |
| B | 冷却记账 | ❌ 否决 | 全项目冷却是异构的,符合该形态的只有 1 个消费者 |
| C | HitBox 时序窗口 | ❌ 否决 | 仅 2 个形态不同的消费者,且两处均无测试覆盖 |
核心原则:只抽"两边同构且会漂移"的原语;不为"看起来像"的浅层重复付重构风险。
2. Layer A:已实施
抽取前的 4 份平行实现
EnemyAttackSelector.SelectByWeight(小怪选招)BossBase.UseBossSkillWeighted(Boss 选招,带上一招 0.3× 防重复惩罚)BossSkillExecutor.SelectWeightedSkill(零调用者的死代码,与 2 近似重复)LootResolver.Resolve(战利品掉落;权重被算了两遍——累加一遍、抽取时再算一遍, 两处公式若改动不同步即产生分布偏差)
抽取结果
namespace BaseGames.Core
{
/// 调用方把候选的"有效权重"填入列表(不合格候选填 0),本原语只负责按权重挑索引。
public static class WeightedPick { public static int Index(IReadOnlyList<float> weights); }
}
- 语义:权重 ≤ 0 的项永不被选中(负权重按 0 处理);无正权重返回
-1;含浮点累加误差兜底。 - 放
BaseGames.Core(非 Combat)——因为消费者含战利品掉落,并非战斗专属。 - 零 GC:调用方复用权重缓冲区(
EnemyAttackSelector._weightBuf、BossBase._candidateWeights)。 - 覆盖:10 条 EditMode 单测(含"绝不选中零/负权重项"200 次采样断言、权重分布断言)。
- 顺带清理:删除死代码
SelectWeightedSkill;消除LootResolver权重重复计算。
为何不做 ISelectable 接口 + WeightedSelector<T>(初版草案)
深入代码后否决:
BossSkillSO没有priority字段(Boss 只用加权随机)→ 强加接口会产生无意义成员;LootTableSO.Entries的权重是动态计算的(难度加成),无法用静态接口属性表达;- 候选类型/合格判定两边完全不同,抽到接口层只会造出"上帝抽象"。
→ 改为更低层、更通用的
Index(weights),合格性与权重加成留在各自领域内。
3. Layer B(冷却):否决理由
全项目冷却实现是异构的,不存在可统一的单一形态:
| 系统 | 形态 |
|---|---|
ParrySystem / DashState / ToolSlotManager / SkillManager |
每帧递减的倒计时器 |
BossSkillExecutor |
Dictionary<string,float> 截止时刻 |
EnemyAbilityBase |
单个 float 截止时刻(每组件一个,判定仅 1 行) |
符合"多 key 截止时刻字典"形态的只有 BossSkillExecutor 一处。为单一消费者建抽象=纯仪式;
强行统一则要重写玩家闪避/弹反/道具/技能 4 个无关系统,风险与收益完全不成比例。
4. Layer C(HitBox 时序窗口):否决理由
两个消费者形态本质不同:
MeleeAttackAbility.PlayAttackStep:归一化(hitBoxEnterT/ExitT× duration)、单 HitBox、逐帧Time.deltaTime推进、需跑满整段时长。BossSkillExecutor.ExecutePatternCoroutine:绝对秒数(windup/active/recovery)、多 HitBox 齐开齐关、WaitForSeconds推进。- 玩家侧
WeaponHitBoxInstance(外部动画事件驱动开关)/SkillHitBoxInstance(池化实例生命周期)是另一种语义,不是同一回事。
叠加关键风险:两处都无自动化测试覆盖。重构等于在无测试保护下改动战斗手感时序, 一个细微的时序回归难以发现。重复本身很浅(开/等/关约 5 行),不构成维护危害。 → 判定:收益 < 风险,不做。若将来出现第 3 个同形态消费者,可重新评估。
5. 后续路线:Boss 决策层走 BrainGraph(延后,按需触发)
可行性高:BossBase 已暴露决策所需 API(UseBossSkillWeighted / IsBossSkillExecuting /
IsHPBelow / CurrentPhase / BeginPhaseTransition / IsPhaseTransitioning),
只需一个 IBossControl facet 挂进 IAiContext(同 ICombatant 模式,保证 lambda 不捕获具体实例),
BossSkillExecutor 的富执行层原样保留。
当前不必要。触发条件(任一出现再做):
- 出现第 2/3 个 Boss,或需要"精英怪 / mini-boss"中间形态(要混用小怪状态 + Boss 技能);
- 要清理 BD(Behavior Designer)迁移债——现存
StopBehaviorTree、BD_*注释残留; - 希望 Boss 也吃上小怪那套工具链(状态 Inspector、
TransitionRecord、EditMode 测试)。
执行建议:不要拿已能跑的 ChaoFeng 开刀验证;等下一个新 Boss 时用 BrainGraph 从零搭
(决策走图 + IBossControl,执行仍用 BossSkillExecutor),验证顺畅后再回迁 ChaoFeng。
终态:决策层统一 BrainGraph + 机制原语统一底座 + 执行层按复杂度分层
(小怪 EnemyAbilityBase / Boss BossSkillExecutor)——既消重复,又保留 Boss 编排自由。
6. 注记
UseBossSkillWeighted目前无调用者(原 BD 任务已移除),是等待决策层接入的 Boss API,故保留。- 验证:编译 0 错误 0 警告;EditMode 177/177 通过(原 167 + 新增 10)。