事件与响应(Events & Responses)
本页说明 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。
相关源码文件#
| 区域 | 文件 | 作用 |
|---|---|---|
| framework | rage/framework/src/fwevent/Event.h、EventQueue.h、EventRegdRef.h | fwEvent 基类、模板化固定容量队列、引用计数指针 |
rage/framework/src/ai/event/event.h | aiEvent:事件更新期间的时间步 | |
| game 核心 | game/event/Event.h/.cpp、EventDefines.h、EventPriorities.h、EventNames.*、system/EventData.h | CEvent、池、优先级枚举、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:
- 用
operator==查重,已有相同事件则返回已有实例(不重复入队)。 - 满了先
FlushExpiredEvents()(跳过BeingProcessed的事件)。 - 仍满则找“可移除的最低优先级事件”,仅当它的优先级严格低于新事件时替换。
- 入队的是
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_EVENTTYPEgame/event/system/EventData.h:19-282。文件头注释要求同时更新EventDataMeta.psc与脚本头event_enums.schEventData.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/DAMAGEgame/event/EventDamage.cpp:3064-3087;CEventScriptCommand对手机、同步场景、跳伞、喷气背包等“高优先级任务”提升到SCRIPT_COMMAND_SP(_NO_NM),bAcceptWhenDead时为E_PRIORITY_MAXgame/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.h | CEventDamage(可编辑,带物理响应任务)、CEventDeath、CEventIncapacitated、CEventWrithe、CEventHurtTransition |
EventMovement.h | CEventClimbLadderOnRoute、CEventClimbNavMeshOnRoute、CEventPotential{BeWalkedInto,GetRunOver,Blast,WalkIntoFire/Object/Vehicle}(均为临时事件) |
EventWeapon.h | CEventGunAimedAt、CEventGunShot → WhizzedBy → BulletImpact、CEventMeleeAction、CEventFriendlyAimedAt、CEventFriendlyFireNearMiss |
EventSound.h | CEventSoundBase → CEventFootStepHeard;CEventCommunicateEvent(包装转告的事件) |
EventLeader.h | CEventLeader{EnteredCarAsDriver,ExitedCarAsDriver,HolsteredWeapon,UnholsteredWeapon,EnteredCover,LeftCover} |
EventReactions.h | CEventReactionEnemyPed、CEventReactionInvestigateThreat/DeadPed、CEventExplosionHeard、CEventDisturbance… |
EventShocking.h | CEventShocking(可编辑)及约 50 个子类(CEventShockingCarCrash、…GunshotFired、…SeenPedKilled、…MadDriver…) |
EventScript.h | 投递给脚本的 CEventScriptWithData 族(CEventEntityDamaged、CEventCrimeReported、CEventStatChangedValue…) |
EventNetwork.h | CEventNetwork 及约 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") | 16 | CLockingEventGroupBase | 每个行人一个;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:
- 网络克隆行人一律拒绝(克隆不运行事件响应)。
event.AffectsPed(ped)为假则拒绝。- 联机时合并同一 AI 来源的重复
EVENT_SHOT_FIRED(注释称是为避免撑爆事件池的权宜之计)。 - 记录供脚本查询的“该行人收到过的事件”(
RecordEventForScript)。 - 加锁后走基类
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:
- 若全局组非空,
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。 AddEventsToScriptGroup:IsExposedToScripts()的事件克隆进SCRIPT_AIEventGroup.cpp:730-746。- 载具事件、
CInformFriendsEventQueue::Process、CInformGroupEventQueue::Process。 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:
- 目标是网络克隆或正在转移所有权 → 交给
NetworkInterface::GivePedScriptedTask(由网络层发给拥有者)game/script/script_cars_and_peds.cpp:1261-1267。 - 有“脚本大脑”的行人只能由自己的大脑脚本下任务,否则丢弃
script_cars_and_peds.cpp:1271-1283。 - 联网行人的序列任务长度受同步上限约束(断言)
script_cars_and_peds.cpp:1285-1299。 - 构造
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),并解析父级DecisionMakerParentRefEventDataManager.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各子类的触发点分布在网络代码中,本页未逐一追踪。- 时间线事件在运行时是否完全休眠仅基于源码检索推断。