UE5.8 Unreal Animation Framework 专题系列

UE5.8 UAF 专题(一):核心运行时、插件边界与 UAFComponent

从 UAF.uplugin、UUAFComponent、FUAFAssetInstance、UUAFRigVMAsset、变量、组件和系统工厂解释 Unreal Animation Framework 的运行时骨架。

总览

这一篇只看 UAF 核心运行时。源码入口主要是 UAF.upluginComponent/AnimNextComponent.hUAFAssetInstance.hUAFAssetInstanceComponent.hFactory/UAFSystemFactoryParams.hDataRegistry.h。先把“系统跑在哪里”弄清楚,后面的 Trait、AnimNode、Chooser、Motion Matching 才不会飘。

UE5.8 UAF 专题(一):核心运行时、插件边界与 UAFComponent 配图
一个 UAF 系统由资产工厂产生实例,实例持有变量和组件,UUAFComponent 负责把它接到 Actor 与 SkeletalMeshComponent 上。

源码依据

UAF.uplugin 明确把插件标为 Experimental,默认不启用。运行时模块 UAFPreDefault 阶段加载,编辑器能力拆到 UAFEditorUAFUncookedOnly。依赖里有 RigVMWorkspaceHierarchyTableGameplayInsightsPropertyBindingUtils 等,这已经说明 UAF 不是一个单纯播放动画的插件,而是带资产编辑、变量绑定、调试和图编译的一整套框架。

核心类的职责:

类型 职责
UUAFComponent ActorComponent,注册/注销 UAF 系统,读写变量,管理输入输出,决定是否写回 SkeletalMeshComponent
FUAFAssetInstance UAF 资产运行时实例,持有变量、默认组件、宿主关系、调试 ID
FUAFAssetInstanceComponent 挂在实例上的扩展组件,派生类型用 DECLARE_UAF_ASSET_INSTANCE_COMPONENT() 提供 RTTI
UUAFRigVMAsset 可承载 RigVM 逻辑的资产基类,持有 RigVM、变量默认值、函数数据、引用变量资产和默认组件
FUAFSystemFactoryParams 程序化生成系统实例的参数集合,可加公共变量、组件和初始化任务
FDataRegistry 全局动画数据注册表,管理 reference pose、命名数据、引用计数数据块

UAFComponent 在 Actor 上做什么

UUAFComponent 是 UAF 和游戏对象之间的主连接点。它是 UActorComponent,也实现了 UE::UAF::ISystemOutputAdapter。源码里能看到它覆盖 OnRegisterOnUnregisterBeginPlayEndPlayActivateDeactivate,并在注册时设置输入输出、分配系统、缓存 Skeletal Mesh。

它暴露的关键 API 包括:

  • SetVariable(const FAnimNextVariableReference&, const ValueType&)
  • GetVariable(const FAnimNextVariableReference&, ValueType&)
  • WriteVariable(...)
  • BlueprintSetVariableReference(...)
  • BlueprintGetVariableReference(...)
  • BlueprintSetInputBinding(...)
  • QueueTask(FName ModuleEventName, TUniqueFunction<void(const FModuleTaskContext&)>, ETaskRunLocation)
  • QueueInputTraitEvent(FAnimNextTraitEventPtr)
  • GetSystemReference()
  • AddComponentPrerequisite / AddComponentSubsequent
  • AddModuleEventPrerequisite / AddModuleEventSubsequent

UUAFComponent 的输出配置也很重要。EUAFSkeletalMeshComponentOutputMode 有两个值:WriteToSkeletalMeshComponentPoseSkipWriteToSkeletalMeshComponentPose。前者直接驱动 Mesh,后者适合做中间 UAF 输出,比如上游生成 ValueBundle,下游另一个 UAF 系统再消费。

实例、变量和组件

FUAFAssetInstance 是运行时状态容器。它保存:

  • TArray<TInstancedStruct<FUAFAssetInstanceComponent>> Components
  • FUAFInstanceVariableData Variables
  • 资产硬引用 Asset
  • 实例脚本结构 ScriptStruct
  • 宿主实例弱引用 HostInstance

