【UE】记录当前播放的动画序列名(AnimSequence-Montage)

记录当前播放的动画序列名(AnimSequence/Montage)

适用范围:Unreal Engine 5.x。本文示例使用自定义动画实例类 UBaseAnimInstance

在调试动画蓝图时,我们经常需要知道角色当前使用的是哪一个动画资源。状态机只能告诉我们当前状态,而一个状态内部可能同时包含 Sequence PlayerSequence Evaluator、混合节点以及 Slot,因此仅记录状态名还不够。

下面实现一个统一入口:

  • Sequence Player:从动画节点引用中取得正在更新的 AnimSequence
  • Sequence Evaluator:从动画节点引用中取得当前求值的 AnimSequence
  • Montage:在播放 Montage 或动态 Montage 时,直接传入对应动画资源。
  • 最终把资源名保存到 CurrentAnimSequenceName,供蓝图、屏幕调试信息或日志读取。

这套功能更适合作为调试工具。发生动画混合时,可能有多个节点在同一帧更新,最终显示的是最后一次写入的资源名,并不一定是混合权重最大的动画。

效果

1789611700718

1789611690899

1789611715718

1789611669948

一、添加模块依赖

在项目模块的 Build.cs 中加入 AnimGraphRuntime

PrivateDependencyModuleNames.AddRange(new string[]
{
    "AnimGraphRuntime"
});

如果原本已经有 PrivateDependencyModuleNames.AddRange,直接把模块名合并进去即可,不需要重复添加一行配置。

二、头文件声明

下面以 UBaseAnimInstance 为例。generated.h 必须保持为该头文件最后一个 #include

BaseAnimInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimNodeReference.h"
#include "BaseAnimInstance.generated.h"

class UAnimSequenceBase;

UCLASS()
class THIRDPERSONCONTROL_API UBaseAnimInstance : public UAnimInstance
{
    GENERATED_BODY()

public:
    // 当前记录到的动画资源名,便于在动画蓝图或调试界面中读取。
    UPROPERTY(BlueprintReadWrite, Category = "AnimSequence")
    FString CurrentAnimSequenceName;

#pragma region 记录当前播放的动画序列名

    // 用于 AnimGraph 节点函数,也可通过 Montage 参数直接传入动画资源。
    UFUNCTION(BlueprintCallable, Category = "Animation|Sequence",
        meta = (BlueprintThreadSafe))
    void UpdateCurrentAnimSequenceName(
        FAnimNodeReference Node,
        UAnimSequenceBase* Montage);

    // C++ 便捷重载:播放 Montage 或动态 Montage 时无需构造节点引用。
    void UpdateCurrentAnimSequenceName(UAnimSequenceBase* Montage)
    {
        UpdateCurrentAnimSequenceName(FAnimNodeReference(), Montage);
    }

#pragma endregion
};

这里将 Montage 参数声明为 UAnimSequenceBase*,是为了同时兼容普通动画序列和 Montage。UAnimMontage 最终继承自 UAnimSequenceBase,因此可以直接传入。

同名重载没有添加 UFUNCTION,它只是给 C++ 调用的便捷入口。蓝图侧使用的是带 FAnimNodeReference 参数的版本。

三、实现函数

BaseAnimInstance.cpp

#include "BaseAnimInstance.h"

#include "Animation/AnimNode_SequencePlayer.h"
#include "Animation/AnimSequenceBase.h"
#include "AnimNodes/AnimNode_SequenceEvaluator.h"
#include "Kismet/KismetSystemLibrary.h"

void UBaseAnimInstance::UpdateCurrentAnimSequenceName(
    FAnimNodeReference Node,
    UAnimSequenceBase* Montage)
{
    const UAnimSequenceBase* Sequence = nullptr;

    // Sequence Player
    if (const FAnimNode_SequencePlayer* SequencePlayer =
        Node.GetAnimNodePtr<FAnimNode_SequencePlayer>())
    {
        Sequence = SequencePlayer->GetSequence();
    }
    // Sequence Evaluator
    else if (const FAnimNode_SequenceEvaluator* SequenceEvaluator =
        Node.GetAnimNodePtr<FAnimNode_SequenceEvaluator>())
    {
        Sequence = SequenceEvaluator->GetSequence();
    }

    CurrentAnimSequenceName =
        UKismetSystemLibrary::GetDisplayName(Sequence);

    // 直接传入 Montage 时,让 Montage 名称覆盖节点解析结果。
    if (Montage)
    {
        CurrentAnimSequenceName =
            UKismetSystemLibrary::GetDisplayName(Montage);
    }
}

函数首先尝试判断 FAnimNodeReference 实际引用的节点类型:

  1. 如果是 FAnimNode_SequencePlayer,调用 GetSequence() 获取动画序列。
  2. 如果是 FAnimNode_SequenceEvaluator,同样调用 GetSequence() 获取当前求值的动画序列。
  3. 如果额外传入了 Montage,则用该资源名覆盖前面从节点中取得的结果。

