AI选敌:不要在排序比较器里寻路
自动关联目录:AI选敌:不要在排序比较器里寻路
核心判断:候选排序应比较已经计算好的分数。把同步寻路和全场 AI 枚举放进比较或候选循环,会把一次“选最近目标”放大成不可控的 DS 尖峰。
一、业务冲突:既要选得合理,又不能让所有怪同时选同一人
多人 PVE 通常要按距离、可达性、玩家状态、上一个目标和已被追击数量选敌。欧氏距离便宜但可能隔墙;路径距离更准确却昂贵;限制追击数能改善体验,却需要共享全局占用信息。
二、实际实现:规则完整,计算位置却不对
现有 BT Task 先过滤失效目标,再可选地按距离排序,支持最小/最大范围、排除 Tag、排除状态、避开上一目标和每玩家最大追击数。这些业务规则是完整的。
问题在于启用路径距离后,排序比较器每比较两个候选就同步调用两次 GetPathLength,且没有处理查询失败;选定候选前,又逐个调用“当前多少 AI 正在追此玩家”,其内部会 GetAllActorsOfClass 枚举全场 NPC。若候选数为 C、AI 数为 M,一次重选近似包含 O(C log C) 次路径查询和 O(C*M) 次扫描,还会因 AI 执行顺序产生先到先得偏差。
flowchart LR
P[Perception candidates] --> C[Cheap filters]
C --> K[Euclidean Top-K]
K --> Q[Budgeted reachability queries]
Q --> S[Cached score snapshot]
S --> L[Target slot lease]
L --> B[Commit Blackboard target]三、值得保留:过滤条件数据化,范围使用平方距离
把排除状态、Tag、上一目标与追击上限暴露成任务配置,便于复用;范围过滤使用 DistSquared 避免开方,候选用弱引用并在使用前验证,也都值得保留。设置目标时同时更新 Blackboard 和角色上的 ChaseTarget,使后续规则能观察选择结果,方向同样正确。
四、结构性缺陷:排序阶段重复做昂贵且可能失败的工作
比较器应满足稳定、廉价、无副作用。路径查询既昂贵又可能因动态 NavMesh 返回失败值;失败结果若保留默认长度,反而可能被排到最前。全场枚举追击者没有缓存,也无法表达“本轮已经预留但尚未提交”的槽位。多个 AI 同帧重选时,即使游戏线程串行,也会形成顺序偏置和路径查询洪峰。
五、目标设计:共享 Assignment Service 统一分配预算
服务维护 TargetId -> ActiveLeaseCount,AI 注册/注销和换目标时增量更新,不再枚举世界。选择先做阵营、状态、直线距离等廉价过滤,只对 Top-K 发起限额可达性查询;结果缓存键必须包含 NavRevision、AgentType 和起终点网格。异步结果携带 SelectionRevision,过期即丢弃。
void RequestAssignment(AIHandle ai, CandidateSnapshot candidates)
{
SelectionRevision rev = ++ai.selectionRevision;
auto top = CheapScore(candidates).TakeTopK(pathBudgetPerAI);
PathBudget.Enqueue(ai, rev, top, [=](PathScores scores) {
if (!ai.IsCurrent() || ai.selectionRevision != rev) return;
TargetHandle target = PickWithAvailableSlot(scores, SlotLedger);
TargetLease lease = SlotLedger.TryReserve(ai, target);
if (lease) CommitTarget(ai, lease, rev);
});
}六、决策规则:准确性必须按阶段购买
| 阶段 | 允许的计算 | 目标 |
|---|---|---|
| 候选召回 | Perception/空间索引 | 不漏掉附近可能目标 |
| 廉价过滤 | 阵营、存活、Tag、平方距离 | 快速缩小工作集 |
| Top-K 粗排 | 距离、仇恨、任务权重 | 只保留少量高价值候选 |
| 可达性 | 限额路径测试、缓存 | 不在排序比较器中重复执行 |
| 槽位仲裁 | 共享计数与 Lease | 避免全场扫描和超卖 |
| 提交 | Revision 校验 | 拒绝迟到异步结果 |
这里的核心优化不是把某个查询改快,而是把每只 AI 私有的重复计算改成共享、可预算、可失效的分配服务。