变量访问不是裸 floatFName 查表,而是通过 FAnimNextVariableReferenceFAnimNextParamType 做强类型读写:

float Speed = 0.0f;
if (UAFComponent->GetVariable(SpeedVariable, Speed))
{
    Speed = FMath::Max(Speed, 0.0f);
}

UAFComponent->SetVariable(SpeedVariable, Speed);

实例组件是扩展点。FUAFAssetInstance::GetOrAddComponent<T>() 会懒创建组件并调用 OnBindToInstance()。Graph、Module、Layering、Injection 等功能都可以用组件把状态挂在实例上,而不是塞到一个超大对象里。

USTRUCT()
struct FProjectAnimBudgetComponent : public FUAFAssetInstanceComponent
{
    GENERATED_BODY()
    DECLARE_UAF_ASSET_INSTANCE_COMPONENT()

    int32 MaxSearchesPerFrame = 2;

private:
    virtual void OnBindToInstance() override
    {
        // 在这里缓存实例相关信息,避免每帧从外部找。
    }
};

系统工厂和初始化任务

FUAFSystemFactoryParams 用来描述如何生成和初始化一个系统。它内部持有 FUAFSimpleSystemBuilder,可以添加公共变量结构、变量资产、RigVM 资产和默认组件;也可以添加初始化任务。

FUAFSystemFactoryParams Params;

Params.AddPublicVariablesStruct<FProjectAnimVariables>();
Params.AddComponent<FProjectAnimBudgetComponent>();

Params.AddInitializeTask(
    [](const UE::UAF::FInstanceTaskContext& Context)
    {
        Context.AccessVariablesStruct<FProjectAnimVariables>(
            [](FProjectAnimVariables& Vars)
            {
                Vars.Speed = 0.0f;
                Vars.bIsInCombat = false;
            });
    });

FInstanceTaskContext 的 API 很克制:SetVariableAccessVariableAccessVariablesStructGetAssetInstance()。这说明初始化任务应该只做实例初始化,不应该在里面偷跑复杂 gameplay 逻辑。

Reference Pose 和 DataRegistry

动画系统最怕“每个节点都自己算骨架映射”。UAF 用 FDataRegistry 缓存全局数据,尤其是 reference pose。RegisterReferencePose / GetOrGenerateReferencePose 可以按 USkeletalMeshComponentUSkeletalMesh 生成并返回 FDataHandleFDataHandle 是引用计数句柄,底层 FAllocatedBlock 保存内存、元素数量和 FAnimNextParamType

FReferencePose 里有 LOD 排序骨骼、mesh/skeleton/LOD 索引映射、父骨骼映射、曲线标记和 fast path 标记。FLODPose 再基于它保存当前 LOD 的 local transforms,并提供 LOD 间 copy、ref pose、identity、bone name 查找等能力。

这套设计的工程意义是:复杂节点、Pose Search、Warping、Mirroring 都应该复用同一个 reference pose 和 LOD 映射,不要各自手搓。

项目落地

最小接入建议:

  1. 在测试角色上添加 UUAFComponent,先绑定一个简单 UAF 系统资产。
  2. 显式设置 OutputComponent,避免 Actor 上有多个 SkeletalMeshComponent 时选错。
  3. 把输入拆成少量公共变量,例如速度、加速度、朝向、移动模式、战斗状态。
  4. SetVariable / GetVariable 做 C++ 边界,蓝图只负责调试和快速试验。
  5. 使用 AddComponentPrerequisite 或模块事件依赖确定 Tick 顺序,避免动画读取上一帧 movement 数据。
  6. 不写回 Mesh 的中间 UAF 系统,明确设为 SkipWriteToSkeletalMeshComponentPose

使用案例:第三人称角色旁路验证

