游戏动画接入:fwAnimDirector、MoVE 接口、Clip Set、AnimScene 与 IK
本页说明 RAGE 动画核心(见 rage-animation-and-move.md)如何被框架层(rage/framework/src/fwanimation)和游戏层(game/animation、game/ik)装配成每个实体的动画栈:fwAnimDirector 及其组件、三棵动作树的内容、MoVE 参数的跨线程数据流、clip 字典/clip set/network def 的流式与驻留策略、CMovePed 的网络槽位、面部动画接入、AnimScene(游戏内编排场景)、以及 IK 管理器与求解器池。
背景与相关页面#
- 动画核心数据结构、动作树、MoVE 运行时、帧内时序总表:rage-animation-and-move.md。
- 过场动画(另一套时间轴系统,与 synced scene/AnimScene 不同):cutscenes.md。
CPed架构与 ped 渲染侧的PreRender:../06-peds/ped-architecture.md、../06-peds/ped-motion-and-rendering.md。- 动画任务(
CTaskScriptedAnimation、CTaskCutScene、运动任务等)与CMoveNetworkHelper的使用:../07-ai-tasks/task-catalogue.md。 - 资产 store 与流式请求:../04-rage-framework/streaming-engine.md;
fwEntity/fwDynamicEntityComponent:../04-rage-framework/entity-and-archetype.md。 - 布娃娃(ragdoll 组件与 NM 的交互):../09-physics/game-physics-and-ragdoll.md。
- 脚本命令(
TASK_PLAY_ANIM、synced scene、anim scene 原生命令):../14-script-runtime/natives-and-commands.md。
相关源码文件#
| 文件 | 内容 |
|---|---|
rage/framework/src/fwanimation/animdirector.h/.cpp | fwAnimDirector、预渲染/更新入口、资产回调、Queue |
rage/framework/src/fwanimation/directorcomponent*.h/.cpp | 组件基类与 7 种组件(Creature、MotionTree、Move、Expressions、RagDoll、SyncedScene、FacialRig) |
rage/framework/src/fwanimation/move.h/.cpp | fwMoveNetworkInterface、fwMoveNetworkPlayer、fwMove |
rage/framework/src/fwanimation/clipsets.h/.cpp/.psc | fwClipSet、fwClipSetManager、clip_sets.meta 格式 |
rage/framework/src/fwanimation/animmanager.h/.cpp | fwAnimManager(clip 字典 store 的静态门面) |
rage/framework/src/fwanimation/facialclipsetgroups.*、expressionsets.*、syncedscene.* | 面部 clip set 组、表达式集、synced scene 描述数据 |
rage/framework/src/fwscene/stores/{clipdictionarystore,networkdefstore,expressionsdictionarystore,framefilterdictionarystore,posematcherstore}.* | 动画相关资产 store(归属 04 页,这里只引用) |
game/animation/Move.h、MovePed.*、MoveObject.*、MoveVehicle.* | CMove 族与网络 id 表 CClipNetworkMoveInfo |
game/animation/AnimManager.*、EventTags.*、FacialData.*、GestureManager.*、PMDictionaryStore.*、ClipDictionaryStoreInterface.* | 游戏动画管理、事件标签、面部数据 |
game/animation/AnimScene/** | AnimScene 系统 |
game/animation/debug/** | 动画调试工具(__BANK) |
game/ik/IkManager.*、IkRequest.h、ikmanagersolvertypes.psc、solvers/* | IK 管理器与求解器 |
game/scene/DynamicEntity.cpp | 创建 creature/director 与每帧动画入口 |
1. fwAnimDirector:每实体的动画容器#
fwAnimDirector 负责“动画系统初始化、MoVE 与调度器管理、高层动画特性与更新”src/dev_ng/rage/framework/src/fwanimation/animdirector.h:56-59。它由组件组合而成:最多 sm_MaxNumComponents = 12 个组件及其阶段,另有 (类型数-1)×(阶段数+1) 的 u8 索引表加速按类型+阶段查找 src/dev_ng/rage/framework/src/fwanimation/animdirector.h:445-450。
1.1 组件、阶段与消息#
| 枚举 | 值 | 证据 |
|---|---|---|
eComponentType | MotionTree、Creature、Move、Expressions、RagDoll、SyncedScene、FacialRig | directorcomponent.h:29-40 |
eMessageId | Update、ShuttingDown、MotionTreePreDelegate/MidDelegate/PostDelegate、CreatureChanged、FrameDataChanged、SkeletonChanged | directorcomponent.h:44-56 |
ePhase | kPhasePrePhysics=0、kPhaseMidPhysics、kPhasePreRender;kPhaseAll=-1、kPhaseNone=-2 | directorcomponent.h:89-97 |
组件类型在 fwAnimDirectorComponent::InitClass 中逐一 RegisterClass,再调用每种类型的 InitDerivedClass 与 InitPoolClass src/dev_ng/rage/framework/src/fwanimation/directorcomponent.cpp:110-143。注册宏 FW_ANIM_DIRECTOR_IMPLEMENT_COMPONENT_TYPE 为每种组件生成一个 *PooledObject 派生类和 fwPool;池满时回退到普通堆分配(GetNoOfFreeSpaces() > 0 才用池)src/dev_ng/rage/framework/src/fwanimation/directorcomponent.h:245-281。
池容量(ANIMDIRECTOR_POOL_MAX 在 PC/PS4/XB1 为 1024、其他 310;ANIMDIRECTOR_POOL_PED_MAX 为其 1/3 或 110)src/dev_ng/rage/framework/src/fwanimation/animdirector.h:47-50:
| 组件 | 池大小 | 证据 |
|---|---|---|
| Creature | POOL_MAX | directorcomponentcreature.cpp:45 |
| MotionTree | POOL_MAX*2 + PED_MAX | directorcomponentmotiontree.cpp:36 |
| Move | POOL_MAX | directorcomponentmove.cpp:43 |
| Expressions | POOL_MAX/3 + PED_MAX | directorcomponentexpressions.cpp:44 |
| RagDoll、FacialRig、SyncedScene | PED_MAX | directorcomponentragdoll.cpp:42、directorcomponentfacialrig.cpp:91、directorcomponentsyncedscene.cpp:64 |
1.2 创建与初始化#
游戏实体在 CDynamicEntity::CreateAnimDirector 中先创建 crCreature(使用框架全局的组件工厂、帧数据工厂、加速器)并按需添加 creature 组件:ExtraDofs(2 个 mover DOF + 7 个额外 DOF:第一人称相机权重、左右手约束辅助权重、左右手 CH 骨骼的旋转/平移)、ShaderVars、BlendShapes;随后让 fwDynamicEntityComponent 创建 director,并加入起始动画 scene-update 标志 src/dev_ng/game/scene/DynamicEntity.cpp:1360-1436。ped 以 (ragdoll=true, facialRig=true) 创建 src/dev_ng/game/Peds/Ped.cpp:2871。
fwAnimDirector::Init(withRagDoll, withFacialRig) 的顺序 src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:80-117:
- Creature 组件;
InitPrePhysicsMotionTree:pre-physics 动作树组件 + Move 组件(kPhasePrePhysics)src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1079-1089;InitPreRenderMotionTree:pre-render 动作树,内容为Pose节点,若实体InitIkRequest返回真则外包一个Ik节点src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1107-1135(ped 总是用自己的crKinematics初始化 IK 请求src/dev_ng/game/Peds/Ped.cpp:32784-32788);- 有 facial rig 时
InitMidPhysicsMotionTree:mid-physics 动作树(不 finalize creature)+ FacialRig 组件src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1093-1103; InitExpressions;- 需要时 RagDoll 组件(
kPhasePrePhysics)。
图证据:pre-physics 树的根由 mvNetwork::Init 生成(见核心页 11.3);mid-physics 与 pre-render 中的面部链由 fwAnimDirectorComponentFacialRig::Init 请求 src/dev_ng/rage/framework/src/fwanimation/directorcomponentfacialrig.cpp:114-170;Pose/Ik 见上;表达式节点由 CreateComponentExpressions 挂到 pre-render 树或面部表达式根(AddExpression)src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1403-1436。pre-render 树中各子链的确切拼接位置(Merge 如何与 Pose/IK 结合)未逐一追踪 request 插入目标,图中为并列表示。
1.3 表达式装配#
InitExpressions 先等待所有本 director 的活动树完成,然后(ped 专用,硬编码 GetType()==4)用 default 表达式字典中的 upperbody_fixup、upperbody_shadow、independent_mover 等表达式仅为 creature 添加额外 DOF(InitializeCreature,并不追加执行)src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1139-1216;之后若实体有 fwExpressionSet 则用其字典与表达式列表,否则用 archetype 的表达式字典/哈希;名为 pack:/facial.expr 的表达式放入 mid-physics 阶段,其余放入 pre-render 阶段 src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1257-1340。AddExpression(expr, midPhase) 在 mid 阶段需要时会按需创建 mid-physics 树与 facial rig,并以面部表达式根为插入点 src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1403-1436。ped 另有 CPed::InitExpressions,先追加面部绑定转换表达式 faceinit(mid 阶段)src/dev_ng/game/Peds/Ped.cpp:32790-32797。
2. 更新入口、锁与等待#
| 入口 | 作用 | 证据 |
|---|---|---|
PreUpdate(dt) | 清 m_Updated/m_PoseUpdated,断言上帧已把自己推入队列 | animdirector.cpp:191-199 |
StartUpdatePrePhysics(dt, ignoreVis, parent) → UpdateMove | 向 pre-physics 阶段组件发 kMessageUpdate(Move 组件在此执行 fwMove::Update 并交换输入缓冲),首次时 sm_PrePhysicsQueue.Inc(),再以实体给出的优先级调度 pre-physics 树(父 director 的树作为父依赖) | animdirector.cpp:203-208、animdirector.cpp:381-417 |
EndUpdatePrePhysics(dt) | 施加动画速度/角速度,删除无引用网络玩家,交换 MoVE 输出缓冲,然后 StartUpdateMidPhysics | animdirector.cpp:212-227 |
StartUpdateMidPhysics(dt) | 若存在 mid-physics 树:发 kMessageUpdate 并以优先级 0 调度 | animdirector.cpp:286-295 |
StartUpdatePostCamera(dt) | 相机后可见性补救:未更新则完整 UpdateMove;已更新未合成则仅调度 compose | animdirector.cpp:242-282 |
StartUpdatePreRender(dt, parent, copyLastBoundMatrices) | 设置 frag 碰撞体“复制上次矩阵”,向 pre-render 阶段组件发 kMessageUpdate,调度 pre-render 树 | animdirector.cpp:299-316 |
SetPosed(false) | 若当前处于全局锁且 pre-physics 树已完成,立即走 post-camera 补合成 | animdirector.cpp:320-336 |
WaitFor* / WaitForAllUpdatesToComplete | 单树/本 director 全部/全局等待(后者委托调度器 WaitOnAllComplete) | animdirector.cpp:1613-1654 |
动作树组件与 delegate:fwAnimDirectorComponentMotionTree::Schedule 断言未重复调度且全局锁已持有,置 m_Locked,并以 PreDelegate/MidDelegate(仅当 update)与 PostDelegate 调用全局调度器 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:149-161。三个 delegate 在工作线程上把 kMessageMotionTree{Pre,Mid,Post}Delegate 广播给同阶段组件,PreDelegate 还执行 director 的预委托回调,PostDelegate 结束时清 m_Locked src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:199-249。pre-physics 的 PostDelegate 消息处理中:若需要则 SetActivePoseFromSkel,并把实体 Push 进 sm_PrePhysicsQueue(与之前的 Inc 配对)src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:90-113。
fwAnimDirector::Queue:单消费者/多生产者无锁 FIFO(sysLockFreeRing<fwEntity,512> + 信号量),Inc 只能在消费者(主线程)调用以登记“将会有一个实体到来”,Pop 在临时耗尽时等待,返回 NULL 表示全部处理完 src/dev_ng/rage/framework/src/fwanimation/animdirector.h:471-510。这正是主线程“按动画完成顺序执行 AI 更新”的机制(调用点见核心页时序表步骤 4)。
全局锁:SetLockedGlobal(locked, phase) 只是设置两个静态变量 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.h:136-154,调用点除 CGameWorld、CPostScan 外,还有相机管理器、ped 头像管理器、前端 ped 预览、CPed 即时更新等需要“离线”跑一次动画的地方(例如头像管理器依次锁 PrePhysics/MidPhysics/PreRender 并同步等待)src/dev_ng/game/Peds/rendering/PedHeadshotManager.cpp:2753-2811、src/dev_ng/game/frontend/UIMenuPed.cpp:470-477、src/dev_ng/game/camera/system/CameraManager.cpp:338-341。
3. MoVE 接口层:fwMove / fwMoveNetworkPlayer#
fwMoveNetworkInterface是游戏侧访问网络的接口:设置/读取 float、bool、flag、clip、PM、expressions、filter、frame、observer;插入子网络;广播 request;强制状态;设置 clip set 变量映射;读取输出参数与事件等src/dev_ng/rage/framework/src/fwanimation/move.h:38-215。fwMoveNetworkPlayer包装一个mvSubNetwork,可插入主网络槽位src/dev_ng/rage/framework/src/fwanimation/move.h:254-281。fwMove是实体主网络(m_MotionTree、m_MainNetwork、m_Director),提供Update/PostUpdate/PreMotionTreeUpdate/FinishUpdate/PostFinishUpdate钩子与 synced scene 网络插入接口,按实体类型派生src/dev_ng/rage/framework/src/fwanimation/move.h:435-504。- Move 组件
Init通过实体虚函数CreateMoveObject()与GetAnimNetworkMoveInfo()选择fwMove子类与 network defsrc/dev_ng/rage/framework/src/fwanimation/directorcomponentmove.cpp:54-59;ped 返回CMovePedPooledObject与CClipNetworkMoveInfo::ms_NetworkPedsrc/dev_ng/game/Peds/Ped.cpp:32728-32736。
MoVE 参数跨线程数据流(双缓冲协议):
图证据:Move 组件对 kMessageUpdate/PreDelegate/MidDelegate 的处理 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmove.cpp:100-167;PreMotionTreeUpdate/PostMotionTreeUpdate/SwapInputBuffers/SwapOutputBuffers 与 fwMove::PreMotionTreeUpdate 调 PrepareBuffers src/dev_ng/rage/framework/src/fwanimation/move.h:899-974;输出交换 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmove.cpp:358-376;根状态拷贝输出 src/dev_ng/rage/suite/src/move/move_state.cpp:918-930。Move 组件注释说明 m_OutputBuffersSwapped 用于防止同一帧任务提前阻塞时重复翻转输出缓冲 src/dev_ng/rage/framework/src/fwanimation/directorcomponentmove.cpp:105-107。命令缓冲管理器每帧在组件类更新中 Update src/dev_ng/rage/framework/src/fwanimation/directorcomponentmove.cpp:400。
4. 资产流式:clip 字典、clip set、MoVE 网络与过滤器#
4.1 Store 一览#
| 资产 | Store | 驻留策略 | 证据 |
|---|---|---|---|
| clip 字典 | g_ClipDictionaryStore(fwClipDictionaryStore),fwAnimManager 是其静态门面(FindSlot/StreamingRequest/AddRef/Get…) | 按 clip set 元数据:单人/多人常驻或流式 | src/dev_ng/rage/framework/src/fwscene/stores/clipdictionarystore.h:115、src/dev_ng/rage/framework/src/fwanimation/animmanager.h:38-120 |
| MoVE network def | g_NetworkDefStore | 地图加载后全部加载并 AddRef(常驻) | src/dev_ng/rage/framework/src/fwscene/stores/networkdefstore.h:22、animdirector.cpp:1830-1839 |
| pose matcher | g_PoseMatcherStore | 同上全部常驻 | animdirector.cpp:1841-1850 |
| 表达式字典 | g_ExpressionDictionaryStore(fwAssetRscStore<crExpressionsDictionary>) | default 字典常驻,其余按实体引用 | src/dev_ng/rage/framework/src/fwscene/stores/expressionsdictionarystore.h:13、animdirector.cpp:1852-1867 |
| 帧过滤器字典 | g_FrameFilterDictionaryStore(容量参数 292) | 全部常驻;BANK 下另读 assets:/anim/filters/*_filtermap.xml 调试名 | src/dev_ng/rage/framework/src/fwscene/stores/framefilterdictionarystore.cpp:21、animdirector.cpp:1869-1915 |
| PM 字典 | g_PmDictionaryStore(CPmDictionaryStore : fwAssetRscStore<crpmParameterizedMotionDictionary>) | — | src/dev_ng/game/animation/PMDictionaryStore.h:39-54 |
以上常驻加载发生在 fwAnimDirector::InitStatic(INIT_AFTER_MAP_LOADED),非 FINAL 版还会枚举 common:/non_final/anim/move/dofmaps/ 建立 DOF 名称表,并初始化 synced scene 静态数据 src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1826-1936。游戏通过 CClipDictionaryStoreInterface : fwClipDictionaryStoreGameInterface 接收 clip 字典的加载/移除/引用回调 src/dev_ng/game/animation/ClipDictionaryStoreInterface.h:13。
4.2 Clip set(clip_sets.meta)#
Clip set 是“逻辑 clip id → clip 字典中的 clip”的命名映射,带回退链与元数据,用于让同一 MoVE 网络对不同角色/情境替换动画。
- 数据格式(parser psc):
fwClipSet{m_fallbackId, m_clipDictionaryName, m_clipItems: atBinaryMap<hash, fwClipItem*>, m_moveNetworkFlags};fwClipItemWithProps带eAnimPlayerFlag位集、eAnimPriority、骨骼遮罩;fwDirectedClipItemWithProps加方向;fwClipSetWithGetup加起身/死亡相关 setsrc/dev_ng/rage/framework/src/fwanimation/clipsets.psc:61-90。 - 字典元数据
fwClipDictionaryMetadata{m_streamingPolicy (SP_STREAMING/SP_SINGLEPLAYER_RESIDENT/SP_MULTIPLAYER_RESIDENT), m_memoryGroup, m_streamingPriority},以及内存组预算fwMemoryGroupMetadata、内存情境调整fwMemorySituation、变体集fwClipVariationSet;顶层fwClipSetManager持有这些映射src/dev_ng/rage/framework/src/fwanimation/clipsets.psc:48-58、src/dev_ng/rage/framework/src/fwanimation/clipsets.psc:92-136。 - 加载:
INIT_CORE时从platform:/data/anim/clip_sets/clip_sets.<meta>加载(开发可用参数从 XML 加载并另存 PSO)src/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:26-49、src/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:1736-1759;DLC 可追加,并在会话间移除src/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:1354。 - 常驻:
INIT_AFTER_MAP_LOADED时把所有SP_SINGLEPLAYER_RESIDENT字典以STRFLAG_DONTDELETE|STRFLAG_PRIORITY_LOAD请求并LoadAllRequestedObjects,加载后 AddRef 并登记为常驻对象src/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:1192-1269;进入/离开多人会话时由StartNetworkSession/EndNetworkSession切换多人常驻集合src/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:1586-1735。 - 查找:
fwClipSet::GetClip(clipId, CRO_FALLBACK)——若 set 未列出条目则直接在其字典中查;否则仅当条目存在时查字典;找不到且允许回退时递归查询 fallback setsrc/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:238-285。请求(Request_DEPRECATED)同样沿回退链请求所有字典src/dev_ng/rage/framework/src/fwanimation/clipsets.cpp:478-517。 - 游戏还有两级按情境/优先级的 clip set 预流式器(
CSituationalClipSetStreamer、CPrioritizedClipSetStreamer),在 Streaming 更新组中运行src/dev_ng/game/Core/game.cpp:2760-2763(实现位于game/task等目录,未在本页展开)。
4.3 MoVE 运行时的资产回调#
MoVE clip 节点的 clip 来源有四种上下文:VariableClipSet、ClipDictionary、AbsoluteClipSet、LocalFile src/dev_ng/rage/suite/src/move/move_clip.h:31-34。fwAnimDirector::RetrieveClip 对 VariableClipSet:经 mvMotionWeb::GetMoveNetworkInterface() 找到 fwMoveNetworkInterface,把 clip set 变量 id 映射为实际 clip set id,再 fwClipSetManager::GetClipSet(...)->GetClip(clipId)(含回退),命中则 AddRef 后返回 src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:648-700。这就是任务通过 SetClipSet(clipSetId, clipSetVarId) 让同一网络播放不同动画的机制 src/dev_ng/rage/framework/src/fwanimation/move.cpp:205。同类回调还有 RetrieveExpression/RetrieveNetworkDef/RetrieveFilter/RetrieveTransitionFilter src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:839-1050,它们在 CMovePed::InitClass 中注册给 MoVE src/dev_ng/game/animation/MovePed.cpp:875-880。
5. 初始化链(gameSkeleton 注册点)#
| 顺序 | 注册/调用 | 作用 | 证据 |
|---|---|---|---|
| INIT_CORE | CGtaAnimManager::Init → CGtaAnimManager::InitClass(建 g_AnimManager,非打包版读 ClipUserData.meta)、fwAnimManager::InitCore、CDynamicEntity::InitClass、CPedBoneTagConvertor::Init | 动画管理器 | src/dev_ng/game/Core/game.cpp:1734-1735、src/dev_ng/game/animation/AnimManager.cpp:37-79 |
| 同上 | CDynamicEntity::InitClass → fwAnimDirector::InitClass(crAnimation/crClip/crpm/crExpressions/crCreature/crmtMotionTree 与非核心节点 InitClass、组件注册)→ CBaseIkManager::InitClass → CMovePed::InitClass(mvMove::InitClass) | RAGE 动画与 MoVE | src/dev_ng/game/scene/DynamicEntity.cpp:1525-1534、animdirector.cpp:340-356 |
| 同上 | fwClipSetManager::Init(INIT_CORE) | 读取 clip set 元数据 | game.cpp:1735 |
| 场景初始化 | CBaseIkManager::InitPools | IK 求解器池 | src/dev_ng/game/scene/scene.cpp:455 |
| INIT_BEFORE_MAP_LOADED | fwAnimManager::InitBeforeMap → g_ClipDictionaryStore.Shutdown() | 重置字典 store | src/dev_ng/rage/framework/src/fwanimation/animmanager.cpp:66-69 |
| INIT_AFTER_MAP_LOADED | fwClipSetManager::Init、fwAnimDirector::InitStatic | 常驻字典、network def、过滤器、pose matcher | game.cpp:2177-2178 |
6. 游戏层 CMove 族#
CClipNetworkMoveInfo集中定义所有 network def id(ms_NetworkPed、ms_NetworkMotionPed、ms_NetworkTaskOnFootHuman、ms_NetworkTaskCutScene、ms_NetworkSyncedScene、ms_NetworkObject、各类瞄准/掩体/载具进出/攀爬/降落伞网络等)src/dev_ng/game/animation/Move.h:37-130。CMove : fwMove是游戏基类,另有CMoveAnimatedBuilding(播放单 clip 的动画建筑)src/dev_ng/game/animation/Move.h:152、src/dev_ng/game/animation/Move.h:230-265。CMovePed:ped 主网络,提供分层网络槽位——motion task(运动)、task(主任务,可带过滤器与 tag 同步标志)、secondary(上半身)、additive、facial、synced scene、左右手动画;BlendStatus位跟踪各槽淡入/淡出与面部状态;ePedState区分 animated/static frame/ragdollsrc/dev_ng/game/animation/MovePed.h:7-57、src/dev_ng/game/animation/MovePed.h:143-250。ped 状态切换通过 requestChangePedState与 flagPedStateRagdoll/Animated/StaticFrame等信号驱动,并使用RagdollFilter/StaticFrameFilter自定义过滤器src/dev_ng/game/animation/MovePed.cpp:39-55。任务与上半身/additive/facial 槽通过CSimpleBlendHelper回调插入src/dev_ng/game/animation/MovePed.cpp:57-76。CMoveObject : CMove(物件,含 primary/secondary 网络槽与外部覆盖帧)、CMoveVehicle : CMoveObject(载具机构动画:开/关/暂停状态位、机构 clip 与相位/速率信号)src/dev_ng/game/animation/MoveObject.h:27-110、src/dev_ng/game/animation/MoveVehicle.h:18-76。各类均有*PooledObject池化派生。
任务如何构造 fwMoveNetworkPlayer(CMoveNetworkHelper)并插入这些槽位属于任务系统,见 ../07-ai-tasks/task-catalogue.md。
7. 事件标签与 clip 元数据#
CClipEventTags 以 fwMvPropertyId 为键定义游戏识别的 clip 属性/标签(MoveEvent、Fire、CriticalFrame、viseme TimeOfPreDelay 等)src/dev_ng/game/animation/EventTags.h:23-47,并为结构化标签提供 crProperty 派生访问器:CFireEventTag、CObjectEventTag、CBlendOutEventTag、CIkEventTag、CArmsIKEventTag、CLegsIKEventTag、CLegsIKOptionsEventTag、CLookIKEventTag、CCameraShakeTag、CPadShakeTag、CFacialAnimEventTag、CBlockCutsceneSkippingTag、CApplyForceTag、CBlockRagdollTag、CAudioEventTag、CLoopingAudioEventTag、CMoverFixupEventTag、CDistanceMarkerTag、CPairedClipTag、CFirstPersonCameraEventTag、CMotionBlurEventTag 等,以及 CEventTag : crTag 和三种迭代器 src/dev_ng/game/animation/EventTags.h:48-771。CMotionBlurEventTag 在 EndAnimationUpdate 末尾做每帧更新 src/dev_ng/game/scene/world/GameWorld.cpp:1188-1189。网络玩家的音频与面部触发事件由 fwMoveNetworkInterface::UpdateAudioAndTriggeredFacialEvents 处理 src/dev_ng/rage/framework/src/fwanimation/move.cpp:315。框架层 fwAnimDirector 还提供在 MoVE 网络输出或 clip 相位区间中查找“脚本可见”属性的接口(VisibleToScript 键)src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:63、src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:499-647。
8. 面部动画接入#
面部动画分三层:
- 游戏决策:
CFacialDataComponent(每 ped)管理面部 idle 类型、按哈希/音频触发面部 clip、疼痛/死亡面部、面部 clip set 与 idle 覆盖,并在任务前后与每帧Updatesrc/dev_ng/game/animation/FacialData.h:56-167。面部 clip set 变体组由fwFacialClipSetGroupManager加载/追加src/dev_ng/rage/framework/src/fwanimation/facialclipsetgroups.h:38-126,每帧在 Streaming 组更新src/dev_ng/game/Core/game.cpp:2774。 - 信号:
CMovePed::SetFacialIdleClip/PlayOneShotFacialClip/PlayVisemeClip广播 request(FacialIdleClipChanged、PlayFacialOneShot、PlayViseme)并设置 clip 与时长/速率参数(FacialIdleClip、FacialOneShotClip、FacialVisemeClip、...BlendDuration、FacialVisemeRate);网络游戏中 one-shot 面部可被引用计数阻断src/dev_ng/game/animation/MovePed.cpp:272-345。 - 框架执行:
fwAnimDirectorComponentFacialRig::Init在 mid-physics 树中按“低→高优先级”建立 idle(循环)、one-shot、synced scene、viseme、mouth(语音驱动嘴型,循环)五级 merge-blend 链并Capture到共享帧m_Frame;五个FacialNodeController以与上面相同的参数键从参数缓冲读取 clip/相位/速率/混合时长;在 pre-render 树中以Frame(m_Frame) → Filter(FacialFilter) → Merge把面部结果并入最终姿态src/dev_ng/rage/framework/src/fwanimation/directorcomponentfacialrig.cpp:95-171。FacialFilter只放行面部骨骼、动画法线贴图与控制值 DOFsrc/dev_ng/rage/framework/src/fwanimation/directorcomponentfacialrig.h:89-115、src/dev_ng/rage/framework/src/fwanimation/directorcomponentfacialrig.cpp:427-472。语音驱动嘴型的权重通过SetVoiceDrivenMouthMovementBlend/UpdateVoiceDrivenMouthMovement控制src/dev_ng/rage/framework/src/fwanimation/directorcomponentfacialrig.cpp:820-906。
GestureManager(CGestureManager/g_pGestureManager)管理对话手势 src/dev_ng/game/animation/GestureManager.h:33-63,本页未展开其选择逻辑。
9. Synced scene 与 AnimScene#
9.1 Synced scene(框架层)#
Synced scene 是“游戏内伪过场”:多个实体在同一场景原点上同步播放各自的 clip。fwAnimDirectorComponentSyncedScene 的全局 SyncedScene 记录场景原点(位置/朝向)、时间/上一时间/时长/速率、音频、暂停/绝对/活动/循环/保持末帧/中止标志、销毁回调、引用计数(所有实体移除后才销毁)、可附着的父实体与骨骼、所属脚本线程(脚本退出则中止)src/dev_ng/rage/framework/src/fwanimation/directorcomponentsyncedscene.h:325-376;每实体的 SyncedInstance 持有 clip、面部 clip、播放用的 fwMoveNetworkPlayer、场景 id 与“阻断 mover 更新/场景结束退出/场景内循环”位 src/dev_ng/rage/framework/src/fwanimation/directorcomponentsyncedscene.h:306-323。场景数组 sm_SynchronizedScenes 的容量来自 gameconfig 池 SyncedScenes(默认 1)src/dev_ng/rage/framework/src/fwanimation/directorcomponentsyncedscene.cpp:74-82、src/dev_ng/rage/framework/src/fwanimation/directorcomponentsyncedscene.h:437-443;池满时会尝试释放一个“无主”场景 src/dev_ng/rage/framework/src/fwanimation/directorcomponentsyncedscene.cpp:1070、src/dev_ng/rage/framework/src/fwanimation/directorcomponentsyncedscene.cpp:1128。场景相位在 StartAnimationUpdate 开头统一推进(UpdateSyncedScenes),mover 在 SU_AFTER_ALL_MOVEMENT 回调中更新 src/dev_ng/game/scene/world/GameWorld.cpp:1168-1169、src/dev_ng/game/scene/world/GameWorld.cpp:2698-2701、src/dev_ng/game/scene/world/GameWorld.cpp:2743-2755。
9.2 AnimScene(游戏层编排)#
AnimScene 是数据驱动的“带时间轴的场景”:由 entities(ped/vehicle/object/camera/boolean)和分段(section)中的 events 组成,可选播放列表。
- 管理器:
CAnimSceneManager::Init(INIT_CORE)读取 gameconfig 池AnimScenes(至少 1)创建g_AnimSceneManager,并因 AnimScene 含 map 而禁用 parser 原地加载src/dev_ng/game/animation/AnimScene/AnimSceneManager.cpp:55-78。创建场景时从g_fwMetaDataStore中的CAnimSceneDictionary同步强制加载(STRFLAG_FORCE_LOAD+LoadAllRequestedObjects),拷贝模板场景,合并 flags,分配实例 id 并加入映射;超出上限则失败src/dev_ng/game/animation/AnimScene/AnimSceneManager.cpp:164-219。注意代码注释写“intersect”但实际调用Union合并 flagssrc/dev_ng/game/animation/AnimScene/AnimSceneManager.cpp:198-202。 - 更新阶段:
AnimSceneUpdatePhase=PreAnim, PreAi, PostAi, PostMovement, PostScript, PostCamera, PreRender, ShuttingDownsrc/dev_ng/game/animation/AnimScene/AnimSceneEvent.h:58-69。调用点:PreAnimgame.cpp:2747、PreAiGameWorld.cpp:1914、PostAiGameWorld.cpp:2003、PostMovementGameWorld.cpp:2206、PostScriptsrc/dev_ng/game/script/script.cpp:1170、PostCameraGameWorld.cpp:1303、PreRenderGameWorld.cpp:2553。管理器只在游戏未暂停(或单步)时更新所有场景src/dev_ng/game/animation/AnimScene/AnimSceneManager.cpp:106-129。 - 场景状态机:
State_Initial → LoadMetadata → PreLoadAssets → Loaded → LeadIn → Running ⇄ Paused / Skipping → Finishsrc/dev_ng/game/animation/AnimScene/AnimScene.h:207-218。FSM 只在PreAnim阶段推进,状态变化只允许在OnUpdate中发生,循环最多 100 次以检测死循环;每个阶段都会让所有 section 按阶段触发事件,并递归更新子场景src/dev_ng/game/animation/AnimScene/AnimScene.cpp:620-695、src/dev_ng/game/animation/AnimScene/AnimScene.cpp:761-837。 - 事件与实体:事件类型
PlayAnim, SyncedScene, SyncedSceneAudio, CreatePed/Object/Vehicle, PlayScene, PlaySyncedCamera, PlaySyncedAnim, PlayVfx, ForceMotionState, InternalLoop, LeadIn, PlayCameraAnimsrc/dev_ng/game/animation/AnimScene/AnimSceneEvent.h:40-56;事件生命周期为OnEnterPreload/OnUpdatePreload/OnExitPreload与按阶段的OnEnter/OnUpdate/OnExit,外加OnInit/OnShutdown/OnStartOfScenesrc/dev_ng/game/animation/AnimScene/AnimSceneEvent.h:136-233。实体类型CAnimScenePed/Vehicle/Object/Boolean/Camerasrc/dev_ng/game/animation/AnimScene/entities/AnimSceneEntities.h:29-375。 - 播放动画的实现:
CAnimScenePlayAnimEvent预加载阶段通过请求助手请求 clip 字典src/dev_ng/game/animation/AnimScene/events/AnimScenePlayAnimEvent.cpp:50-61;OnEnter为实体创建CTaskScriptedAnimation(中优先级)并标记所属场景,ped 走 ped 任务、物件走物件任务、载具放入次级任务树src/dev_ng/game/animation/AnimScene/events/AnimScenePlayAnimEvent.cpp:139-199;OnUpdate每帧把任务时间强制对齐为“场景时间 − 事件起点”并同步速率src/dev_ng/game/animation/AnimScene/events/AnimScenePlayAnimEvent.cpp:203-257。即 AnimScene 不直接驱动动作树,而是通过任务系统驱动,时间由场景主导。 - 编辑器
CAnimSceneEditor(220 KB,__BANK)与时间轴滚动条CAnimSceneScrollbar提供 RAG 内编辑;管理器在 BANK 下创建 “Anim Scenes” 调试 banksrc/dev_ng/game/animation/AnimScene/AnimSceneManager.cpp:72-76。
10. IK 系统#
10.1 求解器类型与池#
求解器类型由 psc 生成的位集枚举 IKManagerSolverTypes::Flags 定义:Arm, BodyLook, BodyRecoil, Head, Leg, Torso, TorsoReact, TorsoVehicle, RootSlopeFixup(QuadLeg、QuadrupedReact 被注释)src/dev_ng/game/ik/ikmanagersolvertypes.psc:5-18;四足求解器由 IK_QUAD_LEG_SOLVER_ENABLE/IK_QUAD_REACT_SOLVER_ENABLE 关闭(均为 0)src/dev_ng/game/ik/IkConfig.h:40-56。
| 求解器类 | 基类 / RAGE 类型 | 池上限 | 优先级(执行序) | 证据 |
|---|---|---|---|---|
CTorsoIkSolver | crIKSolverBase / CUSTOM | 64 | 2 | TorsoSolver.cpp:24、TorsoSolver.cpp:57-60 |
CBodyRecoilIkSolver | CUSTOM | 16 | 3 | BodyRecoilSolver.cpp:17、BodyRecoilSolver.cpp:57-61 |
CRootSlopeFixupIkSolver | CUSTOM | 32 | 3 | RootSlopeFixupSolver.cpp:22、RootSlopeFixupSolver.cpp:54-69 |
CTorsoReactIkSolver | CUSTOM | 16 | 4 | TorsoReactSolver.cpp:25、TorsoReactSolver.cpp:110-113 |
CTorsoVehicleIkSolver(经 CTorsoVehicleIkSolverProxy,代理池 32) | CUSTOM | 16 | 5 | TorsoVehicleSolver.cpp:23、TorsoVehicleSolver.cpp:57-62、TorsoVehicleSolverProxy.cpp:12-22 |
CLegIkSolver(经 CLegIkSolverProxy,代理池 40) | crIKSolverLegs / LEGS | 10 | 6 | LegSolver.cpp:29、LegSolver.h:49、LegSolver.cpp:348、LegSolverProxy.cpp:12-23 |
CHeadIkSolver | CUSTOM | 0(ENABLE_HEAD_SOLVER = 0 HeadSolver.h:10) | 6 | HeadSolver.cpp:25、HeadSolver.cpp:78-100 |
CArmIkSolver | crIKSolverArms / ARMS | 64 | 7 | ArmIkSolver.cpp:20-51、ArmIkSolver.h:23 |
CBodyLookIkSolver(经 CBodyLookIkSolverProxy,代理池 40) | CUSTOM | 16 | 8 | BodyLookSolver.cpp:25、BodyLookSolver.cpp:241、BodyLookSolver.cpp:316、BodyLookSolverProxy.cpp:13-28 |
池通过 FW_INSTANTIATE_CLASS_POOL[_SPILLOVER] 定义(带 spillover 系数的池满后可溢出)src/dev_ng/game/ik/solvers/ArmIkSolver.cpp:23、src/dev_ng/game/ik/solvers/LegSolver.cpp:33,在 CBaseIkManager::InitPools 中初始化;对 BodyLook/Leg/TorsoVehicle 三类代理型求解器,额外把“真实求解器池大小”登记到 ms_SolverPoolSizes 作为可分配配额 src/dev_ng/game/ik/IkManager.cpp:420-458、src/dev_ng/game/ik/IkManager.cpp:61。
10.2 管理器与代理分配#
CBaseIkManager每 ped 一个,持有crKinematics m_Kinematics与m_Solvers[ikSolverTypeCount],AddSolver/GetOrAddSolver/RemoveSolver,以及PreIkUpdate/PostIkUpdate钩子src/dev_ng/game/ik/IkManager.h:177-353;Init以 ped 骨架初始化 kinematicssrc/dev_ng/game/ik/IkManager.cpp:139-143。CIkManager在其上提供游戏 API(LookAt、SetIkTarget、各类Request(CIkRequest*)、腿部 IK 模式 OFF/PARTIAL/FULL/FULL_MELEE、头部 look-at 标志等)src/dev_ng/game/ik/IkManager.h:72-176、src/dev_ng/game/ik/IkManager.h:360-652;请求对象有CIkRequestBodyLook/Arm/ResetArm/BodyReact/BodyRecoil/Leg/TorsoVehiclesrc/dev_ng/game/ik/IkRequest.h:119-445。look-at 请求按优先级竞争,低于当前求解器优先级的请求被拒绝src/dev_ng/game/ik/IkManager.cpp:1527-1546。AddSolver(type):仅当对应池有空位才rage_new求解器(代理型创建代理而非真实求解器),成功后RefreshKinematics;池满静默返回 falsesrc/dev_ng/game/ik/IkManager.cpp:201-346。RefreshKinematics:清空 kinematics,按类型从大到小加入(代理若已分到真实求解器则加入真实求解器,否则代理本身占位)src/dev_ng/game/ik/IkManager.cpp:392-416;crKinematics::AddSolver在HACK_GTA4(GTAV 构建为 1,如src/dev_ng/rage/base/src/forceinclude/win64_beta.h:515)下按GetPriority()升序排序src/dev_ng/rage/base/src/crbody/kinematics.cpp:20-27、src/dev_ng/rage/base/src/crbody/kinematics.cpp:110-119,Iterate/Solve均按数组顺序执行src/dev_ng/rage/base/src/crbody/kinematics.cpp:51-77。故执行序即上表优先级列(Torso→Recoil/RootSlope→TorsoReact→TorsoVehicle→Leg/Head→Arm→BodyLook;同优先级的相对顺序依赖非稳定std::sort)。ProcessSolvers()(每帧,ProcessAfterCameraUpdate末尾):用 ped AI LOD 的评分顺序(最多 256 个 ped)排序;先对每个 ped 移除已完成求解器,并按配额为代理设置Blocked(评分靠前者优先拿到真实求解器);再为未阻断且尚无真实求解器的代理AllocateSolver,有变化则RefreshKinematicssrc/dev_ng/game/ik/IkManager.cpp:492-563、src/dev_ng/game/scene/world/GameWorld.cpp:2529-2533。
10.3 执行位置(哪个线程、哪个阶段)#
图证据:PreIkUpdate/StartUpdatePreRender/PostIkUpdate 调用点 src/dev_ng/game/Peds/Ped.cpp:23386、src/dev_ng/game/Peds/Ped.cpp:23419-23431、src/dev_ng/game/Peds/Ped.cpp:23576;CIkManager::PreIkUpdate 分派到各求解器 src/dev_ng/game/ik/IkManager.cpp:966-1103;crmtNodeIk::Update src/dev_ng/rage/suite/src/crmotiontree/nodeik.cpp:64-69;CLegIkSolver::Iterate 注释“Motiontree thread update”并调用 Update(dt,false) src/dev_ng/game/ik/solvers/LegSolver.cpp:470-491;compose 中 Ik 节点的依赖序列与 dependency_ik 的类型分派见核心页 10.3。
要点:
- 自定义求解器(CUSTOM)的
Solve不会在 compose 作业中执行(dependency_ik的default: break)。因此它们的工作必须在主线程PreIkUpdate或工作线程Iterate中完成。例如CBodyLookIkSolver::Iterate函数体为空(Update()被注释掉)src/dev_ng/game/ik/solvers/BodyLookSolver.cpp:373-397;CTorsoIkSolver::Iterate注释写明在 motion tree 线程调用src/dev_ng/game/ik/solvers/TorsoSolver.cpp:282-291。这表明 CUSTOM 求解器在 pre-render 树 update 阶段直接修改骨架局部矩阵,随后 compose 的Pose节点从骨架反求帧时把这些修改带入(推断:基于 Pose/Ik 节点的合成顺序,未逐个阅读每个 CUSTOM 求解器写骨架的代码)。 - ARMS/LEGS 类型求解器的
Iterate计算目标,crIKSolverArms/Legs::Solve在 compose 依赖中执行 RAGE 的两骨 IKsrc/dev_ng/rage/base/src/crbody/iksolver.h:197-254。crIKSolverBase注释将Iterate标为“需在 PPU 运行”、Solve标为“可在 SPU 运行”src/dev_ng/rage/base/src/crbody/iksolverbase.h:68-77,在现世代平台上两者都在工作线程。 - 第一人称等需要“立即”IK 的情形会在主线程再次
PreIkUpdate(0)并以 dt=0 重新调度 pre-render 树src/dev_ng/game/ik/IkManager.cpp:3350-3358。 - 动画通过
CArmsIKEventTag/CLegsIKEventTag/CLookIKEventTag等 clip 标签允许/阻断 IK(第 7 节);腿部 IK 调优通过CLegIkSolver::Tunables(CTuning派生,自动注册)数据驱动src/dev_ng/game/ik/Metadata.psc:12-40。
11. 动画调试工具(__BANK/__DEV)#
| 文件 | 类 | 用途 | 编译条件 |
|---|---|---|---|
debug/AnimViewer.*(约 550 KB) | CAnimViewer | RAG 动画查看器:选择实体、播放 clip、查看动作树/MoVE、各阶段表达式调试(含 mid-physics) | __BANK AnimViewer.h:10;每帧 CAnimViewer::Process game.cpp:2850 |
debug/AnimDebug.* | CDebugRenderer 等 | 动画调试绘制与工具函数 | __BANK |
debug/AnimPlacementTool.* | CAnimPlacementTool | 动画放置/对齐工具 | __BANK |
debug/ClipEditor.* | CAnimClipEditor | clip 标签/属性编辑 | __BANK |
debug/ClipTagMetadata.* | ClipTagAttribute 等 | 标签元数据定义(psc) | __BANK |
debug/GestureEditor.* | — | 手势编辑 | __BANK |
debug/AnimChecks.* | CAnimChecks | 资产检查 | __DEV |
另有框架层 movedump.*(MoVE 网络状态转储的 parser 结构)与 fwAnimDirector::DebugDump src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:2142,以及 sm_DebugEnableMotionTreeLogging(在 post-delegate 中转储被查看实体的动作树)src/dev_ng/rage/framework/src/fwanimation/directorcomponentmotiontree.cpp:116-133。调试器总体见 ../23-debug-security/debug-tools.md。
附:rage/framework/src/fwactuator。该目录与动画无关:只有 fwRecoilEffect : ioActuatorEffect,是支持扳机振动的手柄上的“枪械后坐力”力反馈效果(持续时间、触发扳机 NONE/LEFT/RIGHT、强度)src/dev_ng/rage/framework/src/fwactuator/recoileffect.h:15-49,由游戏控制层 CControl 使用 src/dev_ng/game/system/control.cpp:2746-2755。输入/力反馈详见 ../22-input-platform/input-and-control-mapping.md。
12. 约束与边界行为#
- 组件与 IK 求解器池满时:director 组件回退到堆分配;IK
AddSolver静默失败(代理型则在ProcessSolvers中被阻断,表现为该 ped 暂时没有该 IK)。 - 调度前必须持有全局动作树锁;锁期间禁止读写带 director 的骨架局部矩阵与删除实体(见核心页 12)。
- MoVE 信号必须在
kMessageUpdate(SwapInputBuffers)之前设置才进入本帧;输出在EndUpdatePrePhysics交换后才对游戏可见——因此 AI 读到的 MoVE 事件至少滞后一次动作树更新(推断,基于双缓冲协议)。 InitExpressions中 ped 类型以字面量4判断(注释ENTITY_TYPE_PED = 4)src/dev_ng/rage/framework/src/fwanimation/animdirector.cpp:1170-1171,属于框架对游戏枚举的硬编码依赖。- network def、帧过滤器、pose matcher 全部常驻;新增网络会直接增加常驻内存。
13. 未决问题 / 证据不足#
- pre-render 树中面部 Merge 子链、Pose/IK 子链与表达式节点的确切拼接(request target)未逐一追踪。
fwAnimDirectorComponentRagDoll、fwAnimDirectorComponentExpressions的内部实现、ragdoll 帧与 NM 的交互未在本页展开(见物理页)。CMovePed::Update/FinishUpdate中的具体逻辑(槽位混合状态更新、面部状态机RequestFacialState)只做了接口级阅读。CSituationalClipSetStreamer/CPrioritizedClipSetStreamer、CGestureManager、CFacialDataComponent::Update的选择逻辑未展开。- 各 CUSTOM IK 求解器(Torso/BodyLook/RootSlope/TorsoReact/TorsoVehicle/Recoil)的数学与写骨架位置未逐个验证。