异步选敌:TraceHandle完成不代表仍属于本次施法

自动关联目录:异步选敌:TraceHandle完成不代表仍属于本次施法

核心判断:FTraceHandle 只能证明某次 Chaos 查询可以取结果,不能证明这份结果仍属于当前能力尝试。异步选敌必须同时冻结“哪次施法”和“在哪个空间、用哪些参数查询”。

一、业务问题:选敌跨帧后,施法条件可能已经变化

范围技能用同步 Overlap 很直接,但同帧大量释放会把物理查询集中到 Game Thread。改成异步查询能平滑尖峰,却引入一帧以上的时间差:施法者可能移动、转向、取消,动态半径或过滤条件也可能变化,World 甚至已经退出。

问题不是“查询有没有完成”,而是完成时该采用哪一种语义:

  • 快照语义:命中属于按键那一刻的空间和参数;
  • 新鲜语义:提交前必须仍满足当前空间和参数,否则重查。

两者都可以,混用不可以。

二、实际实现:轮询模型合理,但 Handle 承担了过多语义

实际能力上下文只保存一个异步查询 Handle。第一次进入启动检查时,代码计算动态位置、形状和碰撞对象类型并发起 Overlap;组件把上下文放入异步队列,随后在 Tick 中再次进入完整启动检查。查询完成后写入目标、清空 Handle,再继续目标必需性和自定义条件检查。

这个设计有三点值得保留:查询队列由能力组件统一驱动;同步与异步结果复用同一过滤链;没有给每次查询散落一个长生命周期 Delegate。

缺口也很具体:

  1. Handle 没有 AttemptId、上下文代次和超时;
  2. 轮询前会重新计算动态参数,锥形查询虽保存了发起时 Transform,却用轮询时的角度、高度和长度处理旧 Overlap,形成混合快照;
  3. 重新进入完整启动函数,会再次执行实例策略和前置检查,而不是只推进“查询等待”阶段;
  4. 所检查的结束路径会清理活动实例和待取消项,但异步查询队列没有同等明确的关闭协议。

三、目标方案:把能力尝试建模为分阶段事务

struct FTargetQueryTicket {
    uint64 AttemptId;
    uint32 ContextGeneration;
    TWeakObjectPtr<UWorld> World;
    FTransform IssuedTransform;
    FQueryParamsSnapshot Params;
    FTraceHandle Handle;
    double Deadline;
};

void Poll(FTargetQueryTicket& T) {
    if (!IsCurrentAttempt(T.AttemptId, T.ContextGeneration) ||
        T.World != GetWorld() || Now() > T.Deadline) {
        Retire(T, EReason::Stale);
        return;
    }

    FOverlapDatum Data;
    if (!T.World->QueryOverlapData(T.Handle, Data)) return;

    auto Targets = FilterAgainstSnapshot(Data, T.Params, T.IssuedTransform);
    if (FreshnessPolicy == EFreshness::RequireCurrent &&
        HasQueryInputChanged(T)) {
        ReissueForSameAttempt(T.AttemptId);
        return;
    }
    CommitTargets(T.AttemptId, MoveTemp(Targets));
}

启动路径应拆成 Prepare -> AwaitQuery -> ValidateCurrentState -> Commit。Prepare 只执行一次;轮询只推进 Ticket;Commit 再检查冷却、打断关系和自定义条件。上下文回池或 EndPlay 先提升 Generation、退休队列,再释放上下文。即使底层查询不能取消,旧结果也已经失去提交权。

四、取舍与验证

快照语义响应稳定,但高速移动时视觉上可能命中旧位置;新鲜语义更符合“持续瞄准”,代价是重查和额外延迟。这个选择应成为能力数据,而不是查询实现的偶然结果。

验证至少覆盖:发起后转身或位移、动态范围改变、同一上下文连续激活、查询完成与取消同帧、World 结束、超时、查询失败,以及异步关闭后上下文立即回池。日志应关联 AttemptId、ContextGeneration、IssuedFrame、CompletedFrame、SnapshotDigest、TerminalReason。

五、反思

现有实现正确地把昂贵查询移出同步启动路径,却只异步化了 Chaos 工作,没有异步化业务状态机。引擎 Handle 是完成凭证;Attempt Ticket 才是结果归属凭证。