第一阶段不要直接替换角色 AnimBP。建议把 UAF 当成旁路系统,先只验证输入、图运行和输出质量:

  1. 角色保留现有 USkeletalMeshComponent、AnimBP 和 Montage。
  2. 额外添加一个 UUAFComponent,显式指定它要读取或写入的 Mesh。
  3. OutputMode 先使用 SkipWriteToSkeletalMeshComponentPose,让它生成中间输出但不影响玩家看到的动画。
  4. 每帧从 Movement、Ability、Inventory 汇总少量变量写入 UAF。
  5. 在调试 UI 中显示 UAF 是否激活、当前图、当前 layer、最近一次变量写入时间。
  6. 通过项目开关把某个测试角色切到 WriteToSkeletalMeshComponentPose,只对 QA 地图开放。

项目代码通常不应该到处保存 UUAFComponent 指针并随手写变量,而是做一个桥接组件:

UCLASS(ClassGroup = Animation, meta = (BlueprintSpawnableComponent))
class UProjectUAFBridgeComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, Category = "UAF")
    TObjectPtr<UUAFComponent> UAFComponent = nullptr;

    UPROPERTY(EditAnywhere, Category = "UAF|Variables")
    FAnimNextVariableReference SpeedVariable;

    UPROPERTY(EditAnywhere, Category = "UAF|Variables")
    FAnimNextVariableReference WeaponStanceVariable;

    virtual void TickComponent(
        float DeltaTime,
        ELevelTick TickType,
        FActorComponentTickFunction* ThisTickFunction) override
    {
        Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

        if (!UAFComponent)
        {
            return;
        }

        const float Speed = CachedMovementSpeed();
        const FName WeaponStance = CachedWeaponStance();

        UAFComponent->SetVariable(SpeedVariable, Speed);
        UAFComponent->SetVariable(WeaponStanceVariable, WeaponStance);
    }
};

这样做的好处是:UAF 仍然是动画实现细节,gameplay 系统只负责提供摘要状态;如果试点失败,删掉桥接组件或关掉开关,不会污染角色主逻辑。

架构分析:组件边界怎么划

UUAFComponent 是 UAF runtime 的入口,但它不应该承担项目动画架构里的全部职责。推荐这样拆:

职责 放在哪里 原因
从 Movement/Ability/Inventory 汇总状态 项目桥接组件 gameplay API 经常变,不要让 UAF 图直接依赖
保存 UAF 变量引用 项目桥接组件或数据资产 避免在蓝图里用字符串找变量
图选择、层权重、动画状态 UAF Chooser/StateTree/Layering 数据化、可调试、可交给动画技术美术维护
姿态生成和修正 UAF AnimGraph/Trait/AnimNode 由 EvaluationVM 管理执行顺序
是否启用 UAF、是否回退 项目设置、控制台变量、角色配置 Experimental 插件必须有退出路线

验收标准

做完最小接入后,不要只看“有没有动”。至少验证这些点:

  1. 角色身上有多个 Mesh 时,UAF 输出目标没有选错。
  2. SetVariable 返回成功;失败时能打印变量名、类型和资产路径。
  3. Movement tick 先于 UAF 更新,速度和空中状态不是上一帧。
  4. 关闭 UAF 开关后旧 AnimBP 仍正常工作。
  5. PIE 结束、角色销毁、关卡切换时没有持有失效的系统引用。
  6. 打包构建不依赖 UAFEditorUAFUncookedOnly 头文件。

常见坑

  • SetAsset 注释写着只能在组件未注册时调用;运行中换资产应走图注入、Chooser 或专门的系统切换路径。
  • BlueprintSetVariable(FName, int32) 已 deprecated,应使用变量引用版本。
  • FAnimNextVariableReference 指向资产和变量名,重命名变量后要走编辑器迁移和验证。
  • FUAFAssetInstanceComponentUSTRUCT 不是 UObject,不要用 UObject 生命周期假设管理它。
  • Reference pose 与 LOD 映射必须来自当前输出 Mesh,否则 Warping/Mirroring 很容易在骨骼索引上出错。

源码路径索引

  • UAF/Source/UAF/Public/Component/AnimNextComponent.h
  • UAF/Source/UAF/Public/UAFAssetInstance.h
  • UAF/Source/UAF/Public/UAFAssetInstanceComponent.h
  • UAF/Source/UAF/Public/Factory/UAFSystemFactoryParams.h
  • UAF/Source/UAF/Public/DataRegistry.h