AI战斗:状态机编排意图,Ability 提交结果
自动关联目录:AI战斗:状态机编排意图,Ability 提交结果
核心判断:PreAttack、Attacking、PostAttack 适合表达 AI 的战术阶段,Ability 适合拥有一次攻击的提交、命中、消耗和取消。两边都用自己的计时器推进攻击,会出现动画结束了但伤害仍提交,或状态退出了但 GameplayEffect 仍残留。
一、业务冲突:攻击既是决策,也是可取消事务
AI 要先选定目标和招式,再经过前摇、命中窗口、后摇回到决策;期间可能被眩晕、目标死亡、Actor 回池或服务器纠正。前摇属于可读性,命中属于权威玩法,后摇属于节奏控制,它们不能被一个模糊的 Attacking 布尔值同时承担。
二、实际实现:阶段齐全,但攻击存在多个推进者
工程已经把攻击拆成 PreAttack、Attacking、PostAttack 等状态,并由 DS 状态机驱动。Ability 一侧又有任务每帧读取能力黑板中的目标数组:数组从空变为非空时给自身施加 GameplayEffect,恢复为空时按保存的 ActiveEffectHandle 精确移除。保存句柄而非只按类型删除,是正确的实例化思路。
问题在于状态 Update、Ability Task Tick、动画 Notify 和 GameplayEffect 生命周期都可能成为攻击时钟。任务结束回调没有兜底移除仍在生效的 Effect;另一路 Buff 清理只依赖 BuffId 与来源,无法证明删除的是本次攻击创建的实例。表现类任务还需要明确 Net Execution Policy,不能依赖默认执行域碰巧正确。
sequenceDiagram
participant B as AI Brain
participant S as Combat State
participant A as Ability
participant C as Client Cue
B->>S: AttackIntent(Target, Spec)
S->>A: TryActivate(ExecutionId)
A->>A: Commit cost/cooldown
A-->>C: PreAttack cue + server time
A->>A: Open/close hit window
alt completed
A->>S: Completed(ExecutionId)
else stun/target lost/pool return
S->>A: Cancel(ExecutionId, Reason)
A->>A: remove owned effects/tasks
end三、值得保留:精确句柄和战斗分阶段
分阶段状态让行为树只关心“何时尝试攻击、完成后去哪里”,而不需要展开每段动画;Ability 的 ActiveEffectHandle 又能准确撤销自己创建的效果。这两点组合后可以形成清晰边界:状态机持有战术意图,Ability 持有攻击事务,动画只投影权威时间线。
四、结构性缺陷:一次攻击没有统一身份
若没有 AttackExecutionId,旧 Ability 的异步目标查询、Notify、Task Tick 或结束委托可能写入新一轮攻击。依靠“当前还是 Attacking”无法区分同状态的两次进入。每帧轮询黑板数组还把成本绑定到活跃 Ability 数量,并把目标变更隐藏成状态差分,难以解释是谁移除了目标。
更危险的是取消不完整:OnTaskEnd 为空意味着 Task 被外部终止时,最后一组 ActiveEffectHandle 可能没有回收。状态退出、Ability End、Actor EndPlay 和 Pool Return 若各写一套清理逻辑,也会出现重复删除或遗漏。
五、目标设计:以攻击执行实例统一提交和清理
DS 为每次激活生成执行 ID;Ability 在 Commit 后独占命中窗口、Effect 句柄和 Task,状态机只等待 Completed/Cancelled。目标集合改为事件驱动版本,回调必须校验执行 ID。所有正常完成与异常中止汇聚到同一个幂等 Cleanup;客户端依据服务器开始时间 Seek Montage,只运行 Cue,不决定伤害。
void BeginAttack(const AttackIntent& intent)
{
const AttackExecutionId id = NextExecutionId();
if (!AbilitySystem.TryActivate(intent.spec, intent.target, id)) {
StateMachine.FailAttack(id, RejectReason::ActivationFailed);
return;
}
CurrentAttack = id;
}
void EndAttack(AttackExecutionId id, EndReason reason)
{
if (id != CurrentAttack) return;
CancelTasks(id);
RemoveEffectsCreatedBy(id);
CloseHitWindows(id);
CurrentAttack.Reset();
StateMachine.OnAttackEnded(id, reason);
}六、决策规则:每个阶段只保留一个权威时钟
| 关注点 | 唯一所有者 | 禁止的替代实现 |
|---|---|---|
| 选目标、选招式 | AI Brain / 状态机 | Anim Notify 临时换权威目标 |
| 消耗、冷却、命中 | DS Ability 实例 | 客户端动画事件提交伤害 |
| 前摇、命中窗、后摇 | Ability 权威时间线 | 状态 Timer 与 Ability Timer 各推进一次 |
| 特效与音效 | GameplayCue / 表现层 | Effect Task 默认在所有网络角色执行 |
| Effect 清理 | AttackExecutionId 拥有的句柄集 | 仅按类型或来源批量删除 |
| 中断 | 一个幂等 EndAttack | Stun、死亡、回池各自遗漏清理 |
这套边界不要求把行为树、状态机或 GAS 其中之一删掉,而是让三者各自只回答一个问题:Brain 决定做什么,状态机编排战术阶段,Ability 原子地提交或撤销一次攻击。