UE5.8 Unreal Animation Framework 专题系列

UE5.8 UAF 专题(四):AnimNode、AnimOp 与可组合节点树

解释 UAFAnimNode 插件中的 FUAFAnimNode、FUAFAnimNodeData、FUAFAnimGraphUpdateContext、AnimOp、SequencePlayer、BlendStack、BlendByBool 和 Transition。

总览

UAFAnimNode 给 UAF 提供了一套更接近传统节点树的运行时层。它不是 FAnimNode_Base,而是 FUAFAnimNodeFUAFAnimNodeDataFUAFAnimOp 三件套:节点在 Update 阶段维护拓扑、时间和状态,AnimOp 在 Evaluate 阶段真正产生或修改动画值。

UE5.8 UAF 专题(四):AnimNode、AnimOp 与可组合节点树 配图
AnimNode 在 Update 阶段维护树和时间,AnimOp 在 Evaluate 阶段产出或修改 pose、notifies 和同步贡献。

源码依据

重点文件:

  • UAFAnimNode/Public/UAF/AnimNodeCore/UAFAnimNode.h
  • UAFAnimNode/Public/UAF/AnimNodeCore/UAFAnimNodeData.h
  • UAFAnimNode/Public/UAF/AnimNodeCore/UAFAnimNodeUpdate.h
  • UAFAnimNode/Public/UAF/AnimOpCore/UAFAnimOp.h
  • UAFAnimNode/Public/UAF/AnimNodes/UAFSequencePlayer.h
  • UAFAnimNode/Public/UAF/AnimNodes/UAFBlendStack.h
  • UAFAnimNode/Public/UAF/AnimNodes/UAFBlendByBoolNode.h
  • UAFAnimNode/Public/UAF/AnimNodes/UAFSimpleTransition.h
  • UAFAnimNode/Public/UAF/AnimNodes/InputValueAnimNode.h

AnimNode 和 AnimNodeData

FUAFAnimNodeData 是可序列化的共享数据,核心虚函数是 CreateInstance(FUAFAnimGraphUpdateContext&)FUAFAnimNode 是运行时实例,用 TRefCountPtr 管生命周期,禁止复制移动。

FUAFAnimNode 管:

  • Parent 指针。
  • Children 数组,内联容量为 2。
  • TotalWeight、bIsBlendingOut、bIsNewlyRelevant。
  • PreAnimOp 和 PostAnimOp。
  • PreUpdate / PostUpdate
  • 接口查询和 GC 引用收集。

这让节点可以很轻地组合。比如单子节点节点可以自动传播权重,多子节点节点可以自己控制每个 child 的权重与过渡。

UpdateContext

FUAFAnimGraphUpdateContext 保存 HostObject、VariablesOwner、GCReferences、DeltaTime、PlayRate 栈和待销毁节点。节点更新时可以:

  • GetVariablesOwner() 读写 UAF 实例变量。
  • GetHostObject() 找宿主对象。
  • PushPlayRate() / PopPlayRate() 临时缩放时间。
  • 创建或销毁子节点。
  • 设置 Pre/Post AnimOp。

源码里有一个很好的边界例子:FUAFInputValueAnimNodePreUpdate 读取变量,把 ValueBundle 或 LODPose 缓存在 FUAFInputValueAnimOp 上,因为变量访问只在 Update 阶段安全,Evaluation 阶段只消费缓存。

AnimOp 是评估阶段的命令

FUAFAnimOpUSTRUCT,用于产出或修改 animated values、notifies、sync contributors。它有三个入口:

  • EvaluateValues(FUAFAnimOpValueEvaluator&)
  • EvaluateNotifies(FUAFAnimOpNotifyEvaluator&)
  • EvaluateSynchronization(FUAFAnimOpSyncEvaluator&)

InitializeAs<T>() 会用函数指针比较判断派生类型实现了哪些评估函数。每个 AnimOp 还声明自己消费多少输入:

USTRUCT()
struct FProjectClampAnimOp : public UE::UAF::FUAFAnimOp
{
    GENERATED_BODY()
    UAF_DECLARE_ANIMOP(FProjectClampAnimOp)

    FProjectClampAnimOp()
        : FUAFAnimOp(1)
    {
        InitializeAs<FProjectClampAnimOp>();
    }

    virtual void EvaluateValues(FUAFAnimOpValueEvaluator& Evaluator) override
    {
        // 从 Evaluator 取输入 pose/value,做修正,再 push 回栈。
    }
};

内置节点

节点 数据结构 说明
Sequence Player FUAFSequencePlayerData / FUAFSequencePlayer 播放 UAnimSequence,支持 LoopMode、StartTime、Timeline、RootMotion
Input Value FUAFInputValueAnimNodeData 从 UAF 变量读 FUAFValueBundle
BlendStack FUAFBlendStack 管一个当前 child 和过渡,过渡完成后裁剪节点
Blend By Bool FUAFBlendByBoolNodeData FBindableBool 在 TrueNode/FalseNode 间切换
Simple Transition FUAFSimpleTransitionData Duration + BlendOption,内部用 FUAFBlendTwoAnimOp
Timed Transition FUAFTimedTransition 管 Source/Target、TimeRemaining 和完成通知
Apply Additive FUAFApplyAdditiveData Additive 应用
Make Dynamic Additive FUAFMakeDynamicAdditiveData 动态 additive
Offset Root Bone / Steering 对应 Warping/AnimOp 根骨和朝向修正

自定义节点示例

一个项目节点通常由 Data + Instance + AnimOp 组成:

USTRUCT(DisplayName = "Project Speed Gate")
struct FProjectSpeedGateData : public FUAFAnimNodeData
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category = "Gate")
    FBindableFloat Speed;

    UPROPERTY(EditAnywhere, Category = "Gate")
    FUAFAnimNodeDataEx MovingNode;

    UPROPERTY(EditAnywhere, Category = "Gate")
    FUAFAnimNodeDataEx IdleNode;

    virtual FUAFAnimNodePtr CreateInstance(FUAFAnimGraphUpdateContext& Context) const override;
};

struct FProjectSpeedGateNode : public FUAFBlendStack
{
    FProjectSpeedGateNode(FUAFAnimGraphUpdateContext& Context, const FProjectSpeedGateData& InData)
        : FUAFBlendStack(Context)
        , Data(&InData)
    {
    }

    virtual void PreUpdate(FUAFAnimGraphUpdateContext& Context) override
    {
        const bool bShouldMove = ResolveSpeed(Context) > 3.0f;
        // 根据 bShouldMove 创建目标 child,并通过 TransitionTo 切换。
        FUAFBlendStack::PreUpdate(Context);
    }

    const FProjectSpeedGateData* Data = nullptr;
};

这是结构示例,不是可以直接复制编译的完整插件代码;真实实现还需要绑定解析、节点创建和 GC 引用处理。

项目落地

使用 AnimNode 层的建议:

  1. 想复用“树状组合 + 过渡”时用 AnimNode。
  2. 想在图评估里追加底层任务时用 Trait。
  3. 叶子节点必须设置 PostAnimOp,否则没有输出。
  4. Modifier 节点通常消费一个输入,适合用 FUAFModifierAnimNode
  5. 需要持有 UObject 的节点实现 AddReferencedObjects,不要只靠裸指针。

使用案例:武器持握过渡节点

假设项目有单手剑、双手剑、步枪三种持握方式。旧 AnimBP 里经常会出现一堆 bool:bUseRiflePosebUseTwoHandedPosebIsRelaxed。在 UAF AnimNode 层,可以把它变成一个明确的持握过渡节点:

Input variables
  WeaponStance: Unarmed / Sword1H / Sword2H / Rifle
  bIsAiming
  UpperBodyWeight

