CD Code Digest Hub /GTA V · v2

事件与响应(Events & Responses)

57 分钟阅读 13 个章节 2 张图

本页说明 AI 如何“感知—决策—响应”:RAGE framework 的 fwEvent/fwEventQueue/aiEvent 基础设施,游戏层 CEvent 类层次与事件类型/优先级,每个行人/载具/全局/脚本的事件组(队列),CEventHandler 如何从队列中选出最高优先级事件并把“响应任务”写入任务树槽位,数据驱动的决策制定器(decision makers,events.meta),惊吓事件(shocking events),以及事件在帧内的生产与销毁时机。最后说明 rage/suite 中名称相近但完全不同的“时间线事件”库。

背景与相关页面#

  • 任务树、槽位与 MakeAbortable 中止协议见 task-system;这里产生的响应任务就是那里的 PED_TASK_PRIORITY_* 槽的主要来源。
  • 响应任务本身(game/task/Response、威胁响应、逃跑、惊吓反应、激怒系统)见 task-catalogue。
  • 事件扫描器(CEventScanner 及其子扫描器)、关系组、感知、AI LOD 属于行人智能,见 ped-intelligence-and-population。
  • 网络事件 CEventNetwork* 的生成方(会话、邀请、交易等)见 network-sessions-and-transitions;网络中转的 AI 事件(如 CEventRespondedToThreat 跨机通信)见 network-events-and-arrays。
  • 脚本侧读取事件队列的原生命令见 natives-and-commands。
  • 碎片(fragment)使用的时间线事件见 fragments-cloth-glass-rope。

相关源码文件#

区域文件作用
frameworkrage/framework/src/fwevent/Event.h、EventQueue.h、EventRegdRef.hfwEvent 基类、模板化固定容量队列、引用计数指针
rage/framework/src/ai/event/event.haiEvent:事件更新期间的时间步
game 核心game/event/Event.h/.cpp、EventDefines.h、EventPriorities.h、EventNames.*、system/EventData.hCEvent、池、优先级枚举、eEventType 枚举与名字表
事件族game/event/Events.*、EventDamage.*、EventMovement.*、EventWeapon.*、EventSound.*、EventLeader.*、EventReactions.*、EventScript.*、EventShocking.*、EventNetwork.*、EventErrors.*、EventEditable.*具体事件类
队列/处理game/event/EventGroup.*、EventHandler.*、EventResponse.h、EventResponseFactory.*事件组、处理器、响应容器、按任务类型的响应工厂
决策game/event/decision/*、game/event/system/EventDataManager.*、EventDataMeta.psc决策制定器(运行时 + events.meta 元数据)
惊吓事件game/event/ShockingEvents.*、ShockingEventEnums.h、EventShocking.*/.psc惊吓事件管理与调优
时间线事件rage/suite/src/event/*、rage/suite/src/eventtypes/*evtType/evtInstance/evtSet/evtTimeline/evtPlayer,与 AI 无关

1. 三种“事件”不要混淆#

名称基类用途生命周期
AI 事件fwEvent → aiEvent → CEvent行人/载具感知到的刺激(受伤、枪声、碰撞、惊吓…),驱动响应任务池分配,存在于事件组队列中,按 tick 过期
脚本/网络事件fwEvent → CEventNetwork(独立池)、CEvent → CEventScript*/CEventError*投递给脚本的通知(会话、拾取、伤害实体、统计变化、脚本错误)进入 SCRIPT_* 事件组,脚本帧结束时清空
时间线事件rage::evtInstance/evtType动画/碎片时间轴上的 ptfx/音效触发资源数据,由 evtPlayer 播放

CEventNetwork : public fwEvent 并自带池,最多 MAX_NETWORK_EVENTS = 500 个 game/event/EventNetwork.h:582-594;CEvent : public aiEvent game/event/Event.h:40;时间线事件见 §9。

2. 框架层:fwEvent、fwEventQueue、aiEvent#

fwEvent 是带引用计数(fwRefAwareBase)的抽象事件 rage/framework/src/fwevent/EventRegdRef.h:31-35,要求实现 Clone、operator==、GetEventType、GetEventPriority、RetrieveData;内部计时器 m_AccumulatedTime 是“tick 计数”而非毫秒,HasExpired() 为 (!persistent && accumulated > GetLifeTime()) || flagForDeletion,默认 GetLifeTime() 返回 0;HasPriority 默认比较 GetEventPriority() rage/framework/src/fwevent/Event.h:28-105。结论:未设置持久化且未覆盖寿命的事件,被 tick 一次后即过期(推断自上述公式与 TickEvents 的调用方式,见 §5)。

