UE5.8 Unreal Animation Framework 专题系列

UE5.8 UAF 专题(二):RigVM 资产、变量系统与编译边界

讲 UUAFRigVMAsset、变量 PropertyBag、FAnimNextVariableReference、函数句柄、EditorData、UncookedOnly 编译和运行时共享数据。

总览

UAF 的资产基类 UUAFRigVMAsset 继承自 URigVMHost。这说明 UAF 的编辑和执行不是传统 UAnimBlueprintGeneratedClass 那套路线,而是围绕 RigVM、变量 PropertyBag、函数句柄和编译后的 shared data 组织。理解这一层,才能知道哪些东西能在运行时改,哪些只能在编辑器编译阶段生成。

UE5.8 UAF 专题(二):RigVM 资产、变量系统与编译边界 配图
编辑器里编辑 RigVM 图和变量,编译后运行时只消费稳定的函数、变量、组件和共享数据。

源码依据

重点文件:

  • UAF/Source/UAF/Public/AnimNextRigVMAsset.h
  • UAF/Source/UAF/Public/InstanceTask.h
  • UAF/Source/UAF/Public/InstanceTaskContext.h
  • UAF/Source/UAF/Public/BindableValue/UAFBindableTypes.h
  • UAF/Source/UAF/Public/BindableValue/UAFPropertyBinding.h
  • UAF/Source/UAF/Internal/RigVMRuntimeDataRegistry.h
  • UAFAnimGraph/Source/UAFAnimGraphUncookedOnly/Internal/AnimNextController.h
  • UAFAnimGraph/Source/UAFAnimGraphUncookedOnly/Internal/AnimNextTraitStackUnitNode.h

UUAFRigVMAsset 内部持有:

成员 作用
FRigVMExtendedExecuteContext ExtendedExecuteContext RigVM 执行上下文模板,每个实例需要复制并重绑内存
URigVM* RigVM 资产承载的 VM
FInstancedPropertyBag VariableDefaults 变量默认值,公共变量排序在前
FInstancedPropertyBag CombinedPropertyBag 给外部变量提供稳定属性指针
ReferencedVariableAssets 运行时会引用/实例化变量的 UAF 资产
ReferencedVariableStructs 原生结构体变量来源
ReferencedVariableRigVMAssets 其他 RigVM runtime asset 变量来源
FunctionData 可由 native 调用的 RigVM 函数数据
DefaultInjectionSite 默认注入点变量引用
Components 实例默认组件

变量系统不是普通黑板

UAF 变量依赖 FAnimNextVariableReferenceFAnimNextParamType。读取时可以做类型转换,访问内存时要求严格类型匹配。FUAFAssetInstanceFInstanceTaskContext 都提供模板 API:

FVector Velocity = FVector::ZeroVector;
Instance.GetVariable(VelocityVariable, Velocity);

Instance.AccessVariable<float>(
    SpeedVariable,
    [](float& Speed)
    {
        Speed = FMath::Max(Speed, 0.0f);
    });

Instance.SetVariable(bAimingVariable, true);

结构体变量更适合项目边界:

USTRUCT(BlueprintType)
struct FProjectAnimPublicVars
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Speed = 0.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bIsAiming = false;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName WeaponStance = NAME_None;
};

把它注册为公共变量后,外部系统可以稳定读写,Chooser、StateTree、Motion Matching 等也能引用这些变量。

BindableValue 的意义

BindableValue/UAFBindableTypes.h 提供了大量可绑定值类型,例如 FBindableBoolFBindableFloatFBindableInt32FBindableNameFBindableVectorFBindableQuatFBindableTransformFBindableStructFBindableObject。它们不是单纯“一个默认值”,而是支持常量值和运行时绑定两条路径。

UAFBlendByBoolNode 里能看到典型用法:节点数据里保存 FBindableBool BoolValue,注释明确写着可以通过 Binding 字段绑定到运行时 UAF bool 变量。这样节点资产里可以先有默认值,项目运行时再把它接到系统变量。

RigVM 函数与参数调用

UUAFRigVMAsset 暴露了两个关键函数:

  • CallFunctionHandle(FFunctionHandle, FRigVMExtendedExecuteContext&, FInstancedPropertyBag&)
  • ExecuteParameterlessFunction(FFunctionHandle, FRigVMExtendedExecuteContext&, void* OutReturnValue, int32 ReturnValueSize)

FUAFAssetInstance::ExecuteParameterlessFunction 还会沿实例层级解析函数所属资产,因此父 System 资产的函数可以从子 Graph 实例调用。这个设计让图资产、系统资产和运行时实例之间有一个较稳定的函数边界。

编译边界

源码里编辑器和运行时被拆得很明显:

  • Runtime 模块:UAFUAFAnimGraphUAFAnimNode
  • Editor 模块:资产定义、详情面板、预览、Rewind Debugger 轨道
  • UncookedOnly 模块:控制器、Schema、编译工具、GraphNodeTemplate、EditorData

UUAFAnimGraph 在 editor-only 数据里保存 SharedDataArchiveBuffer,运行时则用 SharedDataBufferGraphReferencedObjects。注释里还提到编译时会 freeze live graph instances,编译完成后 thaw 以重新分配实例内存。这个边界非常关键:运行时应该消费编译产物,不应该依赖编辑器节点对象。

项目落地

建议的变量设计规则:

  1. 公共变量只放跨系统需要共享的数据,例如 speed、stance、weapon、aim alpha。
  2. 节点内部临时状态放 Trait InstanceData 或 AnimNode 成员,不要写回公共变量。
  3. 大结构体用 WriteVariableAccessVariablesStruct,减少复制。
  4. 命名稳定后再让 Chooser/StateTree/PoseSearch 引用,避免批量重命名。
  5. 运行时代码只 include Runtime 头,编辑器扩展才依赖 UncookedOnly

