自定义 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 同属数据层,本质上我们需要完成的主要有三部分逻辑:
- 数据
- UMovieSceneTrack
- UMovieSceneSection
- 运行时
- FMovieSceneEvalTemplate
- IMovieSceneExecutionToken
- UI
- ISequencerSection
- FMovieSceneTrackEditor
本文也将按照这三部分以此展开。
文末附有本文测试 Demo 链接,读者可对比查阅。
二、 模块解析
1. 数据
Sequencer 中的数据主要有四部分,自定义拓展其两部分功能足矣,一部分是持有 Sections 的 Track,另一部分就 Section 本身。
-
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 说明符调研与示例 -
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 列表的相应接口,
当点击轨道上添加 Section 按钮时回调的方法,需要在此处创建我们数据类的 Section 并返回。virtual class UMovieSceneSection* CreateNewSection() PURE_VIRTUAL(UMovieSceneTrack::CreateNewSection, return nullptr;);
获得 Track 持有的 Section 列表。virtual const TArray<UMovieSceneSection*>& GetAllSections() const PURE_VIRTUAL(UMovieSceneTrack::GetAllSections, static TArray<UMovieSceneSection*> Empty; return Empty;);
其余接口功能清晰,可自行查看 Engine\Source\Runtime\MovieScene\Public\MovieSceneTrack.h
或许读者还发现了,此数据类还继承了 IMovieSceneTrackTemplateProducer,没错,Track 中还需实现此类的
方法, 此方法用于定义何种运行时(即预览)的类需要被创建virtual FMovieSceneEvalTemplatePtr CreateTemplateForSection(const UMovieSceneSection& InSection) const = 0;
-
UMovieSceneColorSection
-
UMovieSceneBoolSection
- UMovieSceneSpawnSection
2. UI
UI 同样由两部分组成,这里笔者将其类均置于了 TesTrackEditor.h 文件中,此处文件完成系统需求逻辑后,后续基本不会对此处逻辑进行开发。包括 ISequencerSection 与 FMovieSceneTrackEditor 两部分。
-
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 操作,此处常用
可以使用此参数来控制 Section 长度是否可拖动。virtual bool SectionIsResizable() const{ return true; }
-
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;
此方法用于创建一个对应 UI 的 ISequencerSection 对象,并使用其子类完成的逻辑来控制这个 Section 的 UI 操作,上述继承 ISequencerSection 获得的能力必须完成此方法才能生效。virtual TSharedRef<ISequencerSection> MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override;
3. 预览
-
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 位置的时间。
-
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
Comments 1 条评论
博主 风飘飖兮难遇雨
==感谢作者,帮大忙了!==
最近被研究Sequence动态绑定和自定义轨道搞得晕头转向了,官方文档给的那个蓝图实现在动态绑定下无法获取到BindingObject。