fwEventQueue<Size, Parent> 是固定容量队列(atQueue<RegdfwEvent, Size>),Add(event) 算法 rage/framework/src/fwevent/EventQueue.h:37-79:

  1. 用 operator== 查重,已有相同事件则返回已有实例(不重复入队)。
  2. 满了先 FlushExpiredEvents()(跳过 BeingProcessed 的事件)。
  3. 仍满则找“可移除的最低优先级事件”,仅当它的优先级严格低于新事件时替换。
  4. 入队的是 event.Clone(),调用方传入的事件对象始终归调用方所有。

带 _Parent 参数的特化把每个操作包进 Lock/Unlock(FWEVENTQUEUE_CRITICAL_SECTION)或只做断言的“保护区”(FWEVENTQUEUE_PROTECTED_SECTION)EventQueue.h:312-470。aiEvent 只增加静态时间步:BeginEventUpdates/EndEventUpdates 包裹一次事件处理,BeginEventQuery 用于“只查询不推进计时”的场合 rage/framework/src/ai/event/event.h:15-60。

3. CEvent:游戏层事件#

3.1 类型枚举、名字表与优先级#

  • eEventType(game/event/system/EventData.h)分三段:AI 事件到 NUM_AI_EVENTTYPE,网络事件到 NUM_NETWORK_EVENTTYPE,脚本错误事件到 NUM_ERRORS_EVENTTYPE,总数 NUM_EVENTTYPE game/event/system/EventData.h:19-282。文件头注释要求同时更新 EventDataMeta.psc 与脚本头 event_enums.sch EventData.h:15-18;EventData.cpp 以 CompileTimeAssert(parser_eEventType_Count == eEVENTTYPE_MAX+2) 保证 C++ 枚举与 parser 枚举一致 game/event/system/EventData.cpp:16。
  • 非 FINAL 构建中 CEventNames::AddEventNames() 用 E2N(EVENT_X, sizeof(CEventX)) 登记名字与大小 game/event/EventNames.cpp:24-40;CEventGroupAI::Add 断言新事件已在名字表中 game/event/EventGroup.h:49-53。
  • 优先级是单独的 eEventPriorityList 枚举,从环境琐事(E_PRIORITY_RIDE_TRAIN)一路到 E_PRIORITY_REVIVED,数值越大越优先 game/event/EventPriorities.h:9-176。大致分层:环境/调查 < 惊吓(有趣/影响他人)< 关系反应 < 潜在碰撞 < 惊吓(潜在危险)< 激怒 < 惊吓(危险/严重危险)< 脚本带数据事件 < 枪击/近失 < 呼叫掩护/喊话 < 劫车 < 领队命令 < 被瞄准/爆炸 < E_PRIORITY_NEW_TASK/SCRIPT_COMMAND/GIVE_PED_TASK < 碰撞 < 空中/水中 < 伤害 < 近战伤害/着火 < 路线攀爬/卡住 < SCRIPT_COMMAND_SP_NO_NM < 起身/翻滚 < 戴手铐 < 布娃娃伤害 < SCRIPT_COMMAND_SP < 失能 < 死亡 < 布娃娃死亡 < 被拖走 < 复活 EventPriorities.h:11-175。注释说明了若干排序意图,例如路线攀爬必须高于伤害事件,否则追击中的行人永远无法完成攀爬 EventPriorities.h:132-136。
  • 同一事件类可动态返回不同优先级:CEventDamage::GetEventPriority 依据是否致死、是否布娃娃、武器是否近战返回 DEATH(_WITH_RAGDOLL)/DAMAGE_WITH_RAGDOLL/DAMAGE_BY_MELEE/DAMAGE game/event/EventDamage.cpp:3064-3087;CEventScriptCommand 对手机、同步场景、跳伞、喷气背包等“高优先级任务”提升到 SCRIPT_COMMAND_SP(_NO_NM),bAcceptWhenDead 时为 E_PRIORITY_MAX game/event/Events.cpp:2830-2880。

3.2 虚接口#

game/event/Event.h:40-212 定义的主要钩子:

方法用途
AffectsPedCore(ped) / AffectsVehicleCore(veh)入队前过滤:该实体是否“感知/关心”此事件(感知范围、关系、室内外等)
IsValidForPed、IsEventReady出队时有效性与延迟
IsTemporaryEvent()临时事件(导航类、领队类、脚本命令等)→ 响应进入 EVENT_RESPONSE_TEMP 槽
HasEditableResponse()是否由决策制定器决定响应(CEventEditableResponse 返回 true)
CanBeInterruptedBySameEvent()是否允许同类型事件打断当前响应
TakesPriorityOver(other) / HasPriority默认比较优先级;子类可覆盖(如领队事件对 EVENT_IN_WATER 让步 game/event/EventLeader.h:55)
ShouldCreateResponseTaskForPed、CreateResponseTask(ped, response)、GetResponseTaskType()生成响应;默认实现调用 CEventResponseFactory game/event/Event.cpp:163-167
RequiresAbortForRagdoll() / RequiresAbortForMelee()要求当前任务必须让位(布娃娃/近战)
IsExposedToPed/Vehicles/Scripts()、ProcessInGlobalGroup()全局组的分发与保留策略
CommunicateEvent(ped, input)把事件转告友方(见 §4.3)
CanDeleteActiveTask()脚本/给定任务事件可清掉其与目标槽之间的任务