之所以让 Montage 拥有更高优先级,是因为 C++ 便捷重载会传入一个空的 FAnimNodeReference

UpdateCurrentAnimSequenceName(FAnimNodeReference(), Montage);

此时节点解析自然会失败,但随后仍能正确记录 Montage 名称。

四、在 AnimGraph 中记录 AnimSequence

在动画蓝图的 AnimGraph 中选择需要观察的 Sequence PlayerSequence Evaluator 节点,然后在 Details 面板的节点函数区域,将更新回调绑定到 Update Current Anim Sequence Name

执行回调时,引擎会把当前节点包装为 FAnimNodeReference 传入函数。Montage 引脚不传值,保持为空即可。函数会根据实际节点类型提取动画资源并更新 CurrentAnimSequenceName

如果多个节点都绑定了这个函数,在状态切换或 Blend 期间它们可能会在同一帧先后写入变量。因此这个值表达的是“最近一次执行回调的动画节点”,而不是严格意义上的“当前唯一播放动画”。

五、记录 Montage

播放普通 Montage

确认播放成功后,将 Montage 传给便捷重载:

if (MontageToPlay && Montage_Play(MontageToPlay) > 0.0f)
{
    UpdateCurrentAnimSequenceName(MontageToPlay);
}

播放动态 Montage

PlaySlotAnimationAsDynamicMontage 接收的是 UAnimSequenceBase*,因此可以直接记录传入的源动画:

if (SequenceToPlay)
{
    UAnimMontage* DynamicMontage = PlaySlotAnimationAsDynamicMontage(
        SequenceToPlay,
        SlotName,
        0.2f,
        0.25f,
        1.0f);

    if (DynamicMontage)
    {
        UpdateCurrentAnimSequenceName(SequenceToPlay);
    }
}

上面的动态 Montage 示例记录的是源 AnimSequence 名称。如果希望显示运行时创建的动态 Montage 名称,也可以改为传入 DynamicMontage

六、读取和显示结果

CurrentAnimSequenceName 已使用 BlueprintReadWrite 暴露给蓝图,可以直接在动画蓝图中读取。例如将它接入自定义屏幕调试节点,运行时便可能看到:

CurrentAnimSequence: AS_Run_F

或:

CurrentAnimSequence: AM_TurnLeft_90

如果只是读取而不希望蓝图改写变量,可以把属性标记改为 BlueprintReadOnly

UPROPERTY(BlueprintReadOnly, Category = "AnimSequence")
FString CurrentAnimSequenceName;

七、注意事项

1. Sequence Evaluator 不一定真的在“播放”

Sequence Evaluator 通常根据显式时间对动画进行求值。本文仍将它纳入记录范围,是因为调试时我们关心的是该节点当前使用了哪个动画资源。

2. 混合期间可能出现名称跳动

状态过渡、Blend Space、Linked Anim Graph 或 Slot 混合期间,多个节点可能同时参与更新。所有节点写入同一个字符串时,结果取决于本帧最后一次调用。

如果需要显示主导动画,应进一步记录各节点的混合权重,再选择权重最大的动画;本文的轻量实现不处理权重。

3. Montage 停止后不会自动清空

便捷重载只在调用时记录资源名。Montage 播放结束后,如果没有其他 AnimGraph 节点继续更新变量,旧名称仍会保留。需要严格同步生命周期时,应在 Montage 的 Blend Out 或 End 回调中清空:

CurrentAnimSequenceName.Reset();

4. BlueprintThreadSafe 不会自动保证线程安全

该元数据表示函数允许出现在动画蓝图的线程安全调用环境中,并不代表函数内部任意操作都会自动变得线程安全。本文写入的是动画实例自身的调试变量;不要在函数中访问 Actor、World、组件状态,或调用只能在游戏线程执行的逻辑。

如果其他游戏线程代码会同时读取这个 FString,应通过动画实例的标准数据同步方式传递结果,或将读取与写入安排在同一线程。不要把这个调试字符串作为关键玩法逻辑的状态依据。

5. 不要缓存节点内部指针

GetAnimNodePtr 返回的指针只应在当前回调中立即使用,不要保存到成员变量,也不要跨帧访问。

八、总结

这套实现把 AnimGraph 节点和 Montage 两种来源统一到了 CurrentAnimSequenceName

Sequence Player / Sequence Evaluator
                └─ FAnimNodeReference ─┐
                                       ├─ UpdateCurrentAnimSequenceName
Montage / Dynamic Montage ─────────────┘
                                                     │
                                                     └─ CurrentAnimSequenceName

它的优点是代码量少、蓝图读取方便,并且同时覆盖常见的 Sequence PlayerSequence Evaluator 和 Montage 调试场景。需要注意的是,这里记录的是最近一次更新到的动画资源名;当多个动画同时混合时,它不能替代真正的动画权重分析。

posted @ 2026-09-17 10:16  EanoJiang  阅读(10)  评论(0)    收藏  举报