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 拥有的句柄集仅按类型或来源批量删除
中断一个幂等 EndAttackStun、死亡、回池各自遗漏清理

这套边界不要求把行为树、状态机或 GAS 其中之一删掉,而是让三者各自只回答一个问题:Brain 决定做什么,状态机编排战术阶段,Ability 原子地提交或撤销一次攻击。