OnEventAdded 对需要布娃娃中止的事件强制该行人下一帧不做时间切片 Event.cpp:216-223。

3.3 内存与延迟#

  • 池:FW_INSTANTIATE_BASECLASS_POOL_SPILLOVER(CEvent, CONFIGURED_FROM_FILE, 0.55f, "CEvent", LARGEST_EVENT_CLASS),槽大小为 sizeof(CEventShocking)(注释认为它是最大的事件类,并有 CompileTimeAssert 校验)game/event/Event.cpp:40-52;CEvent 的 new/delete 带锁(FW_REGISTER_LOCKING_CLASS_POOL + fwPoolCriticalSection)Event.h:38-44。CanCreateEvent(n) 在空闲槽不足时断言并返回 false,__DEV 下首次还会转储整池 Event.cpp:80-105。池在场景初始化时创建,和 CEventNetwork 池相邻 game/scene/scene.cpp:479-480。__DEV 下自定义池分配器会断言子类大小不超过槽大小 game/event/EventDefines.h:47-89。
  • 反应延迟:首次 IsEventReady 时通过 GetTunableDelay(ped, min, max) 随机取延迟;联机中屏幕内任务行人同一毫秒同类型事件的延迟若相差 < 0.1 s 会被再错开 0.1 s,避免多人动作同步;延迟结束时清零累计 tick,防止刚“就绪”就过期 Event.cpp:551-623。CEvent::HasExpired 在延迟期间不判定过期 Event.h:65-68。

3.4 事件类族#

头文件代表类(基类)
Events.h碰撞 CEventVehicleCollision/CEventPedCollisionWithPed/CEventObjectCollision;关系 CEventAcquaintancePed{Hate,Dislike,Like,Respect,Dead,Wanted}(可编辑);CEventExplosion、CEventDraggedOutCar、CEventPedJackingMyVehicle、CEventScriptCommand、CEventGivePedTask、CEventInAir、CEventInWater、CEventOnFire、CEventCallForCover、CEventShoutTargetPosition、CEventNewTask、CEventStaticCountReachedMax、CEventAgitated、CEventScenarioForceAction、CEventPlayerDeath 等
EventDamage.hCEventDamage(可编辑,带物理响应任务)、CEventDeath、CEventIncapacitated、CEventWrithe、CEventHurtTransition
EventMovement.hCEventClimbLadderOnRoute、CEventClimbNavMeshOnRoute、CEventPotential{BeWalkedInto,GetRunOver,Blast,WalkIntoFire/Object/Vehicle}(均为临时事件)
EventWeapon.hCEventGunAimedAt、CEventGunShot → WhizzedBy → BulletImpact、CEventMeleeAction、CEventFriendlyAimedAt、CEventFriendlyFireNearMiss
EventSound.hCEventSoundBase → CEventFootStepHeard;CEventCommunicateEvent(包装转告的事件)
EventLeader.hCEventLeader{EnteredCarAsDriver,ExitedCarAsDriver,HolsteredWeapon,UnholsteredWeapon,EnteredCover,LeftCover}
EventReactions.hCEventReactionEnemyPed、CEventReactionInvestigateThreat/DeadPed、CEventExplosionHeard、CEventDisturbance…
EventShocking.hCEventShocking(可编辑)及约 50 个子类(CEventShockingCarCrash、…GunshotFired、…SeenPedKilled、…MadDriver…)
EventScript.h投递给脚本的 CEventScriptWithData 族(CEventEntityDamaged、CEventCrimeReported、CEventStatChangedValue…)
EventNetwork.hCEventNetwork 及约 80 个会话/邀请/商城/社团等网络通知
EventErrors.h脚本运行错误(数组越界、指令数超限、栈溢出、未知错误)

以上继承关系由类声明确认:Events.h、EventDamage.h、EventMovement.h、EventWeapon.h、EventSound.h、EventLeader.h、EventReactions.h、EventShocking.h、EventScript.h、EventNetwork.h、EventErrors.h 中 class CEventX : public CEventY 的声明(例如 CEventShocking : public CEventEditableResponse game/event/EventShocking.h:15,CEventEditableResponse : public CEvent game/event/EventEditable.h:11)。临时事件的标识可在各头文件 IsTemporaryEvent() const { return true; } 处查到,例如 game/event/EventMovement.h:42。

4. 事件组(队列)与事件来源#

4.1 事件组一览#

