自定义 Sequencer 轨道开发

发布于 2023-02-18  1039 次阅读


自定义 Sequencer 轨道开发

Sequencer是虚幻引擎4中用于实时创建和预览过场动画序列的多轨道编辑器。
具体的使用逻辑笔者不再赘述,官方文档:过场动画和Sequencer,相信能看到本文的读者或多或少都有些额外的开发需求,所以本文主要就对于拓展 Sequencer 进行分析,网上有一些 Sequencer 的源码剖析或开发案例,但针对预览逻辑大多没有太多描述,本文中最终会拓展一条自定义轨道,并完成自定义的预览逻辑。

笔者注:
UE 官方在4.26 版本添加了可自定义的插件,基于此插件,完成上图相应接口的逻辑,可以快速的完成自定义 Sequencer 的开发,如果需求满足,或许可以先尝试使用新版插件。但截至目前为止(UE 5.0.2,笔者版本),此插件仍然为测试版,关于插件用法可参考笔者此文Customizable Sequencer Tracks 插件调研与使用,本文中还是主要基于当前版本的 Sequencer,模仿系统逻辑,来进行自定义轨道的开发。

一、 概述

为了方便测试,笔者新建了名为 CustomSequencer 的插件,在插件中创建了 LevelViewport 并打开了系统的 Sequencer,来观察 Sequencer 预览的效果。
最终文件列表如下:

其中,蓝色框选部分为插件开发部分,本文不再展开,读者可参考最后源码或查看笔者此文UE 插件开发流程
红色框选的部分就是本文的重点,也就是自定义 Sequencer 的部分,其中笔者按类别划分了四个文件夹,但按照功能逻辑,Section 与 Track 同属数据层,本质上我们需要完成的主要有三部分逻辑:

  1. 数据
    • UMovieSceneTrack
    • UMovieSceneSection
  2. 运行时
    • FMovieSceneEvalTemplate
    • IMovieSceneExecutionToken
  3. UI
    • ISequencerSection
    • FMovieSceneTrackEditor

本文也将按照这三部分以此展开。

文末附有本文测试 Demo 链接,读者可对比查阅。

二、 模块解析

1. 数据