初始化参数示例:

FUAFSystemFactoryParams Params;
Params.AddPublicVariablesStruct<FProjectAnimPublicVars>();

Params.AddInitializeTask(
    [](const UE::UAF::FInstanceTaskContext& Context)
    {
        Context.AccessVariablesStruct<FProjectAnimPublicVars>(
            [](FProjectAnimPublicVars& Vars)
            {
                Vars.Speed = 0.0f;
                Vars.WeaponStance = TEXT("Unarmed");
            });
        });

使用案例:给战斗角色设计公共变量 API

UAF 变量最容易被用成“动画黑板”,最后每个系统都往里面塞字段。更稳的做法是先把它当成项目动画 API 设计,按调用方和频率拆结构:

变量结构 写入方 读取方 更新频率
FHeroLocomotionAnimVars Movement / Mover Locomotion graph、Pose History、Chooser 每帧
FHeroCombatAnimVars Ability / Weapon StateTree、Layering、Chooser 状态变化或每帧少量字段
FHeroInteractionAnimVars Interaction / Targeting Motion Matching、Warping、Control Rig 交互期间每帧
FHeroDebugAnimVars UAF graph 或桥接组件 日志、Rewind、QA UI 低频或事件驱动

示例结构可以这样定:

USTRUCT(BlueprintType)
struct FHeroLocomotionAnimVars
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FVector VelocityWS = FVector::ZeroVector;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float GroundSpeed = 0.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float FacingYawDelta = 0.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bIsInAir = false;
};

USTRUCT(BlueprintType)
struct FHeroCombatAnimVars
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName WeaponStance = TEXT("Unarmed");

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName OverlayState = NAME_None;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float AimAlpha = 0.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bWantsUpperBodyAttack = false;
};

桥接组件可以每帧写 locomotion,大状态变化时写 combat:

void UHeroUAFBridgeComponent::PushLocomotionVars(const UCharacterMovementComponent& Move)
{
    UAFComponent->WriteVariable<FHeroLocomotionAnimVars>(
        LocomotionVarsVariable,
        [&](FHeroLocomotionAnimVars& Vars)
        {
            Vars.VelocityWS = Move.Velocity;
            Vars.GroundSpeed = FVector::VectorPlaneProject(Move.Velocity, FVector::UpVector).Size();
            Vars.bIsInAir = Move.IsFalling();
            Vars.FacingYawDelta = ComputeFacingYawDelta();
        });
}

void UHeroUAFBridgeComponent::SetWeaponStance(FName NewStance)
{
    UAFComponent->WriteVariable<FHeroCombatAnimVars>(
        CombatVarsVariable,
        [&](FHeroCombatAnimVars& Vars)
        {
            Vars.WeaponStance = NewStance;
        });
}

这个案例的核心不是代码本身,而是“变量边界”:UAF 只收到动画需要的摘要,不收到完整 Ability 对象、Inventory 对象或 Character 指针。这样 Chooser 和 StateTree 可以稳定读变量,资产也不容易因为 gameplay 重构而碎掉。

编辑器操作流程

实际资产搭建建议按这个顺序:

  1. Animation / Animation Framework 下创建 UAF Shared Variables
  2. 先创建 LocomotionCombatInteraction 三组变量,不要一开始放几十个字段。
  3. UAF SystemUAF Animation Graph 里引用这组 Shared Variables。
  4. Chooser、StateTree、Motion Matching 只引用 Shared Variables 里公开过的字段。
  5. 变量重命名前先查引用,尤其是 Chooser 表和 StateTree task。
  6. 打包前跑一次资产编译和引用检查,确认没有 Editor-only 资产被结果列引用。

如果团队有动画技术美术参与,最好把变量命名写成规范:GroundSpeedbIsInAirWeaponStance 这类语义稳定字段可以公开;TempSpeed2StateFlagA 这类字段不允许进公共变量资产。

架构分析:编译边界的实用判断

读源码时最重要的不是背类名,而是判断什么能在运行时改:

需求 推荐位置 不推荐做法
每帧速度、朝向、输入轨迹 UAF 公共变量 改 RigVM 图或重建资产
武器图选择 Chooser 表和 UAF Graph Factory Asset C++ 里硬编码一串 if/else
状态入口写 AimAlphaTarget StateTree UAF Set Variable 让 AnimGraph 每帧猜 gameplay 状态
节点内部播放时间 Trait InstanceData 或 AnimNode 实例 写进 SharedData 或公共变量
编辑器模板、图节点、Schema UncookedOnly / Editor 模块 Runtime 模块 include 编辑器头

常见坑

  • 不要在运行时直接依赖 UUAFRigVMAssetEditorDataUAnimNextController
  • 不要把变量引用退化成 FName;类型、资产和变量名都需要被验证。
  • Public 变量越多,资产之间耦合越强。先设计“外部 API”,再设计节点内部状态。
  • ExecuteParameterlessFunction 要求函数确实是一个输出返回值的 parameterless function,不适合拿来做复杂异步流程。
  • 编译后 shared data 是只读心智模型;每实例状态放 InstanceData、GraphInstance 或 AssetInstance component。

源码路径索引

  • UAF/Source/UAF/Public/AnimNextRigVMAsset.h
  • UAF/Source/UAF/Public/InstanceTaskContext.h
  • UAF/Source/UAF/Public/BindableValue/UAFBindableTypes.h
  • UAF/Source/UAF/Internal/RigVMRuntimeDataRegistry.h