组容量锁用途
CEventGroupPed("PED_AI")16CLockingEventGroupBase每个行人一个;CPedIntelligence::m_eventGroup
CEventGroupVehicle("VEHICLE_AI")4同上每辆载具一个
CEventGroupGlobal("GLOBAL_AI")32同上单例;惊吓事件等影响所有人的事件
CEventGroupScriptAI("SCRIPT_AI")32无暴露给脚本的 AI 事件
CEventGroupScriptErrors("SCRIPT_ERRORS")32无脚本错误事件
CEventGroupScriptNetwork("SCRIPT_NETWORK")500无网络事件

容量与名字见 game/event/EventGroup.h:23-27、EventGroup.h:213-374。锁:CLockingEventGroupBase::Lock 只有在载具 AI 延迟任务并行运行(aiDeferredTasks::g_Running)时才真正锁 g_EventGroupToken,平时是空操作;“保护区”只断言已加锁 EventGroup.h:148-203。所有 AI 组的 Add 先通知网络层 NetworkInterface::OnAIEventAdded,并在 CEvent::CanCreateEvent() 失败时直接丢弃 EventGroup.h:39-68。

脚本组在脚本线程更新期间(CTheScripts::GetUpdatingTheThreads())把新事件写到“待定队列”,避免脚本遍历时队列被改 EventGroup.h:84-142;每帧脚本处理结束时调用 FlushAllAndMovePending()——清空主队列并把待定事件移入,因此脚本看到的是“上一次脚本更新之后产生的事件”,且每个事件只对一轮脚本可见 game/script/script.cpp:1151-1154。

4.2 行人事件组的入队规则#

CEventGroupPed::Add game/event/EventGroup.cpp:32-120:

  1. 网络克隆行人一律拒绝(克隆不运行事件响应)。
  2. event.AffectsPed(ped) 为假则拒绝。
  3. 联机时合并同一 AI 来源的重复 EVENT_SHOT_FIRED(注释称是为避免撑爆事件池的权宜之计)。
  4. 记录供脚本查询的“该行人收到过的事件”(RecordEventForScript)。
  5. 加锁后走基类 Add(查重、过期清理、低优先级替换、克隆入队),成功后 OnEventAdded。

GetLowestPriorityEventThatCanBeRemoved 覆盖为永不移除 EVENT_SCRIPT_COMMAND EventGroup.cpp:122-144。CPedIntelligence::AddEvent(ev, bForcePersistence, bRemoveOtherEventsToMakeSpace) 是上层入口:组满且不允许腾位时直接失败;成功后可强制持久化 game/Peds/PedIntelligence.h:1600-1620。

4.3 事件来源#

来源机制证据
感知扫描CEventScanner::ScanForEvents 组合多个扫描器(熟人/关系、激怒、侵入个人空间、附近火焰、乘客、编组、惊吓、水中、物理、静止卡住、碰撞、健康),多数继承 CExpensiveProcess 分帧执行game/Peds/PedEventScanner.h:926-990、game/ai/ExpensiveProcess.h:27-75
任务自身例如导航任务在路线上遇到梯子/攀爬点时向自身行人添加 CEventClimbLadderOnRoute/CEventClimbNavMeshOnRoute,由事件处理器产生 CTaskUseLadderOnRoute 等临时响应game/task/Movement/TaskNavBase.cpp:3424-3425、game/event/EventMovement.cpp:66-79
行人间指令CEventGivePedTask 把任务推给另一个行人(如劫车时让被劫者执行 CTaskExitVehicle,优先级 EVENT_RESPONSE_TEMP)game/task/Vehicle/TaskEnterVehicle.cpp:6900、game/event/Events.cpp:3003-3005
转告友方CEvent::CommunicateEvent:用行人空间数组找出半径内的人(无线电 900 m/否则 50 m,执法有无线电时用前者);双方都有无线电则直接克隆事件,否则包成 CEventCommunicateEvent(需要能“听到”);交给 CInformFriendsEventQueue(64 槽,同一行人同类型去重,延迟 0 ms——随机 300–800 ms 的代码已被注释掉);联机时对克隆行人改走网络game/event/Event.cpp:225-293、game/event/Event.h:171-189、game/event/Events.h:87-112、game/event/Events.cpp:157-200、Events.cpp:279-335
脚本CEventScriptCommand(见 §6)—
全局惊吓事件、爆炸声等进入 CEventGroupGlobal(见 §4.4、§8)—

4.4 全局事件组的帧流程#