Sequencer 中的数据主要有四部分,自定义拓展其两部分功能足矣,一部分是持有 Sections 的 Track,另一部分就 Section 本身。

  1. UMovieSceneSection

    #pragma once
    #include "CoreMinimal.h"
    #include "MovieSceneSection.h"
    #include "TestSection.generated.h"
    
    UCLASS()
    class CUSTOMSEQUENCERPLUGIN_API UMovieSceneTestSection :public UMovieSceneSection
    {
        GENERATED_BODY()
    public:
        UMovieSceneTestSection(const FObjectInitializer& ObjectInitializer);
        virtual ~UMovieSceneTestSection();
    
    public:
    #if WITH_EDITOR
        virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
    #endif
    private:
    };

    定义 Section 的数据类,继承自 UMovieSceneSection,此处是真正添加到轨道中的 Section 片段,持有着我们所需的信息,基类中包含了片段起始、结束位置、关键帧数据、曲线数据等。
    我们也可以使用 UPROPERTY 宏来定义我们自己所需的数据,此种方式定义的数据,也将默认反射到 Section 的细节面板上,以供 UI 状态下进行调整。
    当 UI 属性发生变更时,我们可以在 PostEditChangeProperty 中收到相应回调,并通过一定手段进行筛选,以完成特殊的自定义数据逻辑,当然对于数据的限制很多 UPROPERTY 说明符已经足够使用,具体可参考笔者此文:UPROPERTY 说明符调研与示例

  2. UMovieSceneTrack

    #pragma once
    #include "Tracks/MovieSceneSpawnTrack.h"
    #include "Compilation/IMovieSceneTrackTemplateProducer.h"
    #include "Camera/CameraActor.h"
    #include "MovieSceneNameableTrack.h"
    #include "TestTrack.generated.h"
    
    class UMovieSceneTestSection;
    
    UCLASS()
    class CUSTOMSEQUENCERPLUGIN_API UMovieSceneTestTrack : public UMovieSceneNameableTrack, public IMovieSceneTrackTemplateProducer
    {
        GENERATED_BODY()
    public:
        UMovieSceneTestTrack(const FObjectInitializer& ObjectInitializer);
        virtual ~UMovieSceneTestTrack();
    
    public:
        virtual void AddSection(UMovieSceneSection& Section) override;
        virtual class UMovieSceneSection* CreateNewSection() override;
        virtual const TArray& GetAllSections() const override;
        virtual bool HasSection(const UMovieSceneSection& Section) const override;
        virtual bool IsEmpty() const override;
        // virtual void RemoveAllAnimationData() override;
        virtual void RemoveSection(UMovieSceneSection & Section) override;
        virtual FName GetTrackName() const;
        virtual bool SupportsMultipleRows() const;
        virtual bool SupportsType(TSubclassOf SectionClass) const;
    
        // 用于测试的 Actor
        TWeakObjectPtr TestCameraActor;
    
        virtual FMovieSceneEvalTemplatePtr CreateTemplateForSection(const UMovieSceneSection& InSection) const override;
    
    private:
        UPROPERTY()
        TArray Sections;
    };

    定义 Track 的数据类,继承自 UMovieSceneTrack, UMovieSceneNameableTrack 同样继承自该类,只是拓展了一些自定义轨道名称之类的接口,笔者这里继承了 UMovieSceneNameableTrack,以获得自定义名称的能力。
    Track 类中持有着 Section 的列表,并需要实现维护 Section 列表的相应接口,

    • virtual class UMovieSceneSection* CreateNewSection() PURE_VIRTUAL(UMovieSceneTrack::CreateNewSection, return nullptr;); 当点击轨道上添加 Section 按钮时回调的方法,需要在此处创建我们数据类的 Section 并返回。
    • virtual const TArray<UMovieSceneSection*>& GetAllSections() const PURE_VIRTUAL(UMovieSceneTrack::GetAllSections, static TArray<UMovieSceneSection*> Empty; return Empty;); 获得 Track 持有的 Section 列表。

    其余接口功能清晰,可自行查看 Engine\Source\Runtime\MovieScene\Public\MovieSceneTrack.h

    或许读者还发现了,此数据类还继承了 IMovieSceneTrackTemplateProducer,没错,Track 中还需实现此类的 virtual FMovieSceneEvalTemplatePtr CreateTemplateForSection(const UMovieSceneSection& InSection) const = 0; 方法, 此方法用于定义何种运行时(即预览)的类需要被创建

  3. UMovieSceneColorSection

  4. UMovieSceneBoolSection

    • UMovieSceneSpawnSection

2. UI

