CD Code Digest Hub /GTA V · v2

导航与 PathServer:navmesh、寻路线程、导航任务与 game/ai

125 分钟阅读 18 个章节 2 张图

GTA V 里行人的步行寻路(以及载具导航网格任务、掩体/生成点搜索、音频环境查询等附带用途)全部由 pathserver 完成:离线生成的 navmesh 按 150 m 网格切块,以资源形式流式加载;主线程上的客户端(主要是导航移动任务)把请求放进固定数量的请求槽并拿到句柄;独立的 PathServerThread 在持有 navmesh 数据锁的情况下按类型轮转取出请求,执行 A*、string pulling 与后处理,把结果写回槽位;客户端每帧轮询、拷出结果并释放槽位。同一工作线程在请求间隙还做 “housekeeping”:同步动态障碍物、抽取掩体点与水边、为行人人口搜索生成坐标。

本页覆盖 game/pathserver、其所依赖的 rage/framework/src/ai/navmesh 数据层、game/task/Movement 中与 navmesh 请求有关的部分,并盘点 game/ai 目录(这些文件多数与寻路无关,是 AI 通用辅助设施)。

背景与相关页面#

相关源码文件#

位置内容
rage/framework/src/ai/navmesh/navmesh.h/.cppCNavMesh、TNavMeshPoly、TAdjPoly、四叉树、特殊链接、掩体点、层次节点类型
rage/framework/src/ai/navmesh/datatypes.h句柄、错误码、路点标志、TDynamicObject、CNavMeshTrackedObject、影响球等
rage/framework/src/ai/navmesh/requests.h请求类型枚举、CPathServerRequestBase 及各派生请求、CPathFindMovementCosts
rage/framework/src/ai/navmesh/pathserverbase.h/.cppfwPathServer 基类、五个临界区、锁宏、PATH_FLAG_*、动态对象网格容器
rage/framework/src/ai/navmesh/pathserverthread.hTRequestPathStruct、请求槽容量、线程栈大小、TPathFindVars
rage/framework/src/ai/navmesh/navmeshstore.h/.cppaiMeshStore/aiNavMeshStore(流式模块)、动态 navmesh 引用表
rage/framework/src/ai/navmesh/tessellation.cpp、priqueue.h、shortminmax.h、splitarray.h、navmeshextents.*、pedgen.h细分网格、A* 二叉堆、定点包围盒、分块数组、世界划分、ped 生成数据
game/pathserver/PathServer.h/.cppCPathServer 静态门面:初始化、每帧 Process、请求 API、流式请求/驱逐
game/pathserver/PathServerThread.h/.cppCPathServerThread:线程主循环、调度、housekeeping、LOS/音频/洪泛/空地/最近点请求
game/pathserver/PathServer_PathRequest.cpp、_PathSearch.cpp、_PathStringPull.cpp、_PathMisc.cpp路径请求流水线、A*、string pulling、路径后处理与细分辅助
game/pathserver/PathServer_Objects.cpp动态障碍物、细分(tessellation)、ped 追踪、脚本阻挡物
game/pathserver/PathServer_Immediate.cpp、_Ped.cpp、_Cover.cpp、_Store.cpp主线程即时查询、ped 生成与区域开关、掩体点/水边、aiNavMeshStoreInterfaceGta
game/pathserver/PathServer_Hierarchical.cpp、_Grid.cpp、PathServerSPU.*、NavEdit.*、ExportCollision.*、NavMeshGta.*、PathServer_Debug.cpp已禁用/桩代码、编辑器与导出工具、调试可视化
game/task/Movement/TaskNavBase.*、TaskNavMesh.*、TaskMoveWander.*;game/task/System/TaskHelpers.*导航任务与 navmesh 请求辅助类
game/ai/*AI 辅助设施(见 §12)

1. 分层与组件关系#

代码分成三层:

  1. 框架层 rage/framework/src/ai/navmesh(namespace rage):数据结构、流式存储、请求类型和 fwPathServer 基类。基类持有每个导航域的 aiNavMeshStore*、细分用的 m_pTessellationNavMesh、动态 navmesh 表、请求信号量和“强制中止”标志 src/dev_ng/rage/framework/src/ai/navmesh/pathserverbase.h:60-184。
  2. 游戏层 game/pathserver:CPathServer : fwPathServer 是全静态门面 src/dev_ng/game/pathserver/PathServer.h:62;CPathServerThread : fwPathServerThread 是寻路实现 src/dev_ng/game/pathserver/PathServerThread.h:57-64;CPathServerGta : CPathServer 放与 CEntity/CVehicle/CFire 相关的动态对象接口 src/dev_ng/game/pathserver/PathServer.h:1130、PathServer.h:1142-1180;CPathServerGameInterfaceGta : fwPathServerGameInterface 是注入框架层的游戏接口 src/dev_ng/game/pathserver/PathServer.h:1192。
  3. 客户端:导航任务、TaskHelpers 中的 navmesh 辅助类、战斗管理器、音频环境、派遣、脚本命令等。

头文件的总体设计注释与实现一致:请求返回句柄并排队,由独立线程处理;客户端每帧轮询,再用 Lock/Unlock 语法取结果;同类还提供 LOS(实为“可移动性”测试)、掩体点和 ped 生成 src/dev_ng/game/pathserver/PathServer.h:36-58。注释里提到的网格(grid)请求在实现中已停用(§4.1)。

图中证据:客户端 API src/dev_ng/game/pathserver/PathServer.h:214-251;请求槽数组 PathServer.h:913-919;Process 在 CScene::Update 中调用 src/dev_ng/game/scene/scene.cpp:831-832,并调用 RequestAndEvictMeshes src/dev_ng/game/pathserver/PathServer.cpp:900-901;线程取请求、housekeeping、按类型分派 src/dev_ng/game/pathserver/PathServerThread.cpp:1070-1201;路径流水线调用 FindPath/RefinePathAndCreateNodes src/dev_ng/game/pathserver/PathServer_PathRequest.cpp:430、PathServer_PathRequest.cpp:485;物体移动时更新 src/dev_ng/game/scene/Physical.cpp:555-559;线程拷贝新包围盒 src/dev_ng/game/pathserver/PathServer_Objects.cpp:1400-1430;流式放置/移除 src/dev_ng/rage/framework/src/ai/navmesh/navmeshstore.cpp:341-352、navmeshstore.cpp:420-451。

2. navmesh 数据模型#

2.1 世界划分、索引空间与导航域#

  • 网格:游戏内默认每块 navmesh 覆盖 3 个 50 m 扇区(150 m),共 100×100 块,世界范围 X/Y ∈ [-6000, 9000] src/dev_ng/rage/framework/src/ai/navmesh/navmeshextents.cpp:11-16、navmeshextents.h:35-42。NavMeshGta.h 的注释仍写“每块 4x4、8x8 扇区”,与上述默认值不一致,属于过时设计注释 src/dev_ng/game/pathserver/NavMeshGta.h:26-29。
  • nav.dat:运行时从 common:/data/nav.dat 读取 SECTORS_PER_NAVMESH、NAVMESH_LOAD_DISTANCE、MAX_NUM_NAVMESHES_IN_ANY_LEVEL、MAX_NUM_DYNAMIC_NAVMESH_TYPES;块数在代码里硬编码为 100×100 src/dev_ng/game/pathserver/PathServer.cpp:586-683。仓库中没有 nav.dat 数据文件,实际取值无法确认。
  • 索引空间:TNavMeshIndex 是 u32,但只用 14 位;0..9999 是地图块,10000..10099 是动态 navmesh,10100 是唯一的细分网格,0x3FFF 表示无 src/dev_ng/rage/framework/src/ai/navmesh/navmesh.h:113-135。fwPathServer::GetNavMeshFromIndex 按这三段分派到细分网格、store 或动态 navmesh 表 pathserverbase.h:86-105。
  • 导航域 aiNavDomain:kNavDomainRegular 与为空中导航引入的 kNavDomainHeightMeshes src/dev_ng/rage/framework/src/ai/navmesh/datatypes.h:26-35。RegisterStreamingModule 一开始就把 height mesh 域禁用,只有启用时才创建它的 store src/dev_ng/game/pathserver/PathServer.cpp:4584、PathServer.cpp:4637-4655;在本代码中未见到重新启用它的位置(未穷尽搜索)。

2.2 CNavMesh 的内存布局#

CNavMesh 继承 pgBase,是可重定位资源(RORC_VERSION = 2),流式放置函数用 pgRscBuilder::PlaceStream 原地放置 src/dev_ng/rage/framework/src/ai/navmesh/navmesh.h:1432-1450、navmesh.cpp:389-400。主要成员 navmesh.h:1538-1605:

成员作用
m_iFlagsNAVMESH_COMPRESSED_VERTEX_DATA、HAS_SPECIAL_LINKS_SECTION、IS_DYNAMIC、HAS_WATER、DLC、MODIFIED_FOR_DLC_SWAP;高 8 位是 DLC 组号 navmesh.h:1415-1430
m_Matrix动态 navmesh 从局部到世界的矩阵
m_vExtents + m_pNonResourcedData->m_vMinOfNavMesh压缩顶点的解码范围
m_CompressedVertexArray / m_VertexPool压缩顶点(u16 data[3])或未压缩顶点 navmesh.h:208-219
m_VertexIndexArray(u16)、m_AdjacentPolysArray(TAdjPoly)每个多边形从 GetFirstVertexIndex() 开始连续存放顶点索引与逐边邻接 navmesh.h:2341-2347
m_AdjacentMeshesCAdjacentNavmeshLookups:最多 32 个相邻 navmesh 的查找表,TAdjPoly 里只存 5 位下标 navmesh.h:221-237
m_PolysArrayTNavMeshPoly 分块数组
m_pQuadTree用于“点下方是哪个多边形”查询的四叉树;叶子还挂掩体点 navmesh.h:893-938
m_SpecialLinks / m_SpecialLinksIndices梯子上下、攀爬物体等特殊链接,起止点以 u16 存放,并记录原始/当前多边形 navmesh.h:1084-1161
m_iBuildID校验所有 navmesh 是否同批构建(防止打包进陈旧文件)

四个大数组都是 aiSplitArray,块大小按 16 KB 计算(压缩顶点每块 2730 个,顶点索引每块 8192 个) navmesh.h:1366-1369。顶点坐标解码为 u16/65536 × extents + min navmesh.h:1840-1858;按 150 m 块宽推算,XY 量化步长约 2.3 mm(推断,Z 取决于每块的高度范围)。顶点读取会根据是否压缩走两条路径 navmesh.h:2361-2372。点查询入口是 GetQuadTreeLeaf/GetPolyBelowPoint navmesh.cpp:3070-3122。掩体点 CNavMeshCoverPoint 用 u16 坐标存放,类型有矮墙/左右墙等 6 种 navmesh.h:945-950、navmesh.h:1036-1068。

2.3 多边形与邻接的位域#

TNavMeshPoly 注释说明:顶点顺时针(俯视)、边 N 由顶点 N 与 N+1 围成;只有 m_Flags 的低 8 位随网格保存 navmesh.h:578-593。字段 navmesh.h:718-826:

字段位宽/类型含义
m_Flags / m_iNumVertices / m_iPointEnum21 / 4 / 7标志;顶点数(最多 16);A* 在本多边形内选中的候选点编号
m_iFirstVertexIndex / m_iNavMeshIndex16 / 14在顶点索引/邻接数组中的起点;所属 navmesh
m_TimeStamp、m_AStarTimeStampu16 ×2LOS 递归防重入与 A* “首次访问”时间戳
m_PathParentPoly指针A* 回溯父节点
m_MinMaxTShortMinMax0.25 m 定点包围盒
m_Struct34+1×10+3+15音频属性、调试标记、靠近车道节点、室内、孤立、DLC 缝合、联网出生候选、道路、网格边缘、铁轨、浅水、ped 密度(0..7)
m_iCentroidX/Y、m_iCoverDirectionsBitfield、m_iImmediateModeFlagsu8 ×4压缩质心;8 方向掩体位;即时模式开/闭标记
m_Struct44/8/3/9父出口边、所在分块号、特殊链接数(≤7)、特殊链接起始下标

注释写 m_iFirstVertexIndex 在游戏中为 15 位,但宏 NUM_BITS_FOR_1ST_VERTEX_INDEX 实际为 16(工具为 18) navmesh.h:119-123,以宏为准。标志分两类:随网格保存的 SMALL/LARGE/IS_PAVEMENT/IN_SHELTER/TOO_STEEP_TO_WALK_ON/IS_WATER,以及运行时临时位 OPEN/CLOSED/TESSELLATED_FRAGMENT/SWITCHED_OFF_FOR_AMBIENT_PEDS/DISABLED/DEGENERATE_CONNECTION_POLY/REPLACED_BY_TESSELLATION/…;首次访问时用 NAVMESHPOLY_VISITED_RESET_FLAGS_MASK 清掉搜索相关临时位 navmesh.h:502-535。每个多边形最多 16 个顶点,因此候选点上限 NUM_CLOSE_PTS_IN_POLY = 16×4+1 = 65 navmesh.h:543-556。

TAdjPoly 描述一条边 navmesh.h:420-456:第一个字含相邻 navmesh 查找下标(5 位)、相邻多边形号(15 位)、邻接类型(2 位:普通、低攀、高攀、下落 navmesh.h:196-206)、顶点周围与边外的可通行空间(各 5 位);第二个字保存细分前的原始 navmesh/多边形(用于反细分恢复)、邻接被禁用(攀爬失败后禁用)、边提供掩体、高落差、外边界边等位。TShortMinMax 以 s16 存放 0.25 m 分辨率的 AABB,可表示范围约 ±8191 m src/dev_ng/rage/framework/src/ai/navmesh/shortminmax.h:27-48。

2.4 层次导航(hierarchical nav)已在编译期关闭#

__HIERARCHICAL_NODES_ENABLED 定义为 0 navmesh.h:84,因此 aiNavNodesStore 不会创建、也不会注册为流式模块 src/dev_ng/game/pathserver/PathServer.cpp:4657-4671、PathServer.cpp:4699-4701;Hierarchical_FindPath 等全部编译为返回空/false 的桩 src/dev_ng/game/pathserver/PathServer_Hierarchical.cpp:22-28。CHierarchicalNavNode 与资源类 CHierarchicalNavData : pgBase 仍然定义在 navmesh.h 中 navmesh.h:2741-2748、navmesh.h:2933-2943,m_fHierarchicalNodesLoadProximity = 600 等变量也保留着 PathServer.cpp:319-320,但在发行配置下不起作用。

2.5 动态 navmesh(载具自带的 navmesh)#

部分载具模型带有自己的 navmesh(哪些模型有,由 navmeshes.img 中的数据决定 src/dev_ng/game/pathserver/PathServer_Store.cpp:43)。CModelInfoNavMeshRef 记录模型号、引用计数、唯一的“备份副本”指针与流式下标;CDynamicNavMeshStore 是这些条目的数组 src/dev_ng/rage/framework/src/ai/navmesh/navmeshstore.h:715-767。加载时按文件名哈希匹配载具模型 src/dev_ng/game/pathserver/PathServer_Store.cpp:18-44;RequestAndEvictMeshes 按引用计数以 STRFLAG_FORCE_LOAD|STRFLAG_PRIORITY_LOAD 请求,或在计数归零时移除 PathServer.cpp:1224-1245。GetNavMeshFromIndex 对动态段直接返回备份副本 pathserverbase.h:96-102;路径请求开始时会把该副本的矩阵设为请求创建时载具的矩阵,并把载具自身的动态障碍物设为非障碍 src/dev_ng/game/pathserver/PathServer_PathRequest.cpp:317-329。这表明同型号的所有载具共享一份网格,每次请求时重新设置矩阵;这种做法依赖“只有一个寻路线程”的前提(推断)。起点和终点不能落在两个不同的动态 navmesh 上 PathServer_PathRequest.cpp:309-314。

3. 生命周期与每帧流程#

3.1 初始化与会话#

时机调用作用
游戏核心初始化CPathServer::RegisterStreamingModule() src/dev_ng/game/Core/game.cpp:582读取 nav.dat,创建 aiNavMeshStore("NavMeshes", …),设置网格参数,加入 strStreamingEngine 模块表;-nonav 参数(__BANK)可关闭流式 PathServer.cpp:4582-4707
CScene::Init(gameSkeleton 注册 game.cpp:1831)CPathServer::Init(s_PathServerGameInterface, EMultiThreaded) src/dev_ng/game/scene/scene.cpp:581、scene.cpp:630初始化动态 navmesh 表、创建细分网格、初始化相邻偏移、启动寻路线程、创建即时模式优先队列(256),最后 Reset() PathServer.cpp:483-578
载具模型就绪后InitDynamicNavMeshesAfterVehSetup() game.cpp:1528建立动态 navmesh 与载具模型的对应
地图加载前 / 会话开始 / 会话结束InitBeforeMapLoaded、InitSession、ShutdownSession game.cpp:2142、game.cpp:2424、game.cpp:2600分别执行 Reset(true);置 ms_bGameInSession=true 后 Reset();置 false 后 Reset() PathServer.cpp:4094-4107
CScene::ShutdownCPathServer::Shutdown() scene.cpp:678关闭线程、卸载所有网格、释放细分网格与 store PathServer.cpp:1746-1775

Reset 依次持有 navmesh、即时访问、动态对象三把锁,清空所有请求槽;bLoadingNewMap 时卸载全部地图网格 PathServer.cpp:4160-4200。会话之外的保护:RequestPath 在 !ms_bGameInSession 时断言并返回 PATH_HANDLE_NULL PathServer.cpp:2110-2112;线程在 !m_bGameRunning || !ms_bGameInSession 时每秒检查一次并睡眠 src/dev_ng/game/pathserver/PathServerThread.cpp:1061-1067;m_bGameRunning 在第一次 Process() 时置位 PathServer.cpp:1076-1086。

3.2 每帧 CPathServer::Process#

Process() 由 CScene::Update(gameSkeleton 更新调用 game.cpp:2826)以焦点实体位置调用 scene.cpp:831-832、PathServer.cpp:1086;加载场景(CSceneLoader 第 3 阶段)也会以加载点调用一次 src/dev_ng/game/scene/streamer/SceneLoader.cpp:185-187。Process(const Vector3&) 依次执行 PathServer.cpp:872-1072:

  1. 把 NMR_GameplayOrigin 区域更新为当前原点和加载半径(886–890 行)。
  2. 原点 XY 位移超过 50 m 时立即触发下一次请求/驱逐,然后调用 RequestAndEvictMeshes(894–901 行)。
  3. 在 m_bCoverPointsBufferInUse 为假时复制 CCover 的包围区域,供线程抽取掩体点;处理 ped 生成屏蔽区(917–924 行)。
  4. __BANK 压力测试(929–965 行)。
  5. 每 2000 ms 检查一次“已完成但无人取回”的请求。注释说明这一步特意放在 LOCK_REQUESTS 之前,以避免与“先锁请求、再锁 navmesh”的路径形成死锁(969–982 行)。
  6. 在 LOCK_REQUESTS 下修补排队请求(§4.4,985–1053 行)。
  7. 若有延迟添加的脚本阻挡物:置 BLOCK_REQUESTS_ON_ADD_DYNAMIC_OBJECTS,强制中止当前请求,添加后清除标志(1059–1068 行)。

3.3 navmesh 流式:RequestAndEvictMeshes#

  • 频率与互斥:默认每 1000 ms 一次 PathServer.cpp:190、PathServer.cpp:1129-1132。到期后先置 BLOCK_REQUESTS_ON_REQUEST_AND_EVICT,使线程不再取新请求,再对 navmesh 数据锁 TryLock;拿不到就下一帧重试 PathServer.cpp:1137-1152。
  • 加载区域:最多 3 个——游戏原点、网络重生管理器、脚本 pathserverbase.h:606-613。原点半径 m_fNavMeshLoadProximity 默认 100 m,可被 nav.dat 覆盖 PathServer.cpp:319、PathServer.cpp:654-659。脚本用 ADD_NAVMESH_REQUIRED_REGION 添加区域 src/dev_ng/game/script/commands_path.cpp:1607、PathServer.cpp:1266-1268。
  • 切换角色时:player switch 激活且焦点离目的地 XY 超过 10 m 时(比较的是 XYMag2() > 100)暂停请求/驱逐,避免途中积压大量过期请求 PathServer.cpp:1180-1194。
  • 请求/驱逐算法:持 LOCK_STORE_LOADED_MESHES 调用 store 的 RequestAndEvict PathServer.cpp:1198-1205。aiMeshStore::RequestAndEvict 把所有区域之外的已加载网格标为不需要,卸载半径为“区域半径 + 网格半径 + 10 m”,形成滞回;再把每个区域内“区域半径 + 网格半径”范围内的网格标为需要 src/dev_ng/rage/framework/src/ai/navmesh/navmeshstore.h:314-420。
  • 结束时恢复计时器、解锁并清除阻塞标志 PathServer.cpp:1251-1260。

3.4 放置与移除网格:与寻路线程的握手#

  • aiNavMeshStore::PlaceResource 先调用 fwPathServer::ForceAbortCurrentPathRequest(),再依次持有 navmesh 数据锁和即时访问锁 navmeshstore.cpp:420-451;Remove 持同样两把锁,更新 m_LoadedMeshes,最后置 m_bFindCover_NavMeshesHaveChanged 并回调 NavMeshesHaveChanged navmeshstore.cpp:341-417,后者通知两个 ped 生成器重新开始 PathServer_Store.cpp:47-51。
  • ForceAbortCurrentPathRequest 置 m_bForceAbortCurrentRequest;若线程正在工作,主线程还会让出时间片 src/dev_ng/rage/framework/src/ai/navmesh/pathserverbase.cpp:210-222。A* 每次迭代都检查该标志,命中时以 PATH_ABORTED_DUE_TO_ANOTHER_THREAD 退出 src/dev_ng/game/pathserver/PathServer_PathSearch.cpp:1184-1196。Run 把这种请求重新标为 pending(不算完成),随后让出时间片,以便主线程尽快拿到锁 PathServerThread.cpp:1232-1236、PathServerThread.cpp:1543-1551。
  • 碎片整理:线程处理请求期间把两个 store 的 SetDefragmentCopyBlocked(true) PathServerThread.cpp:1086-1090、PathServerThread.cpp:1579-1582;游戏接口在存在任何 pending 请求时也报告“禁止碎片整理拷贝” PathServer_Store.cpp:188-196。原因是线程持有指向网格内部的裸指针。
  • 加载钩子:新加载的网格会套用脚本区域开关 PathServer_Store.cpp:54-58、硬编码的邻接修补(ApplyHacksToNewlyLoadedNavMesh,只作用于常规域) PathServer_Store.cpp:69-79,并处理地图交换(map swap/DLC)两侧网格的重新拼接 PathServer_Store.cpp:202。

4. 请求模型:槽位、句柄与轮询#

4.1 请求类型与容量#

所有请求都放在 CPathServer 的静态定长数组里,没有动态队列 src/dev_ng/game/pathserver/PathServer.h:913-928。类型枚举见 src/dev_ng/rage/framework/src/ai/navmesh/requests.h:27-46,容量见 src/dev_ng/rage/framework/src/ai/navmesh/pathserverthread.h:112-118。

类型槽数发起 API → 取结果 API说明
EPath(CPathRequest)32RequestPath → IsRequestResultReady + LockPathResult/UnlockPathResultAndClear最短路径、逃离(flee)、漫游(wander)三种搜索;结果最多 16 个路点(数组为 17) requests.h:492-494、datatypes.h:181
EGrid(CGridRequest)8RequestGrid → GetGridResultAndClear已停用:线程分派时断言 “request type not in use”,SampleWalkableGrid 的实现被 #if 0 掉 src/dev_ng/game/pathserver/PathServerThread.cpp:1154-1166、src/dev_ng/game/pathserver/PathServer_Grid.cpp:15-20
ELineOfSight24RequestLineOfSight → GetLineOfSightResult折线最多 16 个点 requests.h:238
EAudioProperties16RequestAudioProperties → GetAudioPropertiesResult位置下方(及周围)多边形的音频属性
EFloodFill8RequestClosestViaFloodFill 及其包装(最近车道节点、最近有遮蔽/无遮蔽多边形、脚下面积、附近人行道) → Get…ResultAndClear7 个子类型 requests.h:683-695
EClearArea4RequestClearArea → GetClearAreaResultAndClear找一块给定半径的空地
EClosestPosition4RequestClosestPosition → GetClosestPositionResultAndClear最多 16 个结果 requests.h:789

API 声明集中在 PathServer.h:214-319。每个请求基类都带 m_hHandle、m_iType、m_iCompletionCode、发出时间、Pending/Active/Complete/SlotEmpty/WasAborted/WaitingToAbort 六个位,一个逐槽 sysCriticalSectionToken,以及等待该请求的 ped(m_PedWaitingForThisRequest)和终点跟随实体(m_EntityEndPosition) requests.h:121-213。

4.2 发出路径请求:CPathServer::RequestPath#

客户端填写 TRequestPathStruct:起点/终点、参考向量与距离(逃离/漫游用)、可选的起始多边形、实体半径(默认 PATHSERVER_PED_RADIUS = 0.35)、64 位 PATH_FLAG_*、完成半径、最多 8 个影响球(influence sphere)、包含/排除对象(各 8 个)、最大坡度、起止点最大调整距离、导航域等 pathserverthread.h:40-109、datatypes.h:256、requests.h:118-119。RequestPath 的执行过程 src/dev_ng/game/pathserver/PathServer.cpp:2108-2456:

  1. LOCK_REQUESTS;从轮转起点 m_iNextPathIndexToStartFrom 开始找 IsReadyForUse()(m_bSlotEmpty && !m_bWaitingToAbort)的槽位 PathServer.cpp:2133-2147、requests.h:171-174。
  2. Clear() 后写入:等待者 ped、导航域、句柄 m_iNextHandle++、发出时间、起止点(同时保存一份“未调整”的副本)(2149–2181 行)。
  3. 把 PATH_FLAG_* 展开为几十个 bool 字段(2183–2261 行);漫游请求把参考向量压成单位 2D 向量(2207–2212 行)。
  4. 复制调用方给的影响球;若带 AVOID_POTENTIAL_EXPLOSIONS/AVOID_TEAR_GAS,再追加爆炸物/催泪弹影响球,总数不超过 MAX_NUM_INFLUENCE_SPHERES = 8(2316–2341 行,datatypes.h:422)。
  5. 按请求类型调整移动代价 ModifyMovementCosts(2407 行),置 m_bRequestPending = true(2414 行),解锁后 sysIpcSignalSema(m_PathRequestSema) 唤醒线程,返回句柄(2423–2433 行)。
  6. 32 个槽全部占用时返回 PATH_HANDLE_NULL(2442–2456 行)。句柄 0 保留为空句柄 datatypes.h:39-43。

4.3 轮询、读取与释放#

  • 轮询:IsRequestResultReady(h[, type]) 在 LOCK_REQUESTS 下按句柄线性扫描:m_bComplete 时返回 ERequest_Ready,否则 ERequest_NotReady,找不到句柄返回 ERequest_NotFound PathServer.cpp:3248-3279、PathServer.cpp:3412-3437、PathServer.cpp:3535-3536。
  • 读取:LockPathResult 返回指向槽内 m_PathPoints/m_WaypointFlags 的指针,拷贝一份 TPathResultInfo,返回值就是完成码;未完成时返回 PATH_STILL_PENDING,句柄无效时返回 PATH_ERROR_INVALID_HANDLE PathServer.cpp:3543-3614。名字里虽有 “Lock”,返回时并不持有任何锁;指针之所以有效,是因为槽位在客户端释放前不会被复用。
  • 释放:UnlockPathResultAndClear 就是 CancelRequestInternal(h, false) PathServer.cpp:3617-3627;CancelRequest 是 CancelRequestInternal(h, true),额外把完成码写成 PATH_CANCELLED。内部在 LOCK_REQUESTS 和槽位临界区下调用 Reset(),把槽恢复为空闲 PathServer.cpp:4269-4307。其他请求类型的取结果函数在拿到结果的同时 Reset() 槽位,即使函数名不带 AndClear 也是如此(例如 GetLineOfSightResult、GetAudioPropertiesResult) PathServer.cpp:3692-3699、PathServer.cpp:3813。
  • 泄漏保护:已完成但超过 30000 ms 没有取回的请求会触发断言并被强制取消 src/dev_ng/rage/framework/src/ai/navmesh/requests.cpp:15、PathServerThread.cpp:890-932;该检查每 2000 ms 在主线程运行一次 PathServer.cpp:165;游戏暂停时把所有请求的发出时间重置为当前时间 PathServerThread.cpp:854-886。TRACK_LEAKED_PATHS 调试开关会遍历任务池,查找持有该句柄的 CTaskMoveFollowNavMesh PathServerThread.cpp:902-915。
  • 错误码:PATH_FOUND(0)、NOT_FOUND、TIMED_OUT、REACHED_MAX_PATH_POLYS、INVALID_COORDINATES、NAVMESH_NOT_LOADED、ENDPOINTS_INSIDE_OBJECTS、NO_SURFACE_AT_COORDINATES、RAN_OUT_OF_PATH_STACK_SPACE、RAN_OUT_VISITED_POLYS_SPACE、CANCELLED、ERROR_INVALID_HANDLE、NO_POINTS、STILL_PENDING、ABORTED_DUE_TO_ANOTHER_THREAD,以及层次寻路专用的两个 datatypes.h:49-70。

4.4 主线程修补排队请求,并与 AI 时间切片联动#

Process() 在 LOCK_REQUESTS 下对路径槽做两件事 PathServer.cpp:985-1053:

  • 取消等待者的时间切片:对每个已完成且非空的槽,给 m_PedWaitingForThisRequest 置复位标志 CPED_RESET_FLAG_WaitingForCompletedPathRequest(994–1004 行)。CPedAILodManager 看到该标志就给这个 ped 一次完整 AI 更新,即使它处于低 LOD 时间切片模式 src/dev_ng/game/Peds/PedIntelligence/PedAILodManager.cpp:736-740。注释说明:只对“已完成”而非“排队中”的请求这样做,是为了避免大量等路 ped 同时失去时间切片(988–992 行)。
  • 刷新尚未开始处理的请求:对 pending 且未 active、带 m_bKeepUpdatingPedStartPosition 的槽,用 ped 当前位置重写起点。若 ped 正在执行 TASK_MOVE_FOLLOW_NAVMESH 且请求带前视距离,则改用 CTaskNavBase::PathRequest_DistanceAheadCalculation 的结果;若 ped 的 CNavMeshTracker 是最新的,就直接使用它记录的位置与多边形,省去线程端的起点查找;另有一段“防止被动态对象推到薄墙另一侧”的修正(1006–1045 行)。带 m_EntityEndPosition 的槽,每帧把终点更新为该实体位置(1047–1048 行)。
  • 设置方:CTaskMoveFollowNavMesh::InitPathRequestPathStruct 在非退出载具、前视距离较小时设 PATH_FLAG_KEEP_UPDATING_PED_START_POSITION,并把活动任务的目标实体设为 m_pPositionEntity src/dev_ng/game/task/Movement/TaskNavMesh.cpp:748-757;RequestPath 把它们写入槽位 PathServer.cpp:2180-2189。

4.5 请求槽状态机#

证据:入队 PathServer.cpp:2160-2163、PathServer.cpp:2414;出队 PathServerThread.cpp:1759-1762;完成或回退 PathServerThread.cpp:1227-1248;取消/释放 Reset() requests.h:134-142、PathServer.cpp:4288-4306;超时 PathServerThread.cpp:896-931。

取消一个正在处理的请求:CancelRequestInternal 先 Reset()(这会把 m_bRequestActive 清零),然后才检查 if(m_bRequestActive) 并据此置 m_bWaitingToAbort PathServer.cpp:4292-4302。按源码顺序,这个分支永远不会执行,因此 IsReadyForUse() 中对 m_bWaitingToAbort 的保护在这条路径上不起作用。实际的中止依赖另外两处检查:线程在开始前和 A* 每次迭代中检查 PATH_CANCELLED PathServerThread.cpp:1195-1198、PathServer_PathSearch.cpp:1197-1201,string pulling 中也有同样的检查 src/dev_ng/game/pathserver/PathServer_PathStringPull.cpp:328-329。见“未决问题”。

5. 寻路线程 CPathServerThread#

5.1 线程创建#

CPathServerThread::Init 先创建请求信号量,再以 PathServerThreadFunc 为入口、32768 字节栈(注释说明递归算法需要较大栈空间)、PRIO_PATHSERVER_THREAD 优先级创建名为 "PathServerThread" 的线程 src/dev_ng/game/pathserver/PathServerThread.cpp:344-362、pathserverthread.h:145-147。该优先级就是 PRIO_NORMAL src/dev_ng/game/system/ThreadPriorities.h:10。处理器序号由调用方传入(游戏使用默认值 0),但 Durango 上被强制设为 5 PathServerThread.cpp:334-338。A* 优先队列在这里分配,容量 MAX_PATH_STACK_SIZE = 2048 PathServerThread.cpp:387-390、datatypes.h:821。非 GTA_ENGINE 构建(离线工具)改用 Win32 CreateThread 和 CRITICAL_SECTION PathServerThread.cpp:364-385。EProcessingMode::ESingleThreaded 的注释称 Process() 会直接调用 Run() PathServer.h:79-84,但游戏只以 EMultiThreaded 初始化 scene.cpp:630。线程在全局线程表中的位置见 threading-and-render-thread。

5.2 Run 主循环#

CPathServerThread::Run PathServerThread.cpp:1049-1593 每轮依次执行:

  1. 不在会话中时每秒检查一次并睡眠(1061–1067 行)。
  2. 取请求:ms_bProcessAllPendingPathsAtOnce(默认 true PathServer.cpp:390-391)时先直接 GetNextRequest(),没有请求再 WaitForNextRequest()(1070–1082 行)。WaitForNextRequest 在游戏暂停时每 2 s 醒来并重置请求计时;否则 sysIpcWaitSemaTimed(sema, 50ms) 后调用 GetNextRequest(),返回值可能为空,空的一轮正好用来做 housekeeping PathServerThread.cpp:550-610。
  3. 禁止 navmesh store 的碎片整理拷贝(1086–1090 行);只要 ms_iBlockRequestsFlags 非零就每 1 ms 自旋等待一次(主线程正在请求/驱逐或添加脚本阻挡物)(1093–1101 行)。
  4. LOCK_NAVMESH_DATA,置 m_bPathServerThreadIsActive,执行 DoHousekeeping()(1104–1117 行)。
  5. 按 m_iType 分派到 ProcessPathRequest/ProcessLineOfSightRequest/ProcessAudioPropertiesRequest/ProcessFloodFillRequest/ProcessClearAreaRequest/ProcessClosestPositionRequest;完成码默认为 PATH_NOT_FOUND,在槽位临界区内写回 Active=0、Complete=1(被中止的请求改回 pending)(1145–1510 行)。
  6. 路径请求结束后立即 DetessellateAllPolys() 撤销本次细分,并把为大半径实体扩大的动态对象包围盒缩回(1250–1276 行)。
  7. 处理强制中止(1543–1551 行)与时间戳溢出(1563–1572 行),然后解锁并恢复碎片整理(1575–1582 行)。

5.3 调度策略#

  • 类型轮转:从上次处理的类型的下一个开始,依次尝试 7 种请求,取到第一个就返回 PathServerThread.cpp:1653-1695。只要有 pending 的脚本路径、任务 ped 路径或高优先级路径,就把“上次类型”重置,使本轮从路径请求开始(1642–1651 行)。
  • 路径请求内部:优先级为 最大句柄 − 本句柄,即越早发出越优先;m_bHighPrioRoute 再 ×2,任务 ped 或脚本路径再 ×2;取最高者,在 LOCK_REQUESTS 下改为 Active PathServerThread.cpp:1712-1765。CTaskMoveFollowNavMesh 对任务 ped 设 PATH_FLAG_MISSION_PED TaskNavMesh.cpp:616-619;注释提到玩家及其小队的路线使用高优先级 TaskNavMesh.cpp:760。

5.4 Housekeeping(后台任务)#

任务频率实现
增删动态对象、执行延迟的攀爬禁用、把主线程写入的新包围盒拷入m_iThreadHousekeepingFrequencyInMillisecs = 1000 msPathServerThread.cpp:618-644、PathServer.cpp:162
分时抽取掩体点;每 4000 ms 顺带抽取一次水边600 ms / 4000 msPathServerThread.cpp:648-651、PathServer.cpp:424-426、src/dev_ng/game/pathserver/PathServer_Cover.cpp:371-448
ped 生成坐标:在环境 ped(CPathServerAmbientPedGen)与多人出生点(CPathServerOpponentPedGen)之间轮流处理,空闲的一方让给另一方;需要瞬间填满人口时优先环境 pedPC/Durango/Orbis 每 30 ms,其他平台每 500 msPathServerThread.cpp:659-720、PathServer.cpp:216-221

除此之外,每个路径请求开始时也会调用 RemoveAndAddObjects() 和 UpdateAllDynamicObjectsPositions(),把动态对象位置“冻结”在这一刻 src/dev_ng/game/pathserver/PathServer_PathRequest.cpp:159-162。

6. 路径请求流水线#

CPathServerThread::ProcessPathRequest src/dev_ng/game/pathserver/PathServer_PathRequest.cpp:14-634 的阶段如下。

6.1 预处理#

  1. 没有 navmesh 时直接连线:起点所在块不在 navmesh 镜像中(且不是动态 navmesh 路线),或调试开关 m_bDisablePathFinding 打开时,直接返回两点路径;逃离和漫游请求的第二点沿逃离方向或参考向量偏移参考距离(59–94 行)。
  2. 递增 A* 时间戳(102 行);实体半径至少取 0.35(109–111 行);按 REDUCE_OBJECT_BBOXES 和实体半径设置动态对象 6 个平面的测试 epsilon(119–140 行)。
  3. CalculateSearchExtents。对最短路径请求,若起点不在搜索范围内则返回 PATH_NOT_FOUND(144–157 行;注释说的是“目标不在范围内”,代码实际检查的是 m_vPathStart)。
  4. 冻结动态对象;可选建立网格单元缓存(159–171 行)。实体半径大于 0.35 时,临时把所有动态对象的 minmax 扩大相应差值(179–187 行),处理完后在 Run 中缩回。
  5. 起止点解析:把起点/终点从动态对象内部推出(FindPositionClearOfDynamicObjects),并找到所在多边形;失败时返回 PATH_ENDPOINTS_INSIDE_OBJECTS 或 PATH_INVALID_COORDINATES(230–299 行)。动态 navmesh 路线的处理见 §2.5(317–329 行)。
  6. 终点所在网格未加载(PATH_FLAG_SELECT_CLOSEST_LOADED_NAVMESH_TO_TARGET):找离终点最近的已加载网格,取其边界上离终点最近的点,再在 250 m 内找可站立位置作为新终点;完成半径设为 2000,并置 m_bWentFarAsPossibleTowardsTarget。如果此时主线程正在请求/驱逐,返回 PATH_ABORTED_DUE_TO_ANOTHER_THREAD(335–401 行)。
  7. 带 DONT_AVOID_DYNAMIC_OBJECTS 的请求在本次处理期间把全局避障模式切为 ENoObjectAvoidance,结束时恢复(408–417 行、474–488 行、588 行)。

6.2 A* 搜索:FindPath#

搜索节点是“多边形 + 多边形内选中的候选点”,不是顶点图。

  • 短路径快速返回:在没有影响球、空间/掩体偏好,且不是逃离或漫游时,若 (end-start).Mag2() < 10、高差小于 5 m,就先做一次带半径的动态对象 LOS 和 navmesh LOS;两者都通过则直接返回两点路径 PathServer_PathSearch.cpp:909-969。变量名叫 fEarlyOutMaxDist = 10,但比较的是平方距离,等价于直线距离小于约 3.16 m。
  • 主循环:起点多边形以初始代价入堆(1102 行),之后 RemoveTop 直到堆空(1129 行);已被细分替换或已禁用的多边形跳过(1135–1136 行);每轮检查强制中止和取消(1192–1201 行);用 OnFirstVisitingPoly 惰性重置多边形状态(1213–1217 行)。可选的“长搜索中让出 CPU”逻辑受 m_bSleepPathServerThreadOnLongPathRequests 控制,该开关默认 false(64 次迭代检查一次,阈值 8 ms) PathServer_PathSearch.cpp:1122、PathServer_PathSearch.cpp:1150-1182、PathServer.cpp:169-170、PathServerThread.cpp:106。
  • 访问标记:发行构建用 per-poly 的 u16 A* 时间戳。首次遇到某个多边形时清除临时位和父指针,因此每次请求不必遍历所有多边形来清零 PathServerThread.h:611-654。时间戳上限 TIMESTAMP_MAX_VALUE = 0xBFFF,比 u16 最大值少 16384 作为余量,溢出在请求结束后统一处理 pathserverbase.h:323、PathServerThread.cpp:1563-1572。__DEV 构建还可以用最多 8192 项的“已访问列表”,或两种方式对拍校验 PathServerThread.h:547-604、pathserverthread.h:29。
  • 启发式与候选点:VisitPoly_ShortestPath 在相邻多边形的候选点(顶点、边点、质心等,最多 65 个)中选离目标最近的一个,fCost 取该点到目标的欧氏距离,即 h。存在动态对象时改用带 LOS 的选点函数;LOS 被挡则返回 eMustTessellateThisPoly PathServer_PathSearch.cpp:3095-3189、PathServerThread.h:486-491。选中点的编号存在 TNavMeshPoly::m_iPointEnum。
  • 代价函数(最短路径):f = g_parent × 5 + d_segment × fPenaltyForAdjPoly + h × 5,两个系数分别为 ms_fDefaultDistanceTravelledMultiplier 与 ms_fDefaultCostFromTargetMultiplier,默认都是 5 PathServer_PathSearch.cpp:1984-1987、PathServer_PathSearch.cpp:17-18。漫游请求在父代价上累加“段长 ×(惩罚 + 多边形代价)+ 父距离”,并惩罚走向 ped 密度更低的多边形(1941–1960 行);逃离请求另有一套(1962–1976 行);PREFER_DOWNHILL 对上坡加代价(1947–1954 行等)。不同邻接类型(攀爬/下落等)先经 IsThisAdjacencyTypeOk 过滤,再由 GetAdjacencyTypePenalty 计入惩罚 PathServer_PathSearch.cpp:2969-3003。
  • 开放表操作:已在开放表中的多边形用 FindNode + DecreaseKey 更新,否则 Insert;堆满时返回 PATH_RAN_OUT_OF_PATH_STACK_SPACE PathServer_PathSearch.cpp:2159-2255。
  • 特殊链接:多边形上若有梯子或攀爬物体链接,就额外沿链接扩展到目标多边形(2269–2310 行起);扩展完后清 OPEN、置 CLOSED(2599–2603 行)。
  • 回溯:沿 m_PathParentPoly 回溯得到多边形序列,长度上限 MAX_PATH_POLYS = 512,超出返回 PATH_REACHED_MAX_PATH_POLYS PathServer_PathSearch.cpp:2910-2916、requests.h:284。搜索中的所有工作变量都收在 TPathFindVars 里;注释说明这样做是为了便于将来移植到 SPU pathserverthread.h:155-221。

6.3 默认移动代价#

CPathFindMovementCosts 的默认值 PathServer_PathSearch.cpp:17-39(均为 bank_*,可在 RAG 中调整);每个请求还会经 ModifyMovementCosts 按漫游/逃离做修改 src/dev_ng/game/pathserver/PathServer_PathMisc.cpp:1915:

项默认项默认
目标距离乘子 / 已行距离乘子5 / 5非直线移动惩罚1
高攀 / 低攀800 / 400下落 / 每米下落100 / 5
爬梯 / 每米爬梯50 / 5入水 / 出水 / 在水中50 / 25 / 20
每米攀爬物体10漫游走到零密度多边形100
走上陡坡100无方向掩体 / 每缺一个掩体位500 / 75
走向更低 ped 密度(每级)10避开铁轨16000
逃离折返代价 / 柔和版 / 折返乘子400 / 200 / 0.25

6.4 string pulling 与后处理#

  • RefinePathAndCreateNodes:注释明确称之为 string pulling,即把 A* 得到的多边形链和选点拉直成少量互相可见的直线段 src/dev_ng/game/pathserver/PathServer_PathStringPull.cpp:14-20。实现是贪心推进:从当前锚点出发,对后续点依次做 TestNavMeshLOS 与 TestDynamicObjectLOS PathServer_PathStringPull.cpp:317-369。经由非标准邻接(攀爬/下落)到达的多边形必须在入口边中点和多边形内各放一个点,以保证动作位置正确;攀爬点被动态对象挡住时整条路径失败(特殊链接除外) PathServer_PathStringPull.cpp:387-407。调用时传入的上限是 MAX_NUM_PATH_POINTS(16) PathServer_PathRequest.cpp:484-490。
  • 后处理(PathServer_PathRequest.cpp:513-540,开关默认值见 PathServer.cpp:191-198):
    1. MinimisePathDistance(默认开):对每三个相邻点,在中间点所在多边形内移动它,使两段总长最短且保持可见 PathServer_PathMisc.cpp:649-659。
    2. PullPathOutFromEdges(默认开,并带一次 string pull):把路点从多边形边缘往内拉,避免贴墙 PathServer_PathMisc.cpp:913。
    3. AddWaypointsToPreserveHeightChanges:仅在 PRESERVE_SLOPE_INFO_IN_PATH 时插入保留坡度变化的点;CTaskMoveFollowNavMesh 总是设置该标志 TaskNavMesh.cpp:580。
    4. CalcPathBezierControlPoints:在 CUT_SHARP_CORNERS 且 ms_bSmoothRoutesUsingSplines 时,为急弯生成贝塞尔控制点,并用若干线段检测是否通畅 PathServer_PathMisc.cpp:1154-1167。任务侧路线为每个点保存 spline 编号和 t 值 src/dev_ng/game/task/Movement/TaskNavBase.h:109-116,并用 CalcPathSplineCtrlPts 计算控制点 PathServer.h:1061。
    5. 旧的 SmoothPath(切角并插点)仍有定义 PathServer_PathMisc.cpp:1326-1339,但在 game/pathserver 内没有调用者,是死代码。

6.5 结果语义#

  • 最后一点不在原始终点 0.5 m 内时:若路点未用满且最后一点就是调整后的终点,标记 WAYPOINT_FLAG_ADJUSTED_TARGET_POS;若路点已用满且不是因为使用了完成半径,则 m_bLastRoutePointIsTarget = false,提示客户端“还要再请求下一段” PathServer_PathRequest.cpp:547-570。
  • 清掉最后一个路点上的攀爬/下落/梯子标志(572–577 行)。
  • 路点动作标志:高攀、下落、低攀、开门、上梯、下梯、攀爬物体、终点调整、层次寻路、室内、水面、窄入/窄出 datatypes.h:78-99。

6.6 路径标志(节选)#

PATH_FLAG_* 是 64 位标志,共 44 个 src/dev_ng/rage/framework/src/ai/navmesh/pathserverbase.h:278-321:

类别标志
搜索类型FLEE_TARGET、WANDER(二者互斥 PathServer.cpp:2234)、SOFTER_FLEE_HEURISTICS、FLEE_NEVER_END_IN_WATER
地表偏好/限制PREFER_PAVEMENTS、NEVER_LEAVE_PAVEMENTS、NEVER_ENTER_WATER、NEVER_START_IN_WATER、NEVER_LEAVE_WATER、NEVER_LEAVE_DEEP_WATER、PREFER_DOWNHILL、ALLOW_TO_NAVIGATE_UP_STEEP_POLYGONS、AVOID_TRAIN_TRACKS、FAVOUR_ENCLOSED_SPACES、USE_DIRECTIONAL_COVER、COVERFINDER
动作限制NEVER_CLIMB_OVER_STUFF、NEVER_DROP_FROM_HEIGHT、NEVER_USE_LADDERS、MAY_USE_FATAL_DROPS、IF_NOT_ON_PAVEMENT_ALLOW_DROPS_AND_CLIMBS
动态对象DONT_AVOID_DYNAMIC_OBJECTS、REDUCE_OBJECT_BBOXES、IGNORE_NON_SIGNIFICANT_OBJECTS、ALLOW_TO_PUSH_VEHICLE_DOORS_CLOSED、DONT_AVOID_FIRE、DEACTIVATE_OBJECTS_IF_CANT_RESOLVE_ENDPOINTS(任务侧注释称 pathserver 尚未启用它 TaskNavMesh.cpp:582-584)
危险规避AVOID_POTENTIAL_EXPLOSIONS、AVOID_TEAR_GAS
范围与结果SELECT_CLOSEST_LOADED_NAVMESH_TO_TARGET、USE_LARGER_SEARCH_EXTENTS、DONT_LIMIT_SEARCH_EXTENTS、ENSURE_LOS_BEFORE_ENDING、PRESERVE_SLOPE_INFO_IN_PATH、CUT_SHARP_CORNERS、PULL_FROM_EDGE_EXTRA、RANDOMISE_POINTS、USE_BEST_ALTERNATE_ROUTE_IF_NONE_FOUND、EXPAND_START_END_TESSELLATION_RADIUS
调度与特殊SCRIPTED_ROUTE、MISSION_PED、HIGH_PRIO_ROUTE、DYNAMIC_NAVMESH_ROUTE、KEEP_UPDATING_PED_START_POSITION

7. 动态障碍物与运行时细分(tessellation)#

静态 navmesh 不包含运行时的车辆、可移动物体、门和火焰。pathserver 把这些对象作为带朝向包围盒的“动态对象”维护,寻路时遇到被挡住的多边形就在线细分,绕开障碍。

7.1 数据结构#

  • TDynamicObject src/dev_ng/rage/framework/src/ai/navmesh/datatypes.h:599-773:实体指针、车门指针或脚本线程号三选一的 union;m_Bounds 与 m_NewBounds 两份包围盒(双缓冲);4 个 XY 顶点;全局链表与网格单元双向链表指针;TShortMinMax 粗剔除盒;以及 IsOpenable/IsDoor/IsClimbable/IsPushable/IsVehicle/IsFire/IsUserAddedBlockingObject/ScriptedBlockingObject/VehicleDoor/IsSignificant/ForceReducedBoundingBox/… 等标志,还有用 3 位表示“对哪类路径(漫游/最短/逃离)生效”的阻挡标志 datatypes.h:619-626。TDynObjBounds 存原点、上下 Z 和 6 个平面(4 个侧面加顶/底) datatypes.h:554-592。
  • 存储:线程内定长数组 m_DynamicObjectsStore 加链表头 m_pFirstDynamicObject src/dev_ng/game/pathserver/PathServerThread.h:721-722,容量 PATHSERVER_MAX_NUM_DYNAMIC_OBJECTS = 640 pathserverthread.h:37。
  • 空间索引:CDynamicObjectsContainer 把对象放进最多 64 个网格单元;每个路径请求可建立 32×32、256 项的 CGridCellsCache,用于快速查询“与某区域相交的单元” pathserverbase.h:334-461。该缓存的开关 ms_bUseGridCellCache 默认关闭 PathServer.cpp:202。
  • 相关阈值:可推动物体最大质量 500,“重要”小物体最小质量 125、最小体积 0.33 src/dev_ng/game/pathserver/PathServerThread.cpp:50-52。

7.2 注册与更新(游戏侧接入点)#

  • 筛选:CPathServerGta::MaybeAddDynamicObject 先调用 ShouldAvoidObject,通过后才 AddDynamicObject src/dev_ng/game/pathserver/PathServer_Objects.cpp:1613-1625。只考虑有物理实例且开启碰撞的物体和载具;以下情况不避让:已烘焙进 navmesh 的、美术标为“ped 不避让”的(门除外)、武器、带布料的 frag、过小的物体、拾取物、附着物等 PathServer_Objects.cpp:3561-3629。
  • 加入/移除调用点:实体显隐切换 src/dev_ng/game/scene/Entity.cpp:4410-4424;之前加入失败(WasUnableToAddToPathServer)的对象在物理更新或世界处理中重试 src/dev_ng/game/physics/physics.cpp:2877-2882、src/dev_ng/game/scene/world/GameWorld.cpp:2239-2242;从世界移除时一并移除 GameWorld.cpp:945-948。
  • 位置更新:CPhysical 在矩阵变化后(物理多时间片时只在最后一片)调用 UpdateDynamicObject src/dev_ng/game/scene/Physical.cpp:555-559、Physical.cpp:1785-1792。UpdateDynamicObject 在主线程上不加锁地写 m_NewBounds:若线程正在拷贝该对象的包围盒,强制更新时就 sysIpcYield 自旋等待,否则直接返回、留待下次更新;位移平方小于 0.1(约 0.32 m)且航向/俯仰/横滚变化都小于阈值(门 2°,其他 25.8°)时跳过;否则置 m_bCurrentlyUpdatingNewBounds,sys_lwsync,计算包围盒,置 m_bNewBounds,再 sys_lwsync 并清除“正在更新”标志 src/dev_ng/game/pathserver/PathServer_Objects.cpp:2386-2455。阻挡物更新走同样的握手(阈值为 0.1 m / 4°) PathServer_Objects.cpp:2252-2275。线程在 LOCK_DYNAMIC_OBJECTS_DATA 下扫描:遇到正在被主线程更新的对象就跳过(只计数),否则同样用 lwsync 包围拷贝 m_NewBounds → m_Bounds,标记需要重新插入网格单元,并判断是否越出世界范围 PathServer_Objects.cpp:1373-1430。
  • 其他来源:载具车门 PathServer_Objects.cpp:3215-3328;火焰 PathServer_Objects.cpp:2844-2965;通用阻挡物 AddBlockingObject/UpdateBlockingObject/RemoveBlockingObject PathServer.h:371-376;脚本阻挡物(ADD_NAVMESH_BLOCKING_OBJECT src/dev_ng/game/script/commands_path.cpp:1617),它们先进入延迟队列,在 Process() 中阻塞线程后统一加入,并可能为脚本腾出槽位 PathServer_Objects.cpp:1718-1721、PathServer_Objects.cpp:1813-1991。导航任务在乘坐/接近载具时也会主动加入或更新该载具 src/dev_ng/game/task/Movement/TaskNavBase.cpp:1215-1249。
  • 动态对象 LOS:TestDynamicObjectLOS 及带半径版本(两条相距实体半径的平行线)PathServerThread.h:225-230、PathServer_Objects.cpp:3910-3968。平面测试用的 epsilon 按对象类型(车门/火焰不缩小)选择 PathServerThread.h:701-715。

7.3 运行时细分#

  • 避障模式默认是 EPolyTessellation;另有“按对象裁剪多边形”的 EPolyClipping PathServer.cpp:182、PathServer.h:85-90,但注释说明裁剪方案在游戏中未使用,代价太高 pathserverthread.h:120-124。
  • 细分网格:初始化时创建唯一的 CNavMesh(索引 10100),容量 4096 个多边形、每个 3 个顶点,外加记录“碎片来自哪个原始多边形”的 TTessInfo 表 src/dev_ng/rage/framework/src/ai/navmesh/tessellation.cpp:20-74、navmesh.cpp:40。
  • 触发:A* 访问到 LOS 被动态对象挡住的多边形时(eMustTessellateThisPoly),调用 TessellateNavMeshPolyAndCreateDegenerateConnections PathServer_PathSearch.cpp:1824、PathServer_Objects.cpp:574。按头文件注释,它把多边形切成三角碎片,并生成零面积的“退化连接多边形”,用来二分外边、改善绕障效果 PathServerThread.h:283-293。原多边形被标为 REPLACED_BY_TESSELLATION,此后在搜索中跳过;邻接里保留的 Original* 字段供恢复使用 navmesh.h:521-528、PathServer_PathSearch.cpp:1135-1136。起点和终点附近另有预细分逻辑 PathServer_PathMisc.cpp:103、PathServer_PathMisc.cpp:313。
  • 撤销:细分只在单个请求内有效。每个路径请求结束后 DetessellateAllPolys() 遍历在用碎片,把原多边形和邻接恢复原状 PathServerThread.cpp:1250-1267、tessellation.cpp:83-130;处理路径请求前还断言细分多边形数为 0 PathServerThread.cpp:1179。

8. 其他查询与后台服务#

8.1 非路径的异步请求#

  • LOS:对折线的每一段先找起止多边形,做 navmesh LOS(可附加“不跨水面/最大坡度边界”条件),需要时再做动态对象 LOS;结果按段记录,可在第一次失败时停止 PathServerThread.cpp:2654-2786。客户端包括 CNavmeshLineOfSightHelper src/dev_ng/game/task/System/TaskHelpers.h:1607、TaskHelpers.cpp:11152 和 CCombatManager src/dev_ng/game/task/Combat/CombatManager.cpp:1463-1511。
  • 音频属性:音频环境组请求声源所在多边形及周边(最多 24 个)的音频属性、面积与质心 src/dev_ng/game/audio/environment/environmentgroup.cpp:387-517、requests.h:548。
  • 洪泛:通用的洪泛搜索加每种子类型的 ShouldVisitPoly/ShouldEndOnThisPoly 回调 PathServerThread.h:334-352。例如 CTaskMoveGetOntoMainNavMesh 用“脚下面积”判断 ped 是否困在孤立小块上 TaskNavBase.cpp:6590;TaskHelpers 中查找最近的车道节点 TaskHelpers.cpp:18105。
  • 空地 / 最近位置:分别由 CNavmeshClearAreaHelper、CNavmeshClosestPositionHelper 封装 TaskHelpers.h:1642、TaskHelpers.h:1682、TaskHelpers.cpp:11238、TaskHelpers.cpp:11325。通用路径请求封装为 CNavmeshRouteSearchHelper TaskHelpers.h:892、TaskHelpers.cpp:1704。

8.2 即时模式(immediate mode)查询#

部分查询必须同帧得到结果,于是直接在主线程上读 navmesh,不经过请求队列:

  • 锁:只持 m_NavMeshImmediateAccessCriticalSectionToken,不持 navmesh 数据锁,因此可以与寻路线程并发读取;流式放置/移除则必须同时持有两把锁 pathserverbase.h:43-46、src/dev_ng/game/pathserver/PathServer_Immediate.cpp:31-51、navmeshstore.cpp:346-352。
  • 与线程的隔离:即时模式用多边形上独立的 m_iImmediateModeFlags(开/闭)、独立的 256 项优先队列和访问列表,不碰线程的 A* 时间戳 navmesh.h:558-566、PathServer.h:672-684、pathserverbase.h:330。头文件注释也提醒,这个 8 位字段写入时可能有线程访问问题 navmesh.h:808-810。
  • TestShortLineOfSightImmediate:只接受短于 16 m 的线段,不考虑动态对象 PathServer_Immediate.cpp:31-63、pathserverbase.h:329。
  • GetClosestPositionForPed:需要“避开对象”时额外持 LOCK_DYNAMIC_OBJECTS_DATA;参数 bFailIfNoThreadAccess 实际未使用(是阻塞等锁,不会失败返回);结果 Z 会加上 ped 根节点离地高度 src/dev_ng/game/pathserver/PathServer_Ped.cpp:1515-1563;过滤条件(仅人行道、非孤立、非室内、非水、联网出生候选、是否有遮蔽……)见回调 PathServer_Ped.cpp:1567-1589。派遣系统等直接调用它 src/dev_ng/game/game/Dispatch/DispatchHelpers.cpp:2603。

8.3 ped 的 navmesh 追踪#

每个 ped 有一个 CNavMeshTrackedObject。CGameWorld 每帧在 “Ped ai” 计时段之前调用 CPathServer::UpdatePedTracking() GameWorld.cpp:2084-2092。该函数持即时访问锁遍历 ped 池;车内的非玩家 ped(未携带便携拾取物)直接置为无效,其余在位移超过阈值时重新扫描所在多边形 PathServer_Ped.cpp:2178-2219。网络克隆 ped 单独更新 src/dev_ng/game/network/Objects/Entities/NetObjPed.cpp:6791。导航任务用追踪器提供的位置和多边形直接作为请求起点 TaskNavMesh.cpp:694-697。

8.4 掩体点、水边、ped 生成与区域开关#

  • 掩体点:线程把 navmesh 四叉树中的掩体点分时抽取到 128 项的 m_CoverPointsBuffer;主线程的 CCover::Update 调用 AddCoverPoints,每帧从缓冲尾部取一部分 PathServer.h:755-763、pathserverbase.h:590-598、src/dev_ng/game/task/Combat/Cover/cover.cpp:1646、PathServer_Cover.cpp:18-40。双方只用普通 bool m_bCoverPointsBufferInUse 加 sysIpcYield 互斥,注释自己也承认应改为临界区 PathServer_Cover.cpp:384-395。掩体点的消费侧见 task-catalogue(Combat/Cover)。
  • 水边:与掩体点在同一时间片中抽取,最多 16 个点,供音频使用 PathServer.h:177-186、PathServer_Cover.cpp:399-422。
  • 环境 ped 生成:CPathServerAmbientPedGen::FindPedGenCoordsBackgroundTask 在线程中遍历已加载网格,按所需密度生成候选坐标(设计意图见 PathServer.h:55-58)(上限 MAX_POTENTIAL_PEDGEN_COORDS = 128) PathServer_Ped.cpp:338、src/dev_ng/rage/framework/src/ai/navmesh/pedgen.h:18;CPedPopulation 在结果就绪时取走 src/dev_ng/game/Peds/pedpopulation.cpp:1118-1120。生成屏蔽区最多 64 个 pedgen.h:184,由 AddPedGenBlockedArea 添加(惊吓事件会用到,见 events-and-responses) PathServer_Ped.cpp:480。人口侧逻辑见 ped-intelligence-and-population。
  • 多人出生点搜索:脚本发起 CPathServerOpponentPedGen::StartSearch(圆柱或斜向区域),线程分时搜索,最多 256 个坐标 src/dev_ng/game/script/commands_ped.cpp:13035-13040、PathServer_Ped.cpp:670-850、src/dev_ng/game/pathserver/PathServer_Ped.h:143。
  • 区域开关:SwitchPedsOn/OffInArea(控制生成与漫游)、DisableNavMeshInArea/ReEnableNavMeshInArea;最多 64 条 TPathRegionSwitch,记录创建脚本的 UID 以便任务结束时清理;新加载的网格会重放这些开关 PathServer.h:350-369、pathserverbase.h:469-483、PathServer_Ped.cpp:1190-1315。对应脚本命令为 SET_PED_PATHS_IN_AREA、DISABLE_NAVMESH_IN_AREA commands_path.cpp:1551、commands_path.cpp:1610。
  • 禁用攀爬:ped 反复无法完成路线上的攀爬时,可调用 DeferredDisableClimbAtPosition,在线程下次 housekeeping 时禁用该邻接(队列最多 80 项);梯子拥堵由 CBlockedLadders 管理(最多 8 架) PathServer.h:378-386、PathServerThread.h:16、PathServerThread.h:29-54。

9. 线程安全与锁#

游戏构建使用框架层定义的五个全局 sysCriticalSectionToken,以及成对的作用域宏 pathserverbase.h:42-54、pathserverbase.h:211-245、pathserverbase.cpp:17-21:

Token / 宏保护对象(注释原意)典型持有者
m_NavMeshDataCriticalSectionToken / LOCK_NAVMESH_DATA非即时模式对 navmesh 的一切访问寻路线程在整轮处理期间持有 PathServerThread.cpp:1104-1107;流式放置/移除;RequestAndEvictMeshes(TryLock)
m_NavMeshImmediateAccessCriticalSectionToken / LOCK_IMMEDIATE_DATA主线程即时模式访问 navmesh即时查询、UpdatePedTracking;流式放置/移除也要等它
m_DynamicObjectsCriticalSectionToken / LOCK_DYNAMIC_OBJECTS_DATA动态对象(“不加锁的更新函数”除外)线程拷贝包围盒 PathServer_Objects.cpp:1376;延迟添加脚本阻挡物;GetClosestPositionForPed
m_RequestsCriticalSectionToken / LOCK_REQUESTS所有请求槽Request*、IsRequestResultReady、LockPathResult、Cancel*、线程取请求、Process 修补请求、碎片整理判断
m_NavMeshStoreLoadedMeshesCriticalSectionToken / LOCK_STORE_LOADED_MESHES非主线程读取 aiMeshStore::GetLoadedMeshes()RequestAndEvictMeshes PathServer.cpp:1198;该数组的注释 navmeshstore.h:294-295
每个请求的 m_CriticalSectionToken线程写完成状态与主线程取消之间的竞争requests.h:179

除锁以外的同步手段:

  • ms_iBlockRequestsFlags:用 sysInterlockedOr/And 置清的位掩码,有两个位——BLOCK_REQUESTS_ON_REQUEST_AND_EVICT 和 BLOCK_REQUESTS_ON_ADD_DYNAMIC_OBJECTS;位非零时线程在取到请求后自旋等待 PathServer.cpp:1137-1141、PathServer.cpp:1059-1068、PathServerThread.cpp:1098-1101。
  • m_bForceAbortCurrentRequest、m_bPathServerThreadIsActive、m_bPathServerThreadSleepingDuringSearch:普通静态 bool pathserverbase.h:150-162。
  • 动态对象包围盒的双缓冲握手(§7.2)以及掩体点缓冲的 bool 标志(§8.4)。

加锁顺序与死锁:Reset 的顺序是 NAVMESH → IMMEDIATE → DYNAMIC PathServer.cpp:4173-4175,流式放置是 NAVMESH → IMMEDIATE navmeshstore.cpp:450-451。Process 的注释记录过一次真实死锁:过期检查要锁请求,而某个主线程路径会“先锁请求、再锁 navmesh”,所以把过期检查挪出了 navmesh 锁范围 PathServer.cpp:969-975。线程卡顿检测宏 CHECK_FOR_THREAD_STALLS 受 __THREAD_STALL_CHECK 0 控制,默认关闭 pathserverbase.h:188-194。

10. 与导航任务的交互#

CTaskNavBase 系列状态机的逐状态说明见 task-catalogue §3.2,这里只列出与 pathserver 交互的契约:

环节任务侧实现说明
组装请求InitPathRequestPathStruct 是纯虚函数 TaskNavBase.h:587;CTaskMoveFollowNavMesh TaskNavMesh.cpp:559-697 与 CTaskMoveWander src/dev_ng/game/task/Movement/TaskMoveWander.cpp:343 各自实现跟随任务以 ped 的 NavCapabilities 生成基础标志,再合并任务配置标志 TaskNavMesh.cpp:561-567;ped 着火时设 DONT_AVOID_FIRE;此外还会设置坡度保留、避爆炸/催泪弹、任务 ped、柔和逃离、完成半径、起止点调整距离、影响球回调;ped 站在带动态 navmesh 的载具上时设 DYNAMIC_NAVMESH_ROUTE TaskNavMesh.cpp:661-668;起点优先使用 navmesh 追踪器 TaskNavMesh.cpp:580-697
发出CTaskNavBase::RequestPath TaskNavBase.cpp:3918-3964若正在“边走边算下一段”,先取消旧句柄;组装后按 ped LOD 修改标志(ModifyPathFlagsForPedLod),再调用 CPathServer::RequestPath;拿到空句柄视为失败
轮询各等待状态调用 IsRequestResultReady(m_iPathHandle, EPath) TaskNavBase.cpp:1496、TaskNavBase.cpp:1564、TaskNavBase.cpp:1843结果就绪那一帧,§4.4 的复位标志保证该 ped 得到完整 AI 更新
取结果RetrievePath TaskNavBase.cpp:4008-4040LockPathResult → 拷进 CNavMeshRoute(记录“末点是否为目标”“是否含水”等)→ 无论成功失败都 UnlockPathResultAndClear 并清空句柄 TaskNavBase.cpp:4184-4196
取消析构、Restart、ResetForRestart TaskNavBase.cpp:713-770任何持有句柄的退出路径都要 CancelRequest,否则 30 s 后会触发泄漏断言(§4.3)
失败重试CTaskMoveFollowNavMesh::OnFailedRoute TaskNavMesh.cpp:818-879依次尝试三种路线方法:Normal → ReducedObjectBoundingBoxes → OnlyAvoidNonMovableObjects,分别对应 REDUCE_OBJECT_BBOXES/IGNORE_NON_SIGNIFICANT_OBJECTS TaskNavBase.h:521-526、TaskNavMesh.cpp:586-597;三种都试过后,后续请求允许走陡坡 TaskNavMesh.cpp:599-604,并依次考虑“传送到队长”“回到主 navmesh(CTaskMoveGetOntoMainNavMesh)”或“等待后重搜”;设了 FNM_QuitTaskIfRouteNotFound 则直接退出
分段与重规划见 task-catalogue §3.2pathserver 一次最多返回 16 个路点,不以目标结束的路线由任务在接近末端时预请求下一段;前方出现障碍、目标移动等情况由任务决定重新请求

CTaskMoveGetOntoMainNavMesh 是一个小型恢复任务:先发洪泛请求计算脚下面积,再从目标反向请求路径,以找到通往主网格的出口 TaskNavBase.cpp:6478、TaskNavBase.cpp:6590、TaskNavBase.cpp:6627。载具的 CTaskVehicleGoToNavmesh 同样使用 pathserver,详见 vehicle-ai-and-road-network。

11. PS3 SPU 作业、平台与构建差异#

  • SPU 作业只是空壳:PathServerSPU.job 把入口定义为 PathServer_FindPathSPU 并包含 task_spu.h src/dev_ng/game/pathserver/PathServerSPU.job:1-2;PathServerSPU.cpp 只在 __SPU 下编译,函数体为空 src/dev_ng/game/pathserver/PathServerSPU.cpp:1-2、PathServerSPU.cpp:30-36;头文件注释只是计划把 FindPath 的热循环移植到 SPU(“随机访问约 2 MB 内存,需要定制本地存储与 DMA”),接口声明本身被注释掉 src/dev_ng/game/pathserver/PathServerSPU.h:1-8。PS3 上寻路仍由 PPU 线程完成。
  • Durango:寻路线程固定在 5 号核心(§5.1)。Xenon:处理器号越界时归 0;可用 XTrace 记录下一次路径请求 PathServerThread.cpp:401-410、PathServerThread.cpp:1183-1188。ped 生成频率:PC/Durango/Orbis 30 ms,其他 500 ms(§5.4)。
  • 离线工具构建(非 GTA_ENGINE):使用 Win32 线程与临界区,并从磁盘 LoadAllMeshes PathServer.cpp:545-555。
  • NAVMESH_EXPORT 等于 __WIN32PC && !__FINAL src/dev_ng/game/pathserver/NavGenParam.h:11-15:启用 CNavMeshDataExporter,从运行中的游戏导出碰撞供 navmesh 生成使用;导出期间关闭 navmesh 流式 src/dev_ng/game/pathserver/ExportCollision.h:17-45、PathServer.cpp:4678-4681。
  • __NAVEDITORS 等于 __BANK && DEBUG_DRAW:NavEdit 提供局部提高生成分辨率、禁止掩体/下落/攀爬等的编辑区域(CNavResArea) src/dev_ng/game/pathserver/NavEdit.h:7-47。
  • 调试:PathServer_Debug.cpp 整个文件包在 #if !__FINAL 中(可视化、压力测试、导出问题请求) src/dev_ng/game/pathserver/PathServer_Debug.cpp:1;PathServer.h 中大量 bank_bool 调试开关 PathServer.h:111-136;__DEV 下的已访问列表模式(§6.2);-nonav 参数(§3.1)。

12. game/ai 目录盘点#

game/ai 不是寻路代码,而是一组被 ped/载具/场景/任务共用的 AI 基础设施:

文件主要类型职责
AITarget.h/.cppCAITarget统一表示“目标”:实体、实体+局部偏移、世界坐标三种模式,提供位置/速度查询 src/dev_ng/game/ai/AITarget.h:18-71;game 下有 53 个头文件引用该类型(rg -l 计数,含其自身)
ExpensiveProcess.h/.cppCExpensiveProcess、CExpensiveProcessDistributer把昂贵的周期性工作(ped 的各类扫描器、目标 LOS、掩体搜索、音频;载具的转向/刹车/变道/navmesh LOS/路口更新等)按类型分散到不同帧 src/dev_ng/game/ai/ExpensiveProcess.h:22-71。每种类型有期望间隔(如 ped 扫描器 15 帧、门扫描 8 帧、熟人扫描 2 帧、激动扫描 60 帧),部分类型还限制每帧处理数 src/dev_ng/game/ai/ExpensiveProcess.cpp:70-86、ExpensiveProcess.h:224-260。Update 每帧对一种类型重新计算间隔与相位 ExpensiveProcess.cpp:170-195;初始化在 gameSkeleton 中注册 src/dev_ng/game/Core/game.cpp:1742,每帧更新在 CGameWorld 中调用 src/dev_ng/game/scene/world/GameWorld.cpp:1840。帧计数只有 8 位,所以不规则更新的最大间隔受限 ExpensiveProcess.h:131-157
EntityScanner.h/.cppCEntityScanner : CExpensiveProcess 及 CPedScanner、CVehicleScanner、CDoorScanner、CObjectScanner周期性收集附近实体,按距离排序后最多保留 16 个 src/dev_ng/game/ai/EntityScanner.h:71-128、EntityScanner.h:175、EntityScanner.h:209、EntityScanner.h:234、EntityScanner.h:269;ped/载具扫描使用 CSpatialArray 查询 src/dev_ng/game/ai/EntityScanner.cpp:342
spatialarray.h/.cpp、spatialarraynode.hCSpatialArray、CSpatialArrayNodeSoA 布局(X/Y/Z 与类型标志分开存放)的空间数组,提供球体、XY 圆柱、线段附近、最近 N 个等查询,可选加锁 src/dev_ng/game/ai/spatialarray.h:18-125;ped、载具、场景点各有一个实例 src/dev_ng/game/Peds/Ped.cpp:2239、src/dev_ng/game/Vehicles/Vehicle.cpp:1119、src/dev_ng/game/task/Scenario/ScenarioPointManager.cpp:181
BlockingBounds.h/.cppCScenarioBlockingArea(s)场景(scenario)屏蔽盒,可分别屏蔽 ped/载具,并可要求正在使用场景的 ped 退出;最多 45 个 src/dev_ng/game/ai/BlockingBounds.h:12-85、BlockingBounds.h:103-104;过场动画与脚本资源会添加 src/dev_ng/game/Cutscene/CutSceneBoundsEntity.cpp:586、src/dev_ng/game/script/Handlers/GameScriptResources.cpp:1548。与 pathserver 的 ped 生成屏蔽区(§8.4)是两套机制
AnimBlackboard.h/.cppCAnimBlackboard(atSingleton)记录最近播放过的环境动画(最多 32 条),避免短时间重复播放同一动画;按“待机/反应”区分不同的冷却时间 src/dev_ng/game/ai/AnimBlackboard.h:1-38、AnimBlackboard.h:120
stats.h/.cppnamespace AIStats一组 PF_TIMER 性能计时器声明(路口、场景、人口等) src/dev_ng/game/ai/stats.h:14-34
ambient/AmbientModelSet*(含 .psc)CAmbientModelSet(s)、CAmbientModelSetManager、ped/载具变体场景和环境用的 ped/道具/载具模型集合;通过 CDataFileMgr 的三类数据文件挂载(支持 DLC),由 parser 加载 .meta src/dev_ng/game/ai/ambient/AmbientModelSetManager.h:48-58、AmbientModelSetManager.cpp:45-47
ambient/ConditionalAnim*(含 .psc)CConditionalAnims(Group)、CConditionalClipSet、CConditionalAnimManager按条件选择环境/场景动画片段集(base/enter/exit/variation/reaction/panic 等类型),数据来自 common:/data/ai/conditionalanims.meta 和 DLC src/dev_ng/game/ai/ambient/ConditionalAnims.h:140-157、src/dev_ng/game/ai/ambient/ConditionalAnimManager.cpp:152。场景系统如何使用见 task-catalogue §3.5
debug/system/AIDebugInfo*、AIDebugLogManager*CAIDebugInfo调试文本/屏显的基类与日志管理;只在 AI_DEBUG_OUTPUT_ENABLED(即 __BANK)时编译 src/dev_ng/game/ai/debug/system/AIDebugInfo.h:5-47、src/dev_ng/game/ai/debug/system/AIDebugLogManager.h:6
debug/types/*(20 对文件)CTaskDebugInfo、CPedDebugInfo、CVehicleDebugInfo、CEventsDebugInfo、CCombatDebugInfo …各子系统的调试信息打印器(动画、附着、战斗、伤害、事件、LOD、移动、ped、布娃娃、关系、脚本历史、瞄准、任务、载具及其控制/标志/节点列表/卡住/任务、通缉);在 task-system 的调试设施一节中引用

13. 离线工具链(简述)#

navmesh 是离线生成的资源。src/dev_ng/tools/NavMeshGenerator 基于 fwnavgen 的 fwLevelProcessTool 框架启动精简版游戏来导出碰撞 src/dev_ng/tools/NavMeshGenerator/navmeshgenapp.h:4-34;生成算法本体在 rage/framework/tools/src/cli/fwnavgen(CNavGen 等) src/dev_ng/rage/framework/tools/src/cli/fwnavgen/NavGen.h:818。游戏侧与之对应的是 ExportCollision/NavEdit(§11)。细节见 standalone-tools-and-extra。

14. 容量与调优常量汇总#

常量值证据
路径/网格/LOS/音频/洪泛/空地/最近位置请求槽32 / 8 / 24 / 16 / 8 / 4 / 4pathserverthread.h:112-118
每条路径最多路点16datatypes.h:181
A* 回溯多边形上限 / 堆容量 / 已访问列表(dev)512 / 2048 / 8192requests.h:284、datatypes.h:821、pathserverthread.h:29
动态对象 / 网格单元 / 细分多边形640 / 64 / 4096pathserverthread.h:37、pathserverbase.h:447、navmesh.cpp:40
影响球 / 包含对象 / 排除对象8 / 8 / 8datatypes.h:422、requests.h:118-119
即时模式访问多边形 / 短 LOS 最大长度256 / 16 mpathserverbase.h:329-330
多边形最大顶点数 / 候选点数 / 特殊链接16 / 65 / 7navmesh.h:543-556
掩体点缓冲 / 水边缓冲 / 掩体区域用网格数128 / 16 / 128pathserverbase.h:590-592
区域开关 / 加载区域64 / 3pathserverbase.h:483、pathserverbase.h:606-613
加载半径 / 请求驱逐周期 / 卸载滞回100 m(可被 nav.dat 覆盖)/ 1000 ms / +10 mPathServer.cpp:319、PathServer.cpp:190、navmeshstore.h:347-348
线程唤醒 / housekeeping / 过期检查 / 请求超时50 ms / 1000 ms / 2000 ms / 30000 msPathServerThread.cpp:559-562、PathServer.cpp:162-165、requests.cpp:15
掩体点 / 水边抽取周期600 ms / 4000 msPathServer.cpp:424-426
线程栈 / ped 半径32768 B / 0.35 mpathserverthread.h:147、datatypes.h:256
ped 生成候选 / 屏蔽区 / 多人出生点128 / 64 / 256pedgen.h:18、pedgen.h:184、PathServer_Ped.h:143
延迟禁用攀爬 / 梯子拥堵表80 / 8PathServerThread.h:16、PathServerThread.h:39
场景屏蔽盒 / 环境动画黑板 / 实体扫描结果45 / 32 / 16BlockingBounds.h:103、AnimBlackboard.h:10、EntityScanner.h:77

约束与边界行为#

  • 槽位耗尽:32 个路径槽全被占用时 RequestPath 返回空句柄,调用方必须能处理这种失败(CTaskNavBase 会进入失败等待)。不按时取回或取消的请求会占住槽位,直到 30 s 后被强制回收并触发断言。
  • 路径不一定到达目标:路点上限是 16,m_bLastRoutePointIsTarget=false 或 m_bWentFarAsPossibleTowardsTarget=true 时,客户端必须继续分段请求或接受“尽量靠近”的结果。
  • “Lock” 不是锁:LockPathResult 返回的指针在 UnlockPathResultAndClear 之前有效,期间不持有任何锁,客户端应在同一帧内拷贝完数据。
  • 动态对象是快照:每个路径请求开始时冻结对象位置;搜索过程中对象再移动不会被看到。主线程正在写某个对象的新包围盒时,线程会跳过它这一轮的更新。
  • 细分只在单次请求内有效:结束时全部撤销,因此不会为同一处障碍缓存跨请求的绕行网格(推断:这是用 CPU 换内存与一致性)。
  • 中止与重试:流式放置网格会强制中止长请求;被中止的请求会回到 pending 状态,之后重新处理,客户端看到的只是延迟变长。
  • 动态 navmesh:起止点不能分别位于两个不同的动态 navmesh 上;同型号载具共享一份网格,矩阵按请求设置。
  • 即时查询:只能在主线程调用,会与流式放置/移除互斥(阻塞等待)。TestShortLineOfSightImmediate 超过 16 m 直接返回失败。

未决问题 / 证据不足#

  • 取消正在处理的请求时的竞态:CancelRequestInternal 先 Reset() 再检查 m_bRequestActive,导致 m_bWaitingToAbort 永远不会被置位(§4.5)。如果被取消的槽在线程仍处理它时被 RequestPath 复用,线程结束时可能把新请求标为完成。本页没有追踪 Clear() 与线程完成写回之间的完整时序,也没有找到实际出现问题的证据,因此只能列为潜在风险。
  • nav.dat 的实际值(扇区数、加载距离、动态 navmesh 类型数)不在仓库中,§2.1 与 §3.3 的半径/网格尺寸只能给出代码默认值。
  • 未逐行阅读的部分:TessellateNavMeshPolyAndCreateDegenerateConnections 的几何细节(约 600 行)、FindPath 中逃离/漫游的启发式与特殊链接代价、PullPathOutFromEdges/CalcPathBezierControlPoints 的具体算法、ProcessClearAreaRequest/ProcessClosestPositionRequest 的评分方式、ped 生成的密度与可见性规则(PathServer_Ped.cpp),本页只描述了它们的接口和触发条件。
  • height mesh 域:已确认默认禁用,但没有穷尽搜索是否有其他构建配置会启用它。
  • ESingleThreaded 模式:只有注释描述,游戏没有使用;工具中是否使用未核实。
  • CTaskVehicleGoToNavmesh 如何使用 pathserver(标志、半径)未在本页展开,由车辆页负责。