总览
UAF 的资产基类 UUAFRigVMAsset 继承自 URigVMHost。这说明 UAF 的编辑和执行不是传统 UAnimBlueprintGeneratedClass 那套路线,而是围绕 RigVM、变量 PropertyBag、函数句柄和编译后的 shared data 组织。理解这一层,才能知道哪些东西能在运行时改,哪些只能在编辑器编译阶段生成。
源码依据
重点文件:
UAF/Source/UAF/Public/AnimNextRigVMAsset.hUAF/Source/UAF/Public/InstanceTask.hUAF/Source/UAF/Public/InstanceTaskContext.hUAF/Source/UAF/Public/BindableValue/UAFBindableTypes.hUAF/Source/UAF/Public/BindableValue/UAFPropertyBinding.hUAF/Source/UAF/Internal/RigVMRuntimeDataRegistry.hUAFAnimGraph/Source/UAFAnimGraphUncookedOnly/Internal/AnimNextController.hUAFAnimGraph/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 变量依赖 FAnimNextVariableReference 和 FAnimNextParamType。读取时可以做类型转换,访问内存时要求严格类型匹配。FUAFAssetInstance 和 FInstanceTaskContext 都提供模板 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 提供了大量可绑定值类型,例如 FBindableBool、FBindableFloat、FBindableInt32、FBindableName、FBindableVector、FBindableQuat、FBindableTransform、FBindableStruct、FBindableObject。它们不是单纯“一个默认值”,而是支持常量值和运行时绑定两条路径。
在 UAFBlendByBoolNode 里能看到典型用法:节点数据里保存 FBindableBool BoolValue,注释明确写着可以通过 Binding 字段绑定到运行时 UAF bool 变量。这样节点资产里可以先有默认值,项目运行时再把它接到系统变量。
RigVM 函数与参数调用
UUAFRigVMAsset 暴露了两个关键函数:
CallFunctionHandle(FFunctionHandle, FRigVMExtendedExecuteContext&, FInstancedPropertyBag&)ExecuteParameterlessFunction(FFunctionHandle, FRigVMExtendedExecuteContext&, void* OutReturnValue, int32 ReturnValueSize)
FUAFAssetInstance::ExecuteParameterlessFunction 还会沿实例层级解析函数所属资产,因此父 System 资产的函数可以从子 Graph 实例调用。这个设计让图资产、系统资产和运行时实例之间有一个较稳定的函数边界。
编译边界
源码里编辑器和运行时被拆得很明显:
- Runtime 模块:
UAF、UAFAnimGraph、UAFAnimNode - Editor 模块:资产定义、详情面板、预览、Rewind Debugger 轨道
- UncookedOnly 模块:控制器、Schema、编译工具、GraphNodeTemplate、EditorData
UUAFAnimGraph 在 editor-only 数据里保存 SharedDataArchiveBuffer,运行时则用 SharedDataBuffer 和 GraphReferencedObjects。注释里还提到编译时会 freeze live graph instances,编译完成后 thaw 以重新分配实例内存。这个边界非常关键:运行时应该消费编译产物,不应该依赖编辑器节点对象。
项目落地
建议的变量设计规则:
- 公共变量只放跨系统需要共享的数据,例如 speed、stance、weapon、aim alpha。
- 节点内部临时状态放 Trait
InstanceData或 AnimNode 成员,不要写回公共变量。 - 大结构体用
WriteVariable或AccessVariablesStruct,减少复制。 - 命名稳定后再让 Chooser/StateTree/PoseSearch 引用,避免批量重命名。
- 运行时代码只 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 重构而碎掉。
编辑器操作流程
实际资产搭建建议按这个顺序:
- 在
Animation / Animation Framework下创建UAF Shared Variables。 - 先创建
Locomotion、Combat、Interaction三组变量,不要一开始放几十个字段。 - 在
UAF System或UAF Animation Graph里引用这组 Shared Variables。 - Chooser、StateTree、Motion Matching 只引用 Shared Variables 里公开过的字段。
- 变量重命名前先查引用,尤其是 Chooser 表和 StateTree task。
- 打包前跑一次资产编译和引用检查,确认没有 Editor-only 资产被结果列引用。
如果团队有动画技术美术参与,最好把变量命名写成规范:GroundSpeed、bIsInAir、WeaponStance 这类语义稳定字段可以公开;TempSpeed2、StateFlagA 这类字段不允许进公共变量资产。
架构分析:编译边界的实用判断
读源码时最重要的不是背类名,而是判断什么能在运行时改:
| 需求 | 推荐位置 | 不推荐做法 |
|---|---|---|
| 每帧速度、朝向、输入轨迹 | 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 编辑器头 |
常见坑
- 不要在运行时直接依赖
UUAFRigVMAssetEditorData或UAnimNextController。 - 不要把变量引用退化成
FName;类型、资产和变量名都需要被验证。 - Public 变量越多,资产之间耦合越强。先设计“外部 API”,再设计节点内部状态。
ExecuteParameterlessFunction要求函数确实是一个输出返回值的 parameterless function,不适合拿来做复杂异步流程。- 编译后 shared data 是只读心智模型;每实例状态放
InstanceData、GraphInstance 或 AssetInstance component。
源码路径索引
UAF/Source/UAF/Public/AnimNextRigVMAsset.hUAF/Source/UAF/Public/InstanceTaskContext.hUAF/Source/UAF/Public/BindableValue/UAFBindableTypes.hUAF/Source/UAF/Internal/RigVMRuntimeDataRegistry.h