CGameWorld::Process 在动画与 AI 更新之前调用 ProcessGlobalEvents() game/scene/world/GameWorld.cpp:1840-1846:

  1. 若全局组非空,fwSceneUpdate::Update(SU_ADD_SHOCKING_EVENTS) 对每个行人回调 AddEventsToPed:死亡行人跳过;对 IsExposedToPed(ped) 的事件克隆后 AddEvent(随后仍需通过该行人的 AffectsPed 过滤)GameWorld.cpp:1116-1120、GameWorld.cpp:2643-2648、game/event/EventGroup.cpp:683-704。
  2. AddEventsToScriptGroup:IsExposedToScripts() 的事件克隆进 SCRIPT_AI EventGroup.cpp:730-746。
  3. 载具事件、CInformFriendsEventQueue::Process、CInformGroupEventQueue::Process。
  4. ProcessAndFlush():对每个事件调用 ProcessInGlobalGroup(),返回 true 的保留(惊吓事件在未过期前保留并维持行人生成屏蔽区),其余删除 EventGroup.cpp:661-681、GameWorld.cpp:1110-1135。

AddOrCombine 对 EVENT_EXPLOSION_HEARD 与 EVENT_SUSPICIOUS_ACTIVITY 尝试与已有事件合并,避免全局组被同类事件塞满 EventGroup.cpp:608-658。

5. CEventHandler::HandleEvents:从事件到任务#

5.1 选出最高优先级事件#

CEventGroupPed::GetHighestPriorityEvent game/event/EventGroup.cpp:197-300 遍历队列,候选需同时满足 IsEventReady(延迟已过)、IsValidForPed、AffectsPed;可编辑事件还需“尚未计算响应”或“计算结果是会响应”。脚本命令事件只在严格大于当前最高时替换(同级不并列),其他事件允许并列,最终在并列者中选距离行人最近的一个。遍历过程中被跳过的低优先级事件也会调用 IsEventReady,以推进其延迟计时(注释提到枪声事件依赖这一点)。

5.2 事件历史:临时 / 非临时 / 暂存#

CEventHandlerHistory 记录:当前非临时事件(“active”,如伤害、仇恨)、当前临时事件(“passive”,多为导航类)、被临时事件挤掉而暂存的非临时事件、当前移动事件,以及正在被中止以腾位的响应任务 game/event/EventHandler.h:18-103。规则 game/event/EventHandler.cpp:193-244:

  • 正在响应非临时事件时,新事件只需高于它。
  • 正在响应临时事件时,新的临时事件只需高于临时事件;新的非临时事件还需高于被暂存的非临时事件。
  • 临时事件处理完(临时槽空)后,TickStoredEvent 把暂存的非临时事件恢复为当前事件。

5.3 主流程#

证据:game/event/EventHandler.cpp:275-690。细节:

  • 阻断非临时事件:行人设置了 BlockingOfNonTemporaryEvents(脚本常用)时,优先级低于 E_PRIORITY_BLOCK_NON_TEMP_EVENTS 的非临时事件被丢弃,但脚本命令、领队上下车、场景强制动作例外 EventHandler.cpp:399-416。
  • 同类事件不重复响应:若当前响应的事件类型与新事件相同且不允许同类打断,则标记删除新事件;当没有“当前事件”记录时(例如所有权迁移后),改用响应任务的 GetIsResponseForEventType() 判定 EventHandler.cpp:418-456。
  • 死亡事件兜底:当前事件是优先级 E_PRIORITY_DRAGGING 的 EVENT_GIVE_PED_TASK 但并未在跑 TASK_DRAGGED_TO_SAFETY 时,EVENT_DEATH 强制获得优先权(修复死亡事件被永久阻塞的 bug)EventHandler.cpp:372-388。
  • 中止当前任务:主/物理响应要求主树(或 RunOnSecondary 时次树)活动任务 MakeAbortable(URGENT);失败时若行人正在布娃娃且事件需要布娃娃/近战中止,则除 TASK_DYING_DEAD、TASK_NM_SCRIPT_CONTROL、TASK_INCAPACITATED 与 NM 行为任务外,强制 IMMEDIATE(__DEV 会警告该任务未正确让位);中止成功且任务 MayDeleteOnAbort()、且不是默认任务时直接删除它 EventHandler.cpp:528-620。移动响应只中止活动移动任务 EventHandler.cpp:621-637。
  • 等待中止完成:若当前任务拒绝立即中止,事件被 UnTick()(保持不过期)留到下一帧,响应任务被丢弃并在下一帧重算;历史中记录“正在被中止的任务”,直到它真正结束 EventHandler.cpp:646-679。
  • 结尾 RemoveInvalidEvents(false, true) 只标记无效/不会响应的事件为待删除 EventHandler.cpp:687-690、EventGroup.cpp:146-195;真正删除发生在 CPed::PostPreRender 中的 RemoveExpiredEvents() → FlushExpiredEvents() game/Peds/Ped.cpp:23728、PedIntelligence.h:1688-1695,或队列满时的 Add 内。

事件处理只在完整 AI 更新中执行,且断言行人不是网络克隆 game/Peds/PedIntelligence.cpp:1313-1326、EventHandler.cpp:277。

5.4 响应容器与槽位映射#