UI 同样由两部分组成,这里笔者将其类均置于了 TesTrackEditor.h 文件中,此处文件完成系统需求逻辑后,后续基本不会对此处逻辑进行开发。包括 ISequencerSection 与 FMovieSceneTrackEditor 两部分。

  1. ISequencerSection

    class FMovieSceneTestSection : public ISequencerSection
    , public TSharedFromThis
    {
    public:
        FMovieSceneTestSection(UMovieSceneSection& InSection, TWeakPtr InSequencer);
        virtual ~FMovieSceneTestSection();
    
    public:
        virtual UMovieSceneSection* GetSectionObject() override;
    
        virtual FText GetSectionTitle() const override;
        virtual FText GetSectionToolTip() const override;
        virtual float GetSectionHeight() const override;
    
        virtual int32 OnPaintSection(FSequencerSectionPainter& Painter) const override;
        virtual bool SectionIsResizable() const{ return true; }
        virtual void GenerateSectionLayout(class ISectionLayoutBuilder& LayoutBuilder);
    
        virtual void BeginResizeSection() override;
        virtual void ResizeSection(ESequencerSectionResizeMode ResizeMode, FFrameNumber ResizeTime) override;
    
        virtual void BeginSlipSection() override;
        virtual void SlipSection(FFrameNumber SlipTime) override;
    
    private:
        TWeakObjectPtr Section;
        TWeakPtr Sequencer;
    };

    定义了一些 Section UI 的渲染参数,以及一些 UI 操作,此处常用 virtual bool SectionIsResizable() const{ return true; } 可以使用此参数来控制 Section 长度是否可拖动。

  2. FMovieSceneTrackEditor

    class CUSTOMSEQUENCERPLUGIN_API FMovieSceneTestPreviewEditor : public FMovieSceneTrackEditor
    {
    public:
        FMovieSceneTestPreviewEditor(TSharedRef InSequencer);
        virtual ~FMovieSceneTestPreviewEditor();
    
        static TSharedRef CreateTrackEditor(TSharedRef OwningSequencer);
    
    public:
        virtual TSharedRef MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override;
        virtual bool SupportsSequence(UMovieSceneSequence* InSequence) const override;
        virtual bool SupportsType(TSubclassOf Type) const override;
        virtual const FSlateBrush* GetIconBrush() const override;
        virtual void BuildObjectBindingTrackMenu(FMenuBuilder& MenuBuilder, const TArray& ObjectBindings, const UClass* ObjectClass) override;
        virtual bool IsResizable(UMovieSceneTrack* InTrack) const override;
    
        virtual bool OnAllowDrop(const FDragDropEvent& DragDropEvent, FSequencerDragDropParams& DragDropParams) override;
        virtual FReply OnDrop(const FDragDropEvent& DragDropEvent, const FSequencerDragDropParams& DragDropParams) override;
    
    private:
        void HandleAddTrackOnActorMenuEntryExecute(FMenuBuilder&, TArray);
    };

    定义了一些 Track UI 的渲染参数,显示 Sequencer 轨道以及对应的 UI 操作拓展点。处理按钮、UI 等行为以添加自定义轨道或添加关键帧的回调。
    virtual bool SupportsType(TSubclassOf<UMovieSceneTrack> Type) const override; 此方法用于判断创建轨道的类型,必须实现。
    virtual TSharedRef<ISequencerSection> MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override; 此方法用于创建一个对应 UI 的 ISequencerSection 对象,并使用其子类完成的逻辑来控制这个 Section 的 UI 操作,上述继承 ISequencerSection 获得的能力必须完成此方法才能生效。

