任务系统(Task System):aiTask 状态机、任务树与槽位
本页解释 GTA V 中“任务(task)”这一 AI 行为的基本执行单元:RAGE framework 层的 aiTask/aiTaskTree/aiTaskManager 状态机内核,游戏层的 CTask 派生体系、行人/载具/物体的任务树与优先级槽位、任务中止(MakeAbortable)与子任务替换协议、网络克隆任务(CTaskFSMClone)的接入点、任务类型枚举与类信息注册表,以及任务调优数据(CTuning)的数据驱动加载。几乎所有行人 AI、动作控制、部分载具与物体行为都通过这套机制运行,因此它是理解 game/task(约 60 万行)的入口。
背景与相关页面#
- 任务由
CPedIntelligence每帧驱动;行人智能体、AI LOD/时间切片和人口生成见 ped-intelligence-and-population。 - 事件如何转换为“响应任务”并写入任务树槽位见 events-and-responses。
- 各子目录的任务族谱与核心任务状态机见 task-catalogue;寻路任务与 pathserver 见 navigation-and-pathserver。
- 克隆任务的网络序列化(
CTaskInfo同步树节点、迁移)见 network-object-model 与 sync-trees-and-serialisation。 fwRegdRef、fwPool/池配置、fwFlags等基础设施见 framework-systems 与 memory-and-pools;parser/PSO 见 parser-and-metadata。- 载具 AI 任务(
VehicleAI/task,TASK_VEHICLE_*)复用同一内核,细节见 vehicle-ai-and-road-network。
相关源码文件#
| 层 | 文件 | 作用 |
|---|---|---|
| RAGE framework | rage/framework/src/ai/task/task.h/.cpp | aiTask:FSM 宏、状态/子任务/标志、MakeAbortable、转移表接口 |
rage/framework/src/ai/task/tasktree.h/.cpp | aiTaskTree:按优先级排列的根任务数组、UpdateTasks/UpdateTask 主循环、删除队列 | |
rage/framework/src/ai/task/taskmanager.h/.cpp | aiTaskManager:最多 4 棵任务树的容器 | |
rage/framework/src/ai/task/taskflags.h | aiTaskFlags:信息标志与可调标志 | |
rage/framework/src/ai/task/taskstatetransition.h/.cpp | 概率加权的状态转移表 aiTaskStateTransitionTable | |
rage/framework/src/ai/task/taskregdref.h、ai/debugstatehistory.h、ai/task/taskspew.* | 引用计数指针、调试状态历史、DebugPlayback 事件 | |
rage/framework/src/ai/event/event.h | aiEvent(任务中止时传入的事件基类) | |
| 游戏 System | game/task/System/Task.h/.cpp | CTask:池分配、游戏阶段回调、片段/动画辅助、克隆接口默认实现 |
game/task/System/TaskTypes.h/.cpp | CTaskTypes::eTaskType 任务类型枚举 | |
game/task/System/TaskClassInfo.h/.cpp | 任务类型→名称/大小/状态名函数注册表(T2N) | |
game/task/System/TaskManager.*、TaskTree*.* | CTaskManager、CTaskTree/CTaskTreePed/CTaskTreeMovement/CTaskTreeMotion/CTaskTreeClone | |
game/task/System/TaskFSMClone.* | 网络克隆任务基类 CTaskFSMClone | |
game/task/System/TaskMove.*、TaskComplex.*、TaskHelperFSM.*、TaskHelpers.* | 移动任务接口、旧式 complex 任务适配、任务辅助 FSM | |
game/task/System/Tuning.h/.cpp/.psc | CTuning/CTuningManager/CTunablesGroup 调优数据 | |
game/task/System/DefaultTaskData.*、MotionTaskData.* | 按模型选择默认任务/动作任务的数据集 | |
| 装配 | game/Peds/pedDefines.h、game/Vehicles/VehicleDefines.h、game/Objects/ObjectIntelligence.h | 任务树与优先级枚举 |
game/Peds/Ped.cpp、game/Peds/PedIntelligence.cpp、game/scene/scene.cpp、game/Core/game.cpp | 树创建、每帧驱动、任务池初始化、gameSkeleton 注册 |
1. 分层结构总览#
图中证据:aiTask 成员与虚函数见 rage/framework/src/ai/task/task.h:91-430;aiTaskTree 的 m_Tasks、kMaxPriorities = 5 见 rage/framework/src/ai/task/tasktree.h:164-168;aiTaskManager 的 MAX_TREES_SUPPORTED = 4 见 rage/framework/src/ai/task/taskmanager.h:110-113;CTask : public aiTask 见 game/task/System/Task.h:63;CTaskFSMClone : public CTask 见 game/task/System/TaskFSMClone.h:17;CTaskMove/CTaskMoveBase 见 game/task/System/TaskMove.h:75-101;CTaskComplex 见 game/task/System/TaskComplex.h:22;树类继承见 game/task/System/TaskTree.h:12、TaskTreePed.h:22、TaskTreeMovement.h:21、TaskTreeClone.h:21;管理器继承见 game/task/System/TaskManager.h:23-80。
分层职责:
| 层 | 类型 | 职责 |
|---|---|---|
| 状态机内核 | aiTask | 单个任务的 FSM(OnEnter/OnUpdate/OnExit)、父子链、中止协议、计时 |
| 调度 | aiTaskTree | 一组按优先级排序的根任务;每帧只更新“活动优先级”(数值最小的非空槽)那条链 |
| 容器 | aiTaskManager | 若干并行任务树(行人:主/次/移动/动作四棵) |
| 游戏适配 | CTask 及 CTaskTree* | 实体类型访问、物理/后移动/相机/渲染前阶段回调、片段播放、克隆与查询接口、默认任务与网络约束 |
| 驱动者 | CPedIntelligence/CVehicleIntelligence/CObjectIntelligence | 每帧调用 m_taskManager.Process() 并在各帧阶段调用对应回调 |
2. aiTask:状态机内核#
2.1 FSM 宏与回调#
UpdateFSM 是每个任务必须实现的纯虚函数,用一组宏写成“状态 × 事件”的分派:
#define FSM_Begin if(iState<0){if(0){
#define FSM_End }}else{Assertf(0, "Didn't handle state %i in UpdateFSM", iState);\
return(FSM_Quit);}return(FSM_Continue);
#define FSM_State(a) }}else if(a == iState) {if(0){
#define FSM_OnEvent(a) }else if(a == iEvent) {
#define FSM_OnEnter FSM_OnEvent(OnEnter)
#define FSM_OnUpdate FSM_OnEvent(OnUpdate)
#define FSM_OnExit FSM_OnEvent(OnExit)引自 rage/framework/src/ai/task/task.h:35-42。展开后是一串 if/else if:负状态落入 FSM_Begin 的空分支,未处理的状态在 FSM_End 断言并返回 FSM_Quit。返回值只有 FSM_Continue/FSM_Quit,事件只有 OnEnter/OnUpdate/OnExit 三种 task.h:300-312。典型写法见 CTaskCombat::UpdateFSM,每个状态把三个事件转发到 Xxx_OnEnter/Xxx_OnUpdate/Xxx_OnExit 成员函数 game/task/Combat/TaskNewCombat.cpp:2534-2558。
除 UpdateFSM 外,aiTask 定义了以下可覆盖点:
| 虚函数 | 默认行为 | 调用时机/用途 |
|---|---|---|
ProcessPreFSM() | 设置 SkipProcessPreFSM 后返回 FSM_Continue | 每帧 FSM 前;返回 FSM_Quit 立即结束任务 task.h:700-705 |
ProcessPostFSM() | 设置 SkipProcessPostFSM | 每帧 FSM 及子任务更新之后 task.h:709-714 |
ProcessFSM(state,event) | 直接调 UpdateFSM | CTaskFSMClone 覆盖它以分派到 UpdateClonedFSM task.cpp:435-438 |
ShouldAbort(priority,event) | true | MakeAbortable 先询问整条子链 task.h:724-727 |
DoAbort / CleanUp | 空 | 中止或结束时清理 task.h:718-733 |
GetDefaultStateAfterAbort() | 纯虚 | 被中止后恢复时重新进入的状态 task.h:346 |
MayDeleteOnAbort() / MayDeleteSubTaskOnAbort() / ShouldAlwaysDeleteSubTaskOnAbort() | false / true / MayDeleteOnAbort() | 中止后是否可以直接删除自身或子任务分支 task.h:178-190 |
SupportsTerminationRequest() / RequestTermination() | false | “请求自行优雅结束”的协作式终止;支持时只置 TerminationRequested task.cpp:190-202 |
Copy() | 纯虚 | 克隆任务(序列、控制移动任务的“备份”都依赖它)task.h:158 |
基类默认的 ProcessPreFSM/PostFSM 会在首次调用时给自己打上 Skip* 标志,此后树不再进行这次虚调用;树里对 Pre 用 Likely(!Skip…)、对 Post 用 Unlikely(!Skip…) 分支提示 tasktree.cpp:507-515、tasktree.cpp:776-784。这表明作者预期大部分任务实现 Pre 而很少实现 Post(推断,基于分支提示)。
2.2 数据布局与限制#
| 字段 | 类型 | 说明 |
|---|---|---|
m_pEntity | fwEntity* | 只能设置一次,SetEntity 断言不得更换 task.h:737-740 |
m_pParent | 裸指针 | 刻意不用 regdref,避免常驻父任务被子任务的 SetParent 刷满引用 task.h:392-396 |
m_pSubTask / m_pNewTask | RegdaiTask | 当前子任务与“下一子任务”(由树在合适时机替换)task.h:398-402 |
m_Flags | aiTaskFlags(两个 fwFlags16) | 信息标志 + 可调标志 taskflags.h:105-108 |
m_TaskType | s32 : 16 | SetInternalTaskType 断言 <= 0xFFFF task.h:142、task.h:409-410 |
m_State / m_PreviousState | s32 : 8 | SetState 断言 iState <= 0xFF task.h:413-417、task.cpp:330-332 |
m_TimeInState / m_TimeRunning | float | 由树按本次时间步累加 tasktree.cpp:517-537 |
sm_TimeStep / sm_TimeStepInMilliSeconds | 静态 | BeginTaskUpdates/EndTaskUpdates 包裹一次树更新,GetTimeStep() 替代 fwTimer::GetTimeStep();注释明言若要多线程更新任务必须改掉这些静态量 task.h:97-125 |
注意:m_State 是 8 位有符号位域,实际可用状态值上限由 SetState 的断言保证在 <= 0xFF;DEFINE_STATIC_STATE_NAME_STRINGS 宏配合 X() 列表生成状态名 game/task/System/Task.h:38-48,例如 CTaskEnterVehicle 用 ENTER_VEHICLE_STATES(X) 同时生成枚举与字符串 game/task/Vehicle/TaskEnterVehicle.h:313-356。复制构造与赋值被私有化,任务只能通过 Copy() 克隆 task.h:455-461。
2.3 标志位#
| 信息标志(只读语义) | 含义 |
|---|---|
HasBegun | 至少进入过一次状态 |
HasFinished | 已结束(Quit 或 Pre/Post 返回 Quit) |
IsAborted | 曾被中止 |
SubTaskFinished | 子任务在本帧结束(下一次 OnUpdate 可见) |
AnimFinished | 片段辅助播放结束(CTask::FSM_SetAnimationFlags 置位) |
TerminationRequested | 被请求优雅终止 |
CurrentStateNotYetEntered | 新状态尚未执行 OnEnter |
InMakeAbortable | 正在执行中止,防重入 |
SkipProcessPreFSM / SkipProcessPostFSM | 跳过无实现的 Pre/Post 虚调用 |
| 可调标志(任务自己设置) | 含义 |
|---|---|
KeepCurrentSubtaskAcrossTransition | 状态切换时保留当前子任务(默认切状态会移除子任务) |
RestartCurrentState | 重新执行当前状态(Exit→Enter) |
RestartStateOnResume | 被中止后恢复时重新进入状态 |
QuitOnResume | 恢复时直接退出 |
RemoveSubtask | 请求移除当前子任务 |
Internal_NewSubtask | 内部使用 |
HandleCloneSwapToSameTaskType | 克隆树上同类型任务被替换 |
ProcessNextActiveTaskImmediately | 当本优先级根任务结束时,本帧立刻处理下一个活动优先级 |
SkipSetAnimationFlags | 跳过 FSM_SetAnimationFlags 虚调用 |
两张表来自 rage/framework/src/ai/task/taskflags.h:25-91。构造时初始标志为 CurrentStateNotYetEntered + SkipSetAnimationFlags,状态为 0 task.cpp:36-46,所以每个任务首次更新会先执行状态 0 的 OnEnter。CTask 构造时会把类型设为 TASK_NONE 并绑定片段辅助 game/task/System/Task.cpp:61-70;__DEV 构建的 GetTaskType() 断言 m_TaskType 与 GetTaskTypeInternal() 一致,用于捕捉忘记在构造函数里调用 SetInternalTaskType 的任务 task.cpp:442-449。
2.4 SetState / SetNewTask 的语义#
SetState(i):若与当前不同则置CurrentStateNotYetEntered;清零状态计时、记录前一状态;如果任务使用转移表,还会重置周期性搜索计时器与上帧条件标志;TASK_DETAILED_INFINITE_LOOP_TRAP(即__ASSERT)下记录调用栈task.cpp:330-373。真正的OnExit/OnEnter由树在同一帧的循环中触发,而不是SetState本身。SetNewTask(p):只是登记“下一子任务”,覆盖时删除旧的待定任务task.cpp:378-387;替换动作由树完成(见 §3.3)。__ASSERT下树在OnUpdate之外把m_CanChangeState置假,借此断言在不允许的时机改状态tasktree.cpp:470-471、tasktree.cpp:582-585。
2.5 状态转移表(可选的数据化决策)#
aiTaskStateTransitionTable 允许任务把“从状态 A 在满足条件位 C 时以权重 p 转到 B”写成表,而不是手写 if:
- 表容量:
MAX_TRANSITIONS = 128、MAX_FALLBACKS = 32rage/framework/src/ai/task/taskstatetransition.h:93-100,超出时断言并丢弃taskstatetransition.cpp:81-105。 - 匹配规则:起始状态相同且
(conditionFlags & required) == requiredtaskstatetransition.cpp:40-53。 - 选择:收集所有满足的转移,
fRandom∈[0,1]大于权重和时“随机不选”返回 -1;否则在[0,sum)上按权重轮盘选择;若无任何满足条件,则按起始状态查 fallbacktaskstatetransition.cpp:108-161。因此权重和 ≥1 的表必然选中某个转移(CTaskCombat的表用 9.0/1.0 这类权重,注释说明是为了确保从 Start 状态一定转出game/task/Combat/TaskNewCombat.cpp:12654-12655)。 - 接入 FSM:
FindNewStateTransition/PickNewStateTransition立即查表;PeriodicallyCheckForStateTransitions(avg)以avg × [0.8,1.2]的随机间隔查表,若条件位与上帧不同则本帧立即查task.cpp:240-326。任务需覆盖GetTransitionSet、GenerateTransitionConditionFlags、GetTransitionTableData,否则默认实现断言task.h:663-685。
使用者举例:CTaskCombat 按 CCombatBehaviour::GetCombatType 选四张表之一(持械步战、仅掩体、徒手、水下)TaskNewCombat.cpp:2105-2110;CTaskSmartFlee 有步行/滑雪/载具三张表、CTaskCombatMounted 有骑乘表(见 task-catalogue)。
3. aiTaskTree:每帧更新算法#
3.1 优先级槽位与活动任务#
树是一个长度为“优先级数”的 RegdaiTask 数组,索引越小优先级越高;ComputeActiveTaskPriority() 返回第一个非空槽 tasktree.cpp:230-241。每帧只更新活动槽这一条父→子链,低优先级槽里的任务保持“被中止、待恢复”的状态,不会被 tick。
SetTask(task, priority) 的通用实现 tasktree.cpp:86-179:
- 断言不在处理同一优先级时替换(否则会删除正在运行的任务)。
- 对
priority及更低优先级(索引更大)上所有未中止的任务调用MakeAbortable(URGENT),失败则MakeAbortable(IMMEDIATE)——新任务进入高优先级槽会立刻中止其下所有槽,但不会删除它们。 - 传
NULL表示清槽;同一指针重复设置直接返回;否则删除旧任务、写入新任务、SetEntity、重算活动优先级。
3.2 UpdateTasks:根任务循环#
证据:rage/framework/src/ai/task/tasktree.cpp:361-434。bDontDeleteHeadTask 由移动树传入 true,注释说明“移动任务需要保留一帧”tasktree.cpp:387-391、game/task/System/TaskTreeMovement.cpp:38。
3.3 UpdateTask:单任务的同帧状态迭代#
UpdateTask(pTask) 是整个任务系统最核心的函数 tasktree.cpp:438-794。按调用顺序:
- 死循环保护:
m_iDeadman每次调用减 1(初值 1000),归零时只打印一次任务层级并返回UR_CONTINUEDtasktree.cpp:473-505。 ProcessPreFSM:返回 Quit 则置HasFinished并返回UR_QUITtasktree.cpp:507-515。- 累加
m_TimeRunning、调用FSM_SetAnimationFlags、清除上帧的KeepCurrentSubtaskAcrossTransition | RestartCurrentState、累加m_TimeInStatetasktree.cpp:517-537。 - FSM 迭代循环(同一帧内可多次切状态)
tasktree.cpp:540-749:- 若
RestartStateOnResume或CurrentStateNotYetEntered:清空待定子任务,执行OnEnter;Quit 则补一次OnExit并退出;否则继续循环(下一轮执行OnUpdate)tasktree.cpp:555-576。 - 否则执行
OnUpdate;若状态改变或要求RestartCurrentState:对旧状态OnExit,除非KeepCurrentSubtaskAcrossTransition否则丢弃待定子任务,重置状态计时,对新状态OnEnter;若新状态没有SetNewTask且未要求保留,则置RemoveSubtask。状态未变则标记“需要更新子任务”tasktree.cpp:577-655。 - 子任务替换:若有
NewTask,先FSM_AbortPreviousTask()(游戏层发CEventNewTask中止旧子任务),再删除旧子任务、AddSubTask新子任务、调用OnNewSubTaskCreated钩子tasktree.cpp:657-671;否则若RemoveSubtask,则MakeAbortable(URGENT→IMMEDIATE)后删除tasktree.cpp:672-687。 - 递归更新子任务:仅当本任务本轮“稳定在某状态”时,递归
UpdateTask(sub);子任务 Quit 则置SubTaskFinished | RemoveSubtask并再跑一轮父任务(父任务在同帧看到子任务结束)tasktree.cpp:694-715。 - 循环上限:
MAX_STATE_LOOPS = 30,断言版额外记录fwDebugStateHistory::MAX_SIZE条调用栈再报告疑似死循环;硬上限为30 + MAX_SIZE + 2次后强制跳出tasktree.cpp:717-748。MAX_SIZE = (80 - sizeof(int)) / sizeof(cStateRecord)rage/framework/src/ai/debugstatehistory.h:30-37,按cStateRecord为 u32+u16+u16 计算即 9,硬上限约 41 次(计算结果,依赖结构体无额外填充)。
- 若
- 结束处理:
bQuit或已HasFinished时,置HasFinished,对未结束的子任务MakeAbortable,调用CleanUp,返回UR_QUITtasktree.cpp:757-772。 ProcessPostFSM:Quit 同样结束任务tasktree.cpp:776-784。
关键不变量:
- 父任务的
OnUpdate先于子任务执行,子任务更新只发生在父任务“本轮状态稳定”之后,因此父任务可在同帧完成“发现子任务结束→切状态→OnEnter创建新子任务→新子任务立刻 tick”。 - 一条链上的任务一旦返回 Quit,其所有子任务都会被中止并
CleanUp;根任务结束才会从槽中删除。 - 树更新期间不得对正在处理的优先级
SetTask(m_iPriorityBeingProcessed断言)tasktree.cpp:91-97。
3.4 中止协议:MakeAbortable#
证据:rage/framework/src/ai/task/task.cpp:90-186。要点:
- 优先级只有两档
ABORT_PRIORITY_URGENT、ABORT_PRIORITY_IMMEDIATEtask.h:160-165;调用方惯例是先 URGENT、失败再 IMMEDIATE(树、事件处理器、移动树都如此)tasktree.cpp:135-141。任务通常在ShouldAbort中对 URGENT 说“不”以完成关键动画,对 IMMEDIATE 必须服从(AbortTasksWithPriority中 IMMEDIATE 失败会断言tasktree.cpp:260-281)。 - 被中止的任务不会被删除(除非
MayDeleteOnAbort),而是回到GetDefaultStateAfterAbort()并带RestartStateOnResume;当更高优先级槽清空后,它重新成为活动任务并从该状态OnEnter恢复。这就是“临时事件响应结束后,行人回到原先行为”的机制。 - 已
HasFinished的任务直接视为可中止task.cpp:95-96。 - 游戏层
CTask::FSM_AbortPreviousTask在子任务替换时构造CEventNewTask(优先级E_PRIORITY_NEW_TASK,并标记新任务是否布娃娃相关)传给MakeAbortable,让子任务据此决定是否接受game/task/System/Task.cpp:259-282。
3.5 删除与跨线程延迟删除#
DeleteTask 自根向下逐个 OnTaskDeleted→CleanUp→删除;若 sm_AllowDelete == false,则调用 ClearAllKnownRefs() 并推入静态 sm_TaskDeletionQueue(容量 TASK_DELETION_QUEUE_SIZE = 1280,溢出直接 Quitf),待 SetAllowDelete(true) 时统一释放 tasktree.cpp:815-878、tasktree.h:170-172。两处都断言在更新线程。目前唯一切换者是载具 AI 的延迟并行任务:当 aiDeferredTasks 启用多工作线程时,SetUseLockCommon(true) 会关闭任务删除并同时打开 regdref、事件池、空间数组等的锁 game/vehicleAi/task/DeferredTasks.cpp:140-154。这表明“任务更新是单线程的”这一假设在载具 AI 的工作线程阶段被有限度地放宽,并依靠删除队列+锁维持安全(推断,工作线程中具体执行的任务代码见载具页)。
4. aiTaskManager 与游戏层管理器#
aiTaskManager::Process(dt) 按树索引顺序依次调用 GetTree(i)->Process(dt) taskmanager.cpp:50-57。SetTask(tree, task, priority) 在写入后调用虚函数 OnTaskChanged() taskmanager.h:176-181;CPedTaskManager::OnTaskChanged 设置 SetTaskSetNeedIntelligenceUpdate(true),强制该行人下一帧做完整智能更新(绕过 AI 时间切片)game/task/System/TaskManager.cpp:97-104。树可以由管理器拥有或外部拥有:载具管理器把两棵树作为成员并用 SetExternallyOwnedTree 注册 TaskManager.cpp:108-116。
CTaskManager 增加了按帧阶段遍历活动链的入口:ProcessPhysics(遍历所有树)、ProcessPostMovement、ProcessPostCamera、ProcessPreRender2、ProcessPostPreRender、ProcessPostPreRenderAfterAttachments、ProcessMoveSignals(按指定树)game/task/System/TaskManager.h:30-49。CTaskTree 的这些函数都是“取活动根任务,沿子链逐个调用对应 CTask 虚函数”game/task/System/TaskTree.cpp:20-140;ProcessPhysics 支持 nmTasksOnly 只对 NM 行为任务调用,返回值表示是否需要保持实体唤醒 TaskTree.cpp:20-41。
5. 各类实体的任务树与槽位#
5.1 行人:四棵树#
树(ePedTaskTrees) | 类 | 优先级槽(索引小者优先) | 用途 |
|---|---|---|---|
PED_TASK_TREE_PRIMARY = 0 | CTaskTreePed(克隆行人为 CTaskTreeClone) | PHYSICAL_RESPONSE=0, EVENT_RESPONSE_TEMP, EVENT_RESPONSE_NONTEMP, PRIMARY, DEFAULT | 主行为:物理响应、临时/非临时事件响应、脚本/主任务、默认任务 |
PED_TASK_TREE_SECONDARY = 1 | CTaskTree | PED_TASK_SECONDARY_PARTIAL_ANIM=0 | 叠加在主行为上的次要任务(上身动画、手机、头盔等) |
PED_TASK_TREE_MOVEMENT = 2 | CTaskTreeMovement | MOVEMENT_EVENT_RESPONSE=0, MOVEMENT_GENERAL, MOVEMENT_DEFAULT | 移动任务(IsMoveTask()),输出期望移动混合比/朝向 |
PED_TASK_TREE_MOTION = 3 | CTaskTreeMotion | PED_TASK_MOTION_DEFAULT=0 | 动作任务(CTaskMotionBase),驱动 MoVE 动画网络 |
枚举见 game/Peds/pedDefines.h:264-307。创建见 CPed 初始化:主树 CTaskTreePed(PED_TASK_PRIORITY_MAX, this, EVENT_RESPONSE_TEMP, DEFAULT)、移动树、次树、动作树,并立即放入默认任务 CTaskDoNothing(-1) 与默认移动任务 CTaskMoveStandStill game/Peds/Ped.cpp:1644-1655。主树在网络所有权变化时整体替换:转为本地时新建 CTaskTreePed game/network/Objects/Entities/NetObjPed.cpp:7962-7971,转为克隆时先 FlushImmediately 再换成 CTaskTreeClone 并设默认任务 CTaskNetworkClone NetObjPed.cpp:7977-7999。
树处理顺序由 aiTaskManager::Process 的索引循环决定:主树 → 次树 → 移动树 → 动作树(推断自循环顺序与枚举值 taskmanager.cpp:53-56)。这一顺序正好对应“主任务决定意图→移动任务把意图变成期望速度/朝向→动作任务据此选择动画”的数据流。
CTaskTreePed:默认任务与网络约束#
game/task/System/TaskTreePed.cpp:43-155 在通用 SetTask 基础上增加:
- 正在处理默认槽或默认任务处于
InMakeAbortable时,对默认槽的替换被拒绝并删除传入任务TaskTreePed.cpp:63-74。 - 断言移动任务不得进入主树
TaskTreePed.cpp:80-87。 - 网络克隆行人且
CNetObjPed::CanSetTask()为假时,任何SetTask都被丢弃——远端控制的行人不能被本地直接下任务TaskTreePed.cpp:89-108。 - 中止低优先级任务时传入
CEventNewTaskTaskTreePed.cpp:110-122。 - 默认槽清空后立刻补
GetDefaultTask();Process每帧也确保默认槽非空,然后以默认槽为“默认任务优先级”调用UpdateTasksTaskTreePed.cpp:145-172。
默认任务由 CPed::ComputeDefaultTask 决定 TaskTreePed.cpp:232-240:在车里→默认驾驶任务;被羁押→CTaskInCustody;本地玩家→当前动作任务的 CreatePlayerControlTask();有组的 SWAT/军队→CTaskSwat/CTaskArmy;组员→CTaskFollowLeaderAnyMeans;任务行人或配置了 DoNothingWhenOnFootByDefault→CTaskDoNothing;否则查模型的 CDefaultTaskDataSet(WANDER / DO_NOTHING),缺省为 ComputeWanderTask game/Peds/Ped.cpp:1110-1199。默认任务数据集结构见 game/task/System/DefaultTaskData.psc:4-24,模型通过 GetDefaultTaskDataSetHash() 绑定 Ped.cpp:1363-1367。
CTaskTreeMovement:心跳式所有权#
移动树的根任务通常由主树中的“控制者”任务托管(典型是 CTaskComplexControlMovement),二者通过每帧心跳维持关系:
- 控制者每帧在
ControlSubTask中对GENERAL槽上的移动任务调用SetCheckedThisFrame(true)并UpdateProgress;若移动任务丢失则从自身保存的“备份副本”Copy()一个重新放入game/task/General/TaskBasic.cpp:952-982。 - 移动树每帧先执行
ProcessMoveTasksCheckedThisFrame:已HasFinished的任务删除;GENERAL槽任务若上一帧没被“检查”则视为孤儿,MakeAbortable(IMMEDIATE)后删除;两种情况都会连带中止EVENT_RESPONSE槽;存活的任务清除心跳位game/task/System/TaskTreeMovement.cpp:145-192。 - 克隆行人不处理移动树;无默认移动任务时补
CTaskMoveStandStillTaskTreeMovement.cpp:29-47。替换GENERAL槽时同样连带中止事件响应槽TaskTreeMovement.cpp:111-130。
移动任务实现 CTaskMoveInterface:移动混合比(MBR,0..3 对应静止/走/跑/冲刺)、目标、目标半径、CheckedThisFrame 心跳 game/task/System/TaskMove.h:11-69。最底层的 CTaskMoveBase 在 StateRunning 中调用 ProcessMove 后把 MBR 写入 CPedMotionData::SetDesiredMoveBlendRatio(扫射时写二维向量)并设置期望朝向 game/task/System/TaskMove.cpp:290-331,动作树的 CTaskMotionPed 再读取期望 MBR game/task/Motion/Locomotion/TaskMotionPed.cpp:830。
CTaskTreeMotion#
动作树在默认槽变化、新子任务创建、任务删除时标记 SetCurrentMotionTaskDirty(),供 CPed::GetCurrentMotionTask() 刷新缓存的“当前叶子动作任务”game/task/System/TaskTreeMotion.cpp:13-40、game/Peds/Ped.cpp:6503-6536。默认动作任务由 CPed::StartPrimaryMotionTask 放入 Ped.cpp:6493-6501。
5.2 载具与物体#
| 实体 | 树 | 槽 |
|---|---|---|
| 载具 | VEHICLE_TASK_TREE_PRIMARY, VEHICLE_TASK_TREE_SECONDARY | 主:CRASH=0, PRIMARY;次:ANIM game/Vehicles/VehicleDefines.h:259-279 |
| 物体 | OBJECT_TASK_TREE_PRIMARY, OBJECT_TASK_TREE_SECONDARY | 主:PRIMARY;次:ANIM game/Objects/ObjectIntelligence.h:19-38 |
物体树在 CObjectIntelligence 构造中创建 game/Objects/ObjectIntelligence.cpp:41-42。载具任务的运行与车辆 AI 逻辑见 vehicle-ai-and-road-network。
6. 帧内时序:任务在一帧中被调用的位置#
CPedIntelligence::Process(fullUpdate, dt) 的顺序 game/Peds/PedIntelligence.cpp:1255-1376:
- 维护附近实体列表;完整更新时更新变量。
GetEventScanner()->ScanForEvents()生成事件;UpdatePedDecisionMaker()。- 目标、掩体查找、封闭区域更新。
- 完整更新时:
aiEvent::BeginEventUpdates→GetEventHandler()->HandleEvents()(把最高优先级事件转成响应任务写入槽位)→EndEventUpdates;更新防御区域。 Process_Tasks():若设置了TasksNeedProcessMoveSignalCalls复位标志,则对主/动作/次树调用ProcessMoveSignals(即使不是完整更新);完整更新时重置预任务复位标志、IK 预更新、面部数据,然后m_taskManager.Process(dt)PedIntelligence.cpp:3114-3175。- 完整更新时构建查询状态
BuildQueriableState()(网络同步与外部查询的任务快照),然后感知、潜行、动机更新。
非完整更新(AI 时间切片)时事件处理与任务 FSM 都被跳过,时间以 m_fTimeSinceLastAiUpdate 累积到下次完整更新 PedIntelligence.cpp:1269-1272;需要每帧与 MoVE 交互的任务用 RequestProcessMoveSignalCalls() 订阅 ProcessMoveSignals game/task/System/Task.cpp:183-189、Task.h:148-152。其余阶段回调(物理时间片内的 ProcessPhysics,物理后的 ProcessPostMovement,相机后 ProcessPostCamera,骨骼后 ProcessPreRender2,IK 后 ProcessPostPreRender,附着后 …AfterAttachments)的语义在 CTask 头文件中注释 Task.h:121-149。物理回调期间 CTask::GetNumProcessPhysicsSteps() 给出本次物理子步数 Task.h:386-411。这些回调在世界更新中的确切位置属于主循环与行人更新,见 game-skeleton-and-main-loop。
7. CTask:游戏层基类#
7.1 内存:线程安全池分配器#
CTask 通过 REGISTER_POOL_ALLOCATOR(CTask, TASK_USE_THREADSAFE_POOL_ALLOCATOR) 重载 new/delete,TASK_USE_THREADSAFE_POOL_ALLOCATOR = 1 game/task/System/Task.h:60-69;关闭该宏时构造/析构会断言在更新线程 Task.cpp:61-77。池在场景初始化时创建:堆大小 PS3/360 为 512KB,其他平台为 512KB × 10;槽数从配置文件 fwConfigManager::GetSizeOfPool("CTask") 读取;BANK 构建注册池满回调 PoolFullCallback(打印每个任务所在树/优先级/深度/运行时间)game/scene/scene.cpp:461-474、Task.cpp:326-397。由于是“单一槽大小”的池,所有任务类的 sizeof 受限:GetLargestTaskSize 在断言版上若最大任务超过 416(PS3)/432(360)/736(其他)字节则致命断言 game/task/System/TaskClassInfo.cpp:297-315,打印工具把 ≥380 字节的任务列为错误级输出 TaskClassInfo.cpp:402-418。(CTask 池配置的实际数值在 gameconfig.xml 中,不在本源码树内,未核实。)
CTask::Init 在非会话初始化时创建一组任务辅助对象池(攀爬手点、抓取、路径节点搜索、载具片段请求、环境注视、对话、道具管理);Shutdown 对称释放;ResetStatics 重置大量任务的全局冷却时间戳 Task.cpp:115-174。注册点:CTask::Init 与 CTaskClassInfoManager::Init、CTuningManager::Init 由 CGame 的 gameSkeleton 注册 game/Core/game.cpp:1808-1809、game.cpp:2167、game.cpp:2260。
7.2 类型分类与常用接口#
| 接口 | 作用 |
|---|---|
IsMoveTask() / GetMoveInterface() | 移动任务标识,决定能进哪棵树 Task.h:154-171 |
IsNMBehaviourTask() / IsBlendFromNMTask() / HandlesRagdoll() | 布娃娃/NM 相关判定,影响事件处理器是否强制中止 Task.h:164-266 |
AddGetUpSets() / GetUpComplete() / RequestGetupAssets() | 起身任务向主任务链询问起身动作集,沿子链递归 Task.h:322-367 |
RecordDamage() / HandlesDeadPed() / GetTargetPositionEntity() | 沿子链递归的查询接口 Task.h:207-278 |
GetIsResponseForEventType() | 任务若是某事件的响应,返回事件类型;联机时同步,用于所有权迁移后仍能阻止被同类事件打断 Task.h:380-384 |
IsClonedFSMTask() / CreateQueriableState() | 克隆任务标识与查询状态(默认 new CTaskInfo)Task.h:225-229、Task.cpp:238-241 |
StartClipBySetAndClip() 等 + CClipHelper | 基础片段播放;FSM_SetAnimationFlags 在片段终止时置 AnimFinished Task.cpp:249-257 |
GetScenarioPoint() / GetScenarioPointType() | 场景任务暴露使用中的场景点 Task.h:315-320 |
ConvertPriority(15/25/35/-1)是以距离分级的常量 Task.h:50-56(具体使用方未在本页追踪)。
7.3 兼容层:CTaskComplex 与任务辅助 FSM#
CTaskComplex把旧式“complex task”接口(CreateFirstSubTask/ControlSubTask/CreateNextSubTask)适配到 FSM:固定四个状态CreateFirstSubTask/ControlSubTask/CreateNextSubTask/Quit,默认中止后退出且允许删除game/task/System/TaskComplex.h:22-74。CTaskComplexControlMovement、CTaskComplexOnFire、CTaskComplexGetOutOfWater等仍基于它(见 task-catalogue)。CTaskHelperFSM是不进入任务树、由宿主任务手动驱动的轻量 FSM(有自己的aiTaskHelperFlags),用于路径节点搜索、掩体查找、对话、场景查找等;最大辅助类大小宏SIZE_LARGEST_TASK_HELPER_CLASS = 192game/task/System/TaskHelperFSM.h:13-72。CTaskList(最多 16 个任务)与CTaskSequences(全局 128 条序列)支撑脚本的序列任务与TASK_USE_SEQUENCEgame/task/General/TaskBasic.h:611-616、TaskBasic.h:894-910。
8. 任务类型枚举与类信息注册表#
8.1 CTaskTypes::eTaskType#
任务类型是一个大型 C++ 枚举(不是 .psc 生成的),从 TASK_HANDS_UP = 0 开始到 MAX_NUM_TASK_TYPES 结束 game/task/System/TaskTypes.h:11-664。特点:
- 前 5 个值(
TASK_HANDS_UP、TASK_CLIMB_LADDER、TASK_EXIT_VEHICLE、TASK_COMBAT_ROLL、TASK_AIM_GUN_ON_FOOT)有脚本依赖,固定编号,并以CompileTimeAssert与commands_task.sch同步TaskTypes.h:17-23、TaskTypes.h:666-668、TaskTypes.cpp:12-16。其余值会随增删而移动,因此脚本/网络不能依赖除此之外的数值稳定性(推断)。 - 枚举按主题分段:玩家、基础、攀爬、次要、服务、惊吓事件反应、场景、犯罪、片段、碰撞响应、威胁响应、车辆(公共/私有)、骑乘、goto/移动、动作树任务、武器/掩体、战斗、布娃娃(
TASK_NM_RELAX到TASK_RAGDOLL_LAST之间,注释要求不要在两端之外添加)、项目特定、载具 AI(TASK_VEHICLE_FIRST_TASK起)TaskTypes.h:25-655。IsRagdollTask就是这个区间判断TaskTypes.cpp:18-26;IsCombatTask列举TASK_COMBAT、TASK_THREAT_RESPONSE等TaskTypes.cpp:28-38。 - 某些值只在
__BANK/__DEV存在(如TASK_SCENARIO_ANIM_DEBUG、TASK_PREVIEW_MOVE_NETWORK、TASK_NM_BIND_POSE)TaskTypes.h:163-165、TaskTypes.h:514-516,所以同一任务在不同构建配置下的数值不同(推断:任何跨配置传输任务类型的通道都需要注意)。 TASK_DEFER_TO_SCENARIO是“虚拟类型”,只供决策制定器表示“交由场景点标志决定响应”TaskTypes.h:141。
8.2 CTaskClassInfoManager:注册点#
没有“按类型创建任务”的通用工厂;CTaskClassInfoManager 只是类型→元信息表:每项保存 sizeof、名字串和静态状态名函数指针 game/task/System/TaskClassInfo.h:12-70。注册通过 T2N(TASK_X, CTaskX) 宏在 Init() 中逐条完成(约 500 行)TaskClassInfo.cpp:280-285、TaskClassInfo.cpp:423-944;名字与状态名只在 !__FINAL/!__NO_OUTPUT 时填充。CTask::GetTaskName()/GetStateName() 都转发到这个表 Task.h:78-89,CTaskTreePed::SetTask 在 AI 优化关闭时断言任务已注册 TaskTreePed.cpp:57-59。该文件同时 #include 所有任务目录的 Metadata_parser.h,作为这些 .psc 生成代码的唯一编译单元 TaskClassInfo.cpp:260-275。
“按类型创建”的工厂分散在三处(见各自页面):事件响应工厂 CEventResponseFactory(按 eTaskType switch)、克隆任务的 CQueriableInterface::CreateEmptyTaskInfo + CTaskInfo 创建克隆任务、场景系统 CScenarioManager::CreateTask(按 CScenarioInfo 子类)。
9. 网络克隆任务接口(概要)#
CTaskFSMClone 为需要在远端复现的任务提供双 FSM:本地拥有者运行 UpdateFSM,远端克隆运行 UpdateClonedFSM;分派在 ProcessFSM 中按实体是否为网络克隆决定 game/task/System/TaskFSMClone.cpp:202-215。主要接口 game/task/System/TaskFSMClone.h:17-200:
| 接口 | 用途 |
|---|---|
m_netSequenceID / Get/SetNetSequenceID | 区分同类型任务的不同实例,防止克隆重复执行同一实例 |
CreateQueriableState()(纯虚)/ ReadQueriableState() | 拥有者生成 CTaskInfo,克隆端读回序号、优先级、状态 |
OverridesNetworkBlender / …HeadingBlender / …Attachment / OverridesCollision / OverridesVehicleState / AllowsLocalHitReactions / UseInAirBlender | 告诉网络混合器在任务运行时让出位置/朝向/附着/碰撞控制 |
ControlPassingAllowed() | 关键阶段阻止所有权迁移 |
IsInScope/EnterScope/LeaveScope/GetScopeDistance | 远离相机的克隆任务可“出范围”而不更新(离开距离 = 进入距离 × 1.2)TaskFSMClone.cpp:94-120 |
CreateTaskForClonePed / CreateTaskForLocalPed / Handle*Switch | 所有权迁移时把任务换成对端版本 |
CreateSubTaskFromClone | 克隆端依据查询接口里出现的子任务信息自动创建子任务 TaskFSMClone.cpp:222-295 |
克隆中止时不走通用链式中止,而是先中止子任务,再对自身执行 UpdateClonedFSM(OnExit) 并置 QuitOnResume TaskFSMClone.cpp:179-200。
CTaskTreeClone 是克隆行人的主树:RecalculateTasks 遍历行人查询接口中的 CTaskInfo,以序列号判断是否已创建,选择最高优先级者放入 PED_TASK_PRIORITY_PRIMARY,TASK_DYING_DEAD 总是强制应用以避免“僵尸”行人 game/task/System/TaskTreeClone.cpp:72-200;更新时汇总各克隆任务的网络混合器覆盖标志,根任务结束后立刻重算并同帧更新新任务 TaskTreeClone.cpp:551-598;出范围的任务只要查询接口显示仍在运行就保持(不更新)TaskTreeClone.cpp:600-653。CTaskInfo 的序列化、同步树节点和迁移流程属于 network-object-model。
10. 任务调优数据:CTuning#
很多任务(及事件)在类内声明 class Tunables : public CTuning 和静态 ms_Tunables,再用 IMPLEMENT_*_TASK_TUNABLES(Class, hash) 宏实现构造函数 game/task/System/Tuning.h:219-260。例:IMPLEMENT_COMBAT_TASK_TUNABLES(CTaskCombat, 0x29503dd8) game/task/Combat/TaskNewCombat.cpp:692,对应结构定义在 Combat/Metadata.psc 的 <structdef type="CTaskCombat::Tunables" base="CTuning"> game/task/Combat/Metadata.psc:950。
证据:静态注册与惰性创建管理器 game/task/System/Tuning.cpp:41-55、Tuning.cpp:485-528;Init(INIT_CORE) 调用 FinishInitialization Tuning.cpp:546-569;Load 的 PSO/XML 两条路径与默认值初始化、Finalize 回调 Tuning.cpp:103-216;文件名生成规则(设备前缀、data/tune 或 non_final/tune、组名转小写并去空格)Tuning.cpp:320-390。组名由宏决定:"Combat Tasks"、"Motion Tasks"、"Response Tasks"… 以及事件侧的 "Damage Events"、"Shocking Events" 等;除 Animation Tasks、Damage Events、Leader Events 用 XML 外,大多数组使用 PSO Tuning.h:230-256。__DEV 下 "Physics Tasks" 组路径可被 -nmfolder 参数覆盖 Tuning.cpp:356-362。BANK 构建提供“Tuning Manager”银行、按组加载/保存/从 assets 加载 Tuning.cpp:557-567、Tuning.cpp:218-230。AppendPhysicsTasks/RevertPhysicsTasks 支持把额外的 NM 调优集(flinch/shot/grab/jump roll/simple/brace)追加或回滚(用途推断为 DLC)Tuning.cpp:618-726。
全游戏 IMPLEMENT_*TUNABLES 宏出现在 175 个 .cpp 中(rg -c 统计),不止任务目录:派遣、通缉、目击者、骚乱、野生动物管理等也复用这套机制(统计于本次分析,未逐一列出)。
11. 调试设施#
| 设施 | 构建 | 说明 |
|---|---|---|
TASK_DETAILED_INFINITE_LOOP_TRAP(= __ASSERT) | 断言版 | 状态切换调用栈环形记录,UpdateTask 循环过多时打印 task.h:21、task.cpp:453-631 |
ENABLE_SIMPLE_TASK_STATE_HISTORY_DEBUG | FW_AI_OPTIMISATIONS_OFF(默认 0) | 每个任务的状态历史 task.h:22、rage/framework/src/ai/aioptimisations.h:10 |
aiTaskTree::ms_bSpewStateTransToTTY + ms_pFocusEntity | !__FINAL | 焦点实体的状态切换输出 tasktree.h:29-33 |
m_TaskHistory[8] | !__FINAL | 每棵树最近 8 个 SetTask 的类型 tasktree.h:108-111 |
DebugPlayback(DR_ENABLED) | 调试 | 记录 SetTask/AddSubTask/状态切换(CTaskFlowDebugEvent)rage/framework/src/ai/task/taskspew.h:27-39、taskspew.psc:4-6 |
CTask::VerifyTaskCounts | !__FINAL | 遍历任务池断言无孤儿任务 Task.cpp:286-320 |
game/ai/debug 的 CAIDebugInfo 派生类 | __BANK | 任务/事件/战斗等调试文本,见 navigation-and-pathserver 的 game/ai 盘点一节 |
约束与边界行为#
- 状态数 ≤ 255、任务类型 ≤ 65535、每树 ≤ 5 个优先级、每管理器 ≤ 4 棵树,均由位域或固定数组决定,超出即断言/越界。
- 单线程假设:
sm_TimeStep等静态量、DeleteTask的更新线程断言都假定任务在主更新线程运行;唯一的放宽是载具延迟任务期间的删除队列(容量 1280,溢出Quitf)。 - 默认任务永不为空:主树与移动树每帧补默认任务;在默认任务处理中或中止中替换默认槽会被静默丢弃(并删除传入任务),调用者无法从返回值得知。
- 远端行人:
SetTask对克隆行人静默丢弃;脚本给远端行人下任务需走NetworkInterface::GivePedScriptedTask(见 events-and-responses 的脚本注入一节)。 - 同帧多次状态切换被限制在约 41 轮;超过后剩余工作推迟到下一帧,断言版会报告疑似死循环。
- 中止 ≠ 删除:大多数任务被中止后仍留在槽里等待恢复,
GetDefaultStateAfterAbort与MayDeleteOnAbort的组合决定恢复语义;写新任务时必须同时考虑“首次进入”与“恢复进入”两种OnEnter。
未决问题 / 证据不足#
CTask池槽数、CEvent池槽数来自外部gameconfig.xml,源码树中不存在,本页未给出数值。ConvertPriority枚举的消费方未追踪。aiDeferredTasks工作线程中具体哪些任务代码在非主线程运行,属于载具 AI 页面范围,本页仅说明对任务删除的影响。