CEventResponse 最多持有三个任务:EventResponse(主)、PhysicalResponse(物理,如受击反应)、MovementResponse(移动),外加 OverrideResponseAsNonTemp 与 RunOnSecondary 两个标志;SetEventResponse 断言移动任务只能放在 MovementResponse game/event/EventResponse.h:7-85。SetEventResponseTask 的写入规则 EventHandler.cpp:726-856:

条件写入位置
EVENT_SCRIPT_COMMAND / EVENT_GIVE_PED_TASK事件自带的 iTaskPriority 槽;写 TEMP 前清临时响应,写 ≥NONTEMP 前清事件响应;CanDeleteActiveTask() 时 ClearTasksAbovePriority;脚本命令还会把任务与 CPedScriptedTaskRecord 关联
RunOnSecondary次树 PED_TASK_SECONDARY_PARTIAL_ANIM(如环境注视、激进围观)game/event/EventResponseFactory.cpp:509-531
临时事件且未覆盖为非临时主树 EVENT_RESPONSE_TEMP
其他清 EVENT_RESPONSE_TEMP 后写 EVENT_RESPONSE_NONTEMP
物理响应主树 PHYSICAL_RESPONSE;若没有新的物理响应则清空该槽
移动响应移动树 PED_TASK_MOVEMENT_EVENT_RESPONSE(AddTaskMovementResponse)

最后从队列移除已响应的事件。例:CEventDamage 在非致死时放入物理响应任务 game/event/EventDamage.cpp:3852-3857;CEventPotentialWalkIntoObject 等产生 CTaskWalkRoundEntity 作为移动响应 game/event/EventMovement.cpp:934。

6. 脚本与任务注入#

脚本的 TASK_* 原生命令最终都经由 CScriptPeds::GivePedScriptedTask:

  1. 目标是网络克隆或正在转移所有权 → 交给 NetworkInterface::GivePedScriptedTask(由网络层发给拥有者)game/script/script_cars_and_peds.cpp:1261-1267。
  2. 有“脚本大脑”的行人只能由自己的大脑脚本下任务,否则丢弃 script_cars_and_peds.cpp:1271-1283。
  3. 联网行人的序列任务长度受同步上限约束(断言)script_cars_and_peds.cpp:1285-1299。
  4. 构造 CEventScriptCommand(PED_TASK_PRIORITY_PRIMARY, pTask),先移除已有脚本命令事件,强制下一帧完整 AI 更新,AddEvent 后登记 CPedScriptedTaskRecord(供 GET_SCRIPT_TASK_STATUS 一类查询)script_cars_and_peds.cpp:1301-1321。在序列打开时则改为追加到序列表。

CEventScriptCommand 的构造函数是私有的,只对脚本/网络相关类开放,代码侧应使用 CEventGivePedTask game/event/Events.h:804-834。它是临时事件,创建响应时克隆脚本任务;当优先级为 SCRIPT_COMMAND_SP 且行人正在布娃娃、而任务不处理布娃娃时,先 SwitchToAnimated() Events.cpp:2985-2997。由于脚本任务进入的是 PRIMARY 槽,它会被任何事件响应(TEMP/NONTEMP/PHYSICAL)暂时压过,结束后再恢复——这就是“被打断后继续执行脚本任务”的来源(结论来自槽位语义,见 task-system)。

7. 决策制定器(Decision Makers)#

可编辑事件(CEventEditableResponse 子类:关系事件、伤害、枪击/瞄准、爆炸、惊吓、潜在碰撞等)不直接写死响应,而是查询行人当前的决策制定器。

7.1 运行时结构#

证据:ComputeResponseTaskType 只计算一次并写入 m_iTaskType、逃跑标志与台词哈希 game/event/EventEditable.cpp:35-54;CPedEventDecisionMaker 维护按事件类型的冷却记录(Cooldown.Time/MaxDistance)game/Peds/PedIntelligence.h:74-140;CEventDecisionMaker 池 MAX_STORAGE = 25,可修改组件池 3、每组件动态响应 4 种、屏蔽事件 4 种 game/event/decision/EventDecisionMaker.h:23-104;决策顺序(屏蔽→动态组件→元数据)game/event/decision/EventDecisionMaker.cpp:172-198;元数据查找使用排序缓存二分并递归父级 EventDecisionMaker.cpp:275-323;事件来源分类(威胁/友方/玩家/未定义)EventDecisionMaker.cpp:248-273。脚本可 Clone 一个决策制定器得到带可修改组件的新实例,再添加响应或屏蔽事件 EventDecisionMaker.cpp:200-246。

7.2 单条响应的决策#