FWeaponStanceNode
  -> Current child graph
  -> Target child graph
  -> FUAFTimedTransition
  -> FUAFBlendTwoAnimOp

落地步骤:

  1. Data 里暴露 FBindableName WeaponStance,让它绑定到 UAF 公共变量。
  2. Data 里保存各个 stance 对应的 FUAFAnimNodeDataEx 或图工厂资产。
  3. Node 的 PreUpdate 读取绑定值,只在 stance 变化时创建目标 child。
  4. 切换时使用 FUAFTimedTransition,不要每帧重建子节点。
  5. PostAnimOp 输出当前 stance 混合后的 pose。
  6. 节点加 AddReferencedObjects,确保引用的动画资产或图资产不会被 GC 漏掉。

这类节点适合解决“少量离散状态 + 有过渡 + 需要复用”的问题。它比把所有分支写在 C++ Tick 里更可调,也比在 Chooser 表里塞复杂过渡更清楚。

使用案例:输入缓存节点

源码里的 FUAFInputValueAnimNode 给了一个很实用的模式:Update 阶段读取变量,Evaluate 阶段只消费缓存。如果你要做“上一帧目标点”“本帧命中方向”“脚 IK 目标”这类输入,不要在 AnimOp 里回头读变量。

struct FProjectCachedTargetNode : public FUAFAnimNode
{
    FTransform CachedTarget = FTransform::Identity;

    virtual void PreUpdate(FUAFAnimGraphUpdateContext& Context) override
    {
        Context.GetVariablesOwner()->GetVariable(TargetVariable, CachedTarget);
        SetPostAnimOp(FProjectTargetWarpOp(CachedTarget));
    }
};

原因很简单:Update 阶段能安全访问变量、处理事件、维护节点树;Evaluate 阶段应该像命令执行器,消费已经准备好的数据并产出 pose/value/notify。这个边界能让 EvaluationVM 更稳定,也让调试更容易复现。

架构分析:何时写自定义 AnimNode

需求 写自定义 AnimNode 是否合适 推荐做法
根据变量在几个子图间切换,并保留过渡状态 合适 Data 保存子节点,Node 保存当前/目标 child
只是在最终 pose 上做一个数学修正 不一定 优先写 Trait/Evaluation Task 或 Modifier AnimOp
播放一个普通 Sequence 不合适 直接用内置 Sequence Player
做复杂 asset selection 不合适 Chooser 表更适合维护
每帧读取大量 gameplay 对象 不合适 桥接组件先写 UAF 变量,Node 只读变量

如果一个自定义节点需要知道太多 gameplay 细节,通常说明边界错了。把 gameplay 摘要前置成公共变量,再让节点只关心动画选择和混合。

常见坑

  • PreAnimOp 会在孩子之前执行,PostAnimOp 在孩子之后执行;叶子生产输出优先用 PostAnimOp。
  • PostUpdate 里已经晚于 PreAnimOp 入队,不能再依赖它改变 PreAnimOp。
  • 节点重用前必须 Reset(),否则 bIsNewlyRelevant、权重和父子关系可能残留。
  • FUAFBlendStack::TransitionTo 需要 transition data,否则体验可能是硬切或默认过渡。
  • Evaluation 阶段不要访问只在 UpdateContext 安全的数据。

源码路径索引

  • UAFAnimNode/Source/UAFAnimNode/Public/UAF/AnimNodeCore/UAFAnimNode.h
  • UAFAnimNode/Source/UAFAnimNode/Public/UAF/AnimNodeCore/UAFAnimNodeUpdate.h
  • UAFAnimNode/Source/UAFAnimNode/Public/UAF/AnimOpCore/UAFAnimOp.h
  • UAFAnimNode/Source/UAFAnimNode/Public/UAF/AnimNodes/UAFSequencePlayer.h
  • UAFAnimNode/Source/UAFAnimNode/Public/UAF/AnimNodes/UAFBlendStack.h