3. 预览

  1. FMovieSceneEvalTemplate

    #pragma once
    
    #include "CoreMinimal.h"
    #include "Evaluation/MovieSceneEvalTemplate.h"
    #include "../Tracks/TestPreviewTrack.h"
    #include "TestPreviewTemplate.generated.h"
    
    class UMovieSceneTestPreviewTrack;
    class UMovieSceneTestPreviewSection;
    
    USTRUCT()
    struct FMovieSceneTestPreviewSectionTemplate : public FMovieSceneEvalTemplate
    {
        GENERATED_BODY()
    
        FMovieSceneTestPreviewSectionTemplate();
        FMovieSceneTestPreviewSectionTemplate(const UMovieSceneTestPreviewSection& Section, const UMovieSceneTestPreviewTrack& Track);
    
    private:
        virtual UScriptStruct& GetScriptStructImpl() const override { return *StaticStruct(); }
        virtual void Evaluate(const FMovieSceneEvaluationOperand& Operand, const FMovieSceneContext& Context, const FPersistentEvaluationData& PersistentData, FMovieSceneExecutionTokens& ExecutionTokens) const override;
    
    };

    FMovieSceneEvalTemplate 是能实现自定义预览逻辑的核心类。在此类中,会计算插值好运行时需要播放的数据,并移交给执行 Token 去完成预览逻辑。

    在此类中,需要实现 Evaluate 方法,此方法用于完成最终预览计算前的一些必须的、昂贵的逻辑,用于为真正的预览准备、组织完全的数据。 此函数可能会由线程调用,所以不可在此处实现预览相关逻辑,仅为当前 Evaluate 的位置做一些预览数据的准备工作即可。入参 const FMovieSceneContext& Context 持有当前 Evaluate 位置的信息,可以通过 Gettime() 方法获取当前 Evaluate 位置的时间。

  2. IMovieSceneExecutionToken

    // 代码示例中,笔者使用当前时间计算了相机的高度,并存入了执行 Token 队列中
    void FMovieSceneTestPreviewSectionTemplate::Evaluate(const FMovieSceneEvaluationOperand& Operand, const FMovieSceneContext& Context, const FPersistentEvaluationData& PersistentData, FMovieSceneExecutionTokens& ExecutionTokens) const
    {
        if (GEngine && GEngine->UseSound() && Context.GetStatus() != EMovieScenePlayerStatus::Jumping)
        {
            const UMovieSceneTestPreviewSection* TestSection = Cast(GetSourceSection());
    
            float TimeValue = Context.GetTime().AsDecimal() / 100;
            UMovieSceneTestPreviewTrack* TestTrack = Cast(TestSection->GetOuter());
            ExecutionTokens.Add(FTestPreviewSectionExecutionToken(TestSection, FVector(0,0,TimeValue)));
        }
    }

    FMovieSceneEvalTemplate 的 Evaluate 方法的入参 FMovieSceneExecutionTokens& ExecutionTokens 是真正将要预览的 Token 队列(模板中会进行 Sort ,对 Sort 后的 Token 队列执行预览逻辑),我们需要继承 IMovieSceneExecutionToken 接口,实现我们自己的 Token 类。在 Template 的 Evaluate 方法处,我们需要完成所有的昂贵的计算工作,并创建自定义 Token 类的实例,并将计算得到的即预览所需的数据存入此实例中,方法中,需要将我们自定义的 Token 类型的参数实例添加到入参 FMovieSceneExecutionTokens& ExecutionTokens 中,系统会在合适的时机,调用 Token 中的 Execute() 方法,执行相应的预览逻辑。

    对于数据类 UMovieSceneTrack,自定义 Track 还需继承 IMovieSceneTrackTemplateProducer,并实现 CreateTemplateForSection 方法,添加预览与编辑之间的关联。

    UCLASS()
    class CUSTOMSEQUENCERPLUGIN_API UMovieSceneTestPreviewTrack : public UMovieSceneNameableTrack, public IMovieSceneTrackTemplateProducer
    {
        GENERATED_BODY()
    public:
    ...
    virtual FMovieSceneEvalTemplatePtr CreateTemplateForSection(const UMovieSceneSection& InSection) const override;
    ...
    }
    FMovieSceneEvalTemplatePtr UMovieSceneTestPreviewTrack::CreateTemplateForSection(const UMovieSceneSection& InSection) const
    {
        return FMovieSceneTestPreviewSectionTemplate(*CastChecked(&InSection), *this);
    }

    这样就完成了Runtime 预览的逻辑。

三、完成 Demo 示例

轨道类创建完毕后,为了测试自定义轨道的效果,还需在自定义插件中完成以下三部分:

1. 注册

完成上述逻辑后,我们以及完成了创建一条自定义轨道,并实现了其预览的逻辑,此时只剩下轨道的注册逻辑。
在插件的virtual void StartupModule() override; 方法中,添加

ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked<ISequencerModule>("Sequencer");
MovieSceneTestEditorHandler = SequencerModule.RegisterTrackEditor(FOnCreateTrackEditor::CreateStatic(&FMovieSceneTestEditor::CreateTrackEditor));

注册自定义轨道类型。并在virtual void ShutdownModule() override;中取消注册

ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked<ISequencerModule>("Sequencer");
SequencerModule.UnRegisterTrackEditor(MovieSceneTestEditorHandler);

2. 创建 Viewport 以观察预览效果

TSharedRef<class SDockTab> OnSpawnPluginTab(const class FSpawnTabArgs& SpawnTabArgs);方法中,添加如下逻辑:

TSharedRef<SDockTab> FCustomSequencerPluginModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
    TSharedRef< SDockTab> DockTab = SNew(SDockTab);
    // DockTab->SetTabIcon(FEditorStyle::GetBrush("LevelEditor.Tabs.Viewports"));
    TSharedPtr<FLevelViewportTabContent> ViewportTabContent = MakeShareable(new FLevelViewportTabContent());
    auto MakeLevelViewportFunc = [this](const FAssetEditorViewportConstructionArgs& InConstructionArgs)
    {
        FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
        TSharedPtr<ILevelEditor> LevelEditor = LevelEditorModule.GetFirstLevelEditor();
        return SNew(SLevelViewport, InConstructionArgs).ParentLevelEditor(LevelEditor);
    };
    ViewportTabContent->Initialize(MakeLevelViewportFunc, DockTab, TEXT(" Test Viewport"));
    return  DockTab;
}

3. 打开 Sequencer 界面

可以在void PluginButtonClicked();方法中添加如下逻辑:

void FCustomSequencerPluginModule::PluginButtonClicked()
{
    FGlobalTabmanager::Get()->TryInvokeTab(CustomSequencerPluginTabName);
    ULevelSequence* LevelSequencer = NewObject<ULevelSequence>();
    LevelSequencer->Initialize();
    const UMovieSceneToolsProjectSettings* ProjectSettings = GetDefault<UMovieSceneToolsProjectSettings>();
    FFrameRate TickResolution = LevelSequencer->GetMovieScene()->GetTickResolution();
    LevelSequencer->GetMovieScene()->SetPlaybackRange((ProjectSettings->DefaultStartTime * TickResolution).FloorToFrame(),
        (ProjectSettings->DefaultDuration * TickResolution).FloorToFrame().Value);
    UMovieSceneTestTrack* TestTrack = Cast<UMovieSceneTestTrack>(LevelSequencer->MovieScene->AddMasterTrack(UMovieSceneTestTrack::StaticClass()));
    check(TestTrack != nullptr);
    if (TestTrack != nullptr)
    {
        TestTrack->SetDisplayName(FText::FromString(" Test Track "));
        UWorld* World = GEditor->GetEditorWorldContext().World();
        check(World != nullptr);
        ACameraActor* CameraActor = World->SpawnActor<ACameraActor>();
        TestTrack->TestCameraActor = CameraActor;
    }
    ULevelSequenceEditorBlueprintLibrary::OpenLevelSequence(LevelSequencer);
}

4. 预览效果演示

启动插件后,会弹出一个 Viewport 并弹出系统的 Sequencer 界面,如果 Sequencer 界面未弹出,也可能吸附在了主界面上,可以回到 UE 主界面查看。轨道的效果与系统基本一致,此处仅演示预览效果,在本文的预览逻辑示例代码中,TestPreviewTrack 持有一个相机,预览时会使用当前 EValuate 位置的时间对此相机的高度进行修正。按照系统方法昂贵的计算在 Evaluate 方法中完成的逻辑,我们在 Evaluate 方法处,使用这一时间计算了相机应该设置的高度,并使用此高度创建了自定义 Token 类的实例。自定义的 Token 类中保存了 TestPreviewSection 与 一个 FVector 类型的新坐标,并存入了真正将要执行的 Token 队列,在 Execute 即真正的预览执行时,使用构造时的入参设置了相机的 Location。
这样就完成了对相机高度修正的预览效果。最终表现为,相机高度随着播放位置变化。

笔者 Demo 示例较为简单,但基本涉及到了所有自定义的逻辑,完整跑通了预览流程,读者可从此处获取全部源码 demo链接

常用接口

开发过程中,需要利用很多的系统回调方法,系统也提供了很多

参考链接:
此处有很多内容,可以保留学习
Gamedev Guide

Sequencer 構造解説とカスタムトラック追加 (UE4.18版) – カスタムトラック編
Sequencer 構造解説とカスタムトラック追加 (UE4.18版) – 応用編
如何快速自定义Sequencer Track
过场动画和Sequencer

如堕五里雾中
最后更新于 2023-07-04