CEventDecisionMakerResponse::MakeDecision game/event/decision/EventDecisionMakerResponse.cpp:55-103:对该事件类型下每个 CEventDataResponse_Decision 计算“机会值”,求和后在 [ε, 总和] 上随机抽取。单个决策的机会值 EventDecisionMakerResponse.cpp:105-140、EventDecisionMakerResponse.cpp:611-620:

  • 先过 ValidOnFoot/ValidInCar/ValidInHeli/ValidOnBicycle 等载具状态位;
  • 距离平方在 [DistanceMinSq, DistanceMaxSq] 内(-1 表示不限);
  • 场景、人行横道、恐惧、司机/乘客、来源是否持枪、是否动物、是否友方、室内外、是否联机任务行人等约 50 个决策标志 game/event/decision/EventResponseDecisionFlags.psc:6-53;
  • 通过后按来源类型取 Chance_SourcePlayer/Friend/Threat/Other。

数据结构(parser schema):CEventDecisionMakerResponse { m_Name, m_Event, m_Cooldown{Time, MaxDistance}, m_Decision[] },每个 Decision { TaskRef, Chance_*, DistanceMin/MaxSq, SpeechHash, EventResponseDecisionFlags, EventResponseFleeFlags } game/event/decision/EventDecisionMakerResponse.psc:4-34。

7.3 元数据加载与“响应任务”映射#

  • CEventDataManager 在 INIT_AFTER_MAP_LOADED 时从 common:/data/events(.meta)加载三个数组:响应任务定义、决策响应、决策制定器,然后初始化决策制定器池;同时注册 EVENTS_OVERRIDE_FILE 数据文件挂载器,DLC 可整体覆盖并在卸载时回滚到默认文件 game/event/system/EventDataManager.cpp:26-86、game/event/system/EventDataMeta.psc:5-13。
  • 加载后对每个决策制定器 PostLoad:把其响应按事件类型排序(保留原始顺序作为次键)建立平行缓存数组(上限 MAX_EVENTRESPONSES = 46),并解析父级 DecisionMakerParentRef EventDataManager.cpp:145-163、game/event/system/EventData.cpp:44-96、EventData.h:722-728。
  • 决策的 TaskRef 指向一条 CEventDataResponseTask 子类实例(CEventDataResponseTaskCombat、…Flee、…ShockingEventWatch…),CEventEditableResponse::FindTaskTypeForResponse 按其 parser 结构类型映射到 eTaskType(约 40 种,例如 Combat→TASK_COMBAT、Threat→TASK_THREAT_RESPONSE、Flee→TASK_SMART_FLEE、HeadTrack→TASK_AMBIENT_LOOK_AT_EVENT、DeferToScenarioPointFlags→TASK_DEFER_TO_SCENARIO)game/event/EventEditable.cpp:57-152、EventData.h:302-625。
  • CEventResponseFactory::CreateEventResponse(ped, event, taskType, response) 是按任务类型的大型 switch,构造带事件上下文的具体任务:例如 TASK_SMART_FLEE 实际创建带逃跑覆盖的 CTaskThreatResponse(对来源行人或载具司机),并在非友方时登记威胁;惊吓事件则以事件位置逃跑 game/event/EventResponseFactory.cpp:59-140。

8. 惊吓事件(Shocking Events)#

惊吓事件模拟“路人看到/听到不寻常的事”(车祸、枪声、尸体、疯狂驾驶、好车…),是城市氛围反应的主要来源。

方面实现
类型CEventShocking : CEventEditableResponse 及约 50 个子类,每个子类通过 DECLARE_SHOCKING_EVENT_TUNABLES 拥有静态 sm_Tunables,且 GetTunableDelay 直接取调优中的最小/最大延迟 game/event/EventShocking.h:392-398
调优Tunables : CTuning,字段含寿命、视觉/听觉/车内警察反应距离、仅 AI 时距离缩放、观看/逃离/拍照时长与概率、行人生成屏蔽半径、闲逛影响球半径等 EventShocking.h:32-120;全部归入 "Shocking Events" 调优组(PSO)game/event/EventShocking.cpp:651-664
入口CShockingEventsManager::Add(ev, createdByScript, overrideLifeTime):全局禁用或该等级被抑制则返回 0;设开始时间与寿命(脚本创建且寿命 0 视为无限);把 m_Priority(EInteresting…ESeriousDanger)映射到 E_PRIORITY_SHOCKING_EVENT_*;非脚本事件若已存在等价事件则只更新它;最后加入全局组并返回事件 Id game/event/ShockingEvents.cpp:40-156、game/event/ShockingEventEnums.h:8-16
抑制脚本可按等级抑制新惊吓事件,抑制位在每帧脚本运行前 ProcessPreScripts 清零(即必须每帧重新设置)game/event/ShockingEvents.h:46-73
持续在全局组中 ProcessInGlobalGroup 未过期时保留,并按调优在事件位置维持/更新 pathserver 的行人生成屏蔽区(避免在枪战附近刷人)EventShocking.cpp:107-139
感知每帧通过全局组复制给所有行人,再由各行人的 AffectsPedCore 用感知范围(仅 AI 参与时可缩放)判定 EventShocking.cpp:455-460;行人侧另有 CShockingEventScanner 分帧扫描(PPT_ShockingEventScanner 间隔 10 帧、每帧最多 8 个)game/ai/ExpensiveProcess.cpp:84
响应经决策制定器映射到 TASK_SHOCKING_EVENT_WATCH/GOTO/HURRYAWAY/REACT/BACK_AWAY/STOP_AND_STARE/REACT_TO_AIRCRAFT/THREAT_RESPONSE、TASK_SHOCKING_POLICE_INVESTIGATE、TASK_SHOCKING_NICE_CAR_PICTURE 等(game/task/Response/TaskShockingEvents.*,见 task-catalogue)

9. rage/suite:时间线事件库(与 AI 无关)#

rage/suite/src/event 提供一套资源化的“时间线事件”框架:

类型职责
evtType事件类型的行为:Start/Update/Stop/Modified 回调;Init<_Instance>() 注册实例工厂与资源放置工厂 rage/suite/src/event/type.h:20-44
evtInstance一个事件实例:[MinT, MaxT] 区间、正放/倒放/混入/混出播放标志、优先级、轨道号,可资源化(DECLARE_PLACE)rage/suite/src/event/instance.h:21-82
evtTimeline / evtSet按时间排序的实例集合 / 一起触发的实例集合(如物体破碎时)rage/suite/src/event/timeline.h:19-63、rage/suite/src/event/set.h:23-65
evtPlayer沿参数化时间 t 播放时间线或事件集,同时活跃实例上限 MAX_SIMULTANEOUS_EVENTS = 40 rage/suite/src/event/player.h:21-133
evtTypeManager(EVENTMGR)按组/名注册类型、按名/哈希查找 rage/suite/src/event/manager.h:20-58

rage/suite/src/eventtypes 提供三个具体类型:粒子(evtTypePtfx)、音效单次(evtTypeAudOneShot)、调试打印/画刻度 rage/suite/src/eventtypes/ptfx.h:26-60、rage/suite/src/eventtypes/sound.h:26-62、rage/suite/src/eventtypes/debug.h:19-74。

使用情况:rage/suite/src/event/*.cpp 被编入游戏的 RageMisc 工程 game/VS_Project/RageMisc/RageMisc_2015_unity.vcxproj:6466-6493,其消费者是碎片系统(fragCollisionEventPlayer/fragBreakEventPlayer/fragContinuousEventPlayer,ENABLE_FRAGMENT_EVENTS = 1)rage/suite/src/fragment/eventplayer.h:14-90、rage/suite/src/fragment/fragmentconfig.h:19。碎片调优代码在使用前检查 evtTypeManagerSingleton::IsInstantiated() rage/suite/src/fragment/tune.cpp:1579-1581。在 src/dev_ng 中检索 INIT_EVENTMGR/RegisterTypeInstancePair 只命中已弃用工具 srorc rage/suite/tools/deprecated/srorc/srorc.cpp:110-122,game 目录中没有实例化 evtTypeManager 的代码。这表明在 GTA V 运行时该管理器很可能未被创建、时间线事件处于休眠状态(推断:基于检索未命中,未在运行时验证)。rage/suite/src/eventtypes 也未出现在游戏工程中(游戏工程里的 eventtypes.* 是另一个文件 fragment/eventtypes.*)。

约束与边界行为#

  • 容量:行人事件组仅 16 槽;满时以“新事件严格高于最低可移除事件”才替换,脚本命令永不被挤出;CEvent 池满时所有 Add 静默失败(非 FINAL 下断言)。
  • 寿命以 tick 计:默认寿命 0,未持久化的事件在一次 HandleEvents 后过期;延迟中的事件不过期;因 AI 时间切片,HandleEvents 只在完整更新时运行,tick 频率随 LOD 降低(推断,基于 §5.3 的调用条件)。
  • 克隆行人不处理事件:克隆的事件组拒绝入队,HandleEvents 断言非克隆;远端行人的行为来自 CTaskTreeClone。
  • 决策只算一次:可编辑事件的响应类型在首次参与选择时固定,之后即使上下文变化也不重算(ComputeResponseTaskType 断言未计算过)。
  • 响应失败的回退:决策结果为“不响应”时,处理器会在同一帧尝试次高事件;但为避免死循环,若取回的仍是同一事件则放弃(aiVerifyf)EventHandler.cpp:475-495。
  • 脚本可见性:脚本组事件只存活一轮脚本更新;需要持续关注的状态应由脚本自行记录。

未决问题 / 证据不足#

  • events.meta 本身是数据资产,不在源码树中;具体哪种行人使用哪个决策制定器、各事件的机会值未核实。
  • CEventNetwork 各子类的触发点分布在网络代码中,本页未逐一追踪。
  • 时间线事件在运行时是否完全休眠仅基于源码检索推断。