AI 行为任务、事件与流式导航
重点研究 Framework 可复用机制及其与游戏层的边界;并非逐个审计 GTA 的驾驶、战斗或人口行为。未运行路径搜索。入口:Summary。
1. 决策、任务和路径搜索分层#
Framework ai/task 组织行为状态及子任务,ai/event 表达事件,ai/navmesh 管理网格、搜索请求与路径线程;fwvehicleai 还有车辆路线相关数据。游戏层组合这些机制生成角色/车辆的具体动作。
AI Task 是持续的行为对象,Job 是一段可调度计算。把它们混用会误判可并行性和生命周期。
2. FSM 与层次任务树#
aiTask 提供 FSM 宏,明确区分 OnEnter、OnUpdate、OnExit。状态中可以设置子任务,等待 subtask finished 再转移;MakeAbortable 默认退出当前状态,并可通过默认恢复状态重新进入,也允许子类定制。
aiTaskManager 管理多个任务树和索引/优先级。由此可将总体行为和局部动作分解,避免单个巨型状态机直接承担全部逻辑。但中止语义很重要:动作可能已经持有动画、导航请求或对象引用,切换任务不是只替换指针。OnExit、取消请求与资源清理必须组成一致协议。
这个框架与现代通用 Behavior Tree 有共同的层次组织思想,但本次直接证据是 FSM、task tree 和任务管理接口,不能仅用“行为树”一词抹平其状态与中止机制。
3. AI 更新不是天然线程安全#
BeginTaskUpdates 设置静态 time step,EndTaskUpdates 清零;源码注释明确说明若要多线程更新 task,需要改变这种机制。它还通过断言约束 Begin/End 成对。这证明当前接口依赖共享更新上下文,不能把每个角色任务直接丢进通用 worker 后假定正确。
即使把时间改为参数,实体变更、动画请求、事件队列、随机数和资源所有权仍需逐项处理。新项目适合先并行纯查询或只读感知,产生命令后在确定阶段提交;行为状态机的变更保持明确所有权。
4. 导航搜索:层次数据、优先队列与可定制成本#
CSearchBase 将完成判断、相邻 polygon 可达性、代价/启发式和候选点评分做成虚接口。接口明确出现 A* heuristic;CPathSearchPriorityQueue 使用 binary heap。Heap 节点能表示普通 polygon 或 hierarchical node,保存 cost、已行进距离、位置与前向方向。
这支持在不同导航域/规则中复用搜索基础,但不能据此断言所有车辆/行人路径查询都采用完全相同算法。高层路径、局部 polygon 搜索、LOS、grid 和 floodfill 的目标与终止条件不同。
Heap 预分配最大节点数;节点移动时通过 Node** Handle 修正调用者持有的位置,文档要求 Handle 的存储持续有效。这是在避免重新查找的同时引入显式地址稳定性契约:搜索中断、重用 scratch 或移动结果对象都不能让 Handle 提前失效。
启发式是否 admissible、代价是否一致,取决于派生实现和导航规则。本文没有声称所有搜索都保证全局最短路径。
5. PathServer 的并发协议#
pathserverbase.h 要求所有访问请求集合的代码持 m_RequestsCriticalSectionToken;非 update thread 访问 LoadedMeshes 数组时还需单独的 loaded-mesh 临界区,因为主线程会随时修改数组。请求 semaphore、active/sleeping 标记、ForceAbortCurrentPathRequest 和 stall diagnostics 构成线程通信入口。
这是一种专用服务线程模型,不等于完全无锁队列。队列锁保护请求容器,不自动保护请求所指网格、游戏对象和回调目标。取消请求、停止线程和卸载导航块要以约定顺序协调。
框架头文件仍有游戏类前置声明和“移走非 RAGE 类”的 TODO,说明引擎/游戏边界并不彻底。移植时应抽象查询上下文、过滤器和结果接收接口,而不是直接复制游戏依赖。
6. 大世界导航:数据驻留就是搜索的一部分#
aiMeshStore::RequestAndEvict 接受多个 load regions;保留旧网格时使用 radius + meshRadius + 10,装载区域使用 radius + meshRadius,形成空间滞回。这个 10 是当前实现常量,不是通用最优距离。
需要的网格且未 requested/loading/loaded 时,以 STRFLAG_DONTDELETE 请求。卸载分支在 ref count 大于 1 时按该 Store 协议视为 defrag 进行中并推迟;否则先调用 PrepareToUnloadCB,再清保护标记并 StreamingRemove。这里的 ref count 解释是此局部实现的契约,不能推广到全引擎所有资源。
卸载附近的注释直接指出可能立即需要导航数据锁。这揭示关键权衡:即时释放有利内存,却可能让主线程等待正在搜索的线程。大量玩家兴趣点、高速移动或瞬移会把它放大为帧时问题。
aiNavMeshStore 的 CanPlaceAsynchronously 返回 false,注释说明它仍在 PlaceResource 内调用需要主线程的 Set;对应 SetResource 覆盖为空,避免重复 Set。这是通用 placement/发布流程的一个具体适配差异:即使原始 IO 异步,导航对象装配和接入仍有主线程工作。不能把“导航资源走 streaming”解释为整个导航加载过程都在后台完成。
7. 建议的新项目契约#
路径请求应携带起终点、导航版本、请求者世代、优先级和最大搜索工作量;结果提交时检查任务是否仍活跃、起点是否已显著移动、网格版本是否变化。以上是建议,不是声称现有结构完整包含这些字段。
数据可采用只读快照、epoch 或显式引用保护,避免搜索过程长期持有全局锁;动态障碍更新和网格卸载则在已知阶段提交。是否值得采用这些方法,取决于内存和同时搜索数量,不能用“无锁”替代成本评估。
| 风险场景 | 关键指标 | 建议处置方向 |
|---|---|---|
| 大量角色同时重规划 | 队列年龄、搜索节点数、最老请求 | 错峰、合并重复目标、优先级和工作预算 |
| 玩家高速穿越/瞬移 | 导航缺块、同步 placement、卸载锁等待 | 预取、多兴趣点、显式等待或降级行为 |
| 任务中止后结果到达 | 陈旧结果计数、取消延迟 | 请求世代和提交检查 |
| 高复杂度动态区 | heap 高水位、搜索上限 | 层次路径、局部修复、可解释的失败码 |
| 直接并行化行为更新 | 共享状态竞争与提交顺序 | 只读查询并行,状态写入显式归属 |
以上实验均未执行。
8. 源码证据#
| 文件 | 入口 |
|---|---|
| task.h | 34–90 FSM/中止;97–115 静态时间步 |
| taskmanager.h | 任务树/索引/优先级 |
| searchbase.h | 完成、邻接、代价和启发式接口 |
| priqueue.h | 19 binary heap;65–82 节点/预分配;128 起 Handle 生命周期 |
| pathserverbase.h、pathserverthread.h | 49–52 锁契约;请求、信号、取消及线程状态 |
| navmeshstore.h | 314 起 RequestAndEvict;435–478 请求/卸载;675 同步 placement |