SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

UE4.26 LevelSequence用Track実装

こんにちは、スパーククリエイティブのエンジニアの佐藤陽介です。

レベルシーケンサーで使用可能なC++実装のトラックを紹介していきたいと思います。
読み辛い、間違っている点がありましたらご指摘お願い致します。

レベルシーケンサーについて

シーケンサーは複数種類ありますが実装したトラックはレベルシーケンサーでのみ使用可能なものを想定しています。

レベルシーケンサーはトラックとトラック内のセクションという構成で形成されています。
画像の赤枠がトラック、オレンジ枠がセクションになります。
f:id:spark-sato-yosuke:20211028154502p:plain
今回実装するトラックは画像のAudioトラックのように1トラックに対して複数セクションを配置可能なものを実装します。

トラック実装に必要なモジュール

Runtime

・MovieScene
  Runtime 時に動作するトラック・セクションの基底クラスの定義。

Editor

・Sequencer
  Editor 時にのみ動作する Sequencer 関連のクラス定義。
  各トラック用の Editor 時のみ動作する基底クラスの定義。
・MovieSceneTools
  UE4 標準で実装されている各トラック用の Editor クラス定義

トラック実装に必要なクラス

Runtime

・UMovieSceneNameableTrack
Runtime時のトラックを担うクラス
セクションの管理、セクションの再生停止を管理しているFMovieSceneTrackImplementationを生成等を行う

・UMovieSceneSection
トラック内のセクションに対応したクラス
セクションを右クリックしたときのプロパティのメンバ変数を管理する

・FMovieSceneTrackImplementation
シークエンサーのタイムラインが動いた場合に具体的にどのような挙動を行うのかを実装するクラス
セクションの再生停止のタイミングを実装者が取得する必要がある
FMovieSceneEvalTemplateで代用可能、こちらは再生終了タイミングが関数で取得可能

Editor

・FMovieSceneTrackEditor
Editor時のトラック操作用クラス
トラックの+ボタン、ウィジェット、セクションの生成等のカスタマイズ等の処理を実装可能

・ISequencerSection
Editor時のセクション操作用クラス
セクションカラー、セクションの描画、セクション名等の設定が可能

実装手順

1.2.についてはエディタのモジュール追加についてなので、知っている人は飛ばしてください。

1.C++UE4のプロジェクトを作成

f:id:spark-sato-yosuke:20211108030003p:plain
C++の実装になるので赤枠内を切り替えて下さい。

2.プロジェクトにエディターのモジュールを追加

2-1.モジュールの追加

作成したプロジェクトのSourceフォルダ以下にUE4TrackSampleEdフォルダを作成し
UE4TrackSampleEd.Build.cs、UE4TrackSampleEd.cppを用意します。
f:id:spark-sato-yosuke:20211108035514p:plain
UE4TrackSampleEd.Build.cs

using UnrealBuildTool;

public class UE4TrackSampleEd : ModuleRules
{
	public UE4TrackSampleEd(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UE4TrackSample", "UE4TrackSample" });

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

UE4TrackSampleEd.cpp

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FUE4TrackSampleEd : public IModuleInterface
{
	virtual void StartupModule() override	{	}
	virtual void ShutdownModule() override	{	}
};

IMPLEMENT_MODULE(FUE4TrackSampleEd, UE4TrackSampleEd);
2-2.モジュール設定

「UE4TrackSampleEd.Build.cs」「UE4TrackSampleEditor.Target.cs」「UE4TrackSample.uproject」
にそれぞれの設定を追記します。
f:id:spark-sato-yosuke:20211108035925p:plain

2-3.起動確認

UE4TrackSample.uprojectを右クリックして「Generate Visual Studio project file」を実行してからVisulaStudioを起動させてください。
f:id:spark-sato-yosuke:20211108041308p:plain

3.モジュールの追加

Runtime

PublicDependencyModuleNamesに"MovieScene"を追加

UE4TrackSample.Build.cs

using UnrealBuildTool;

public class UE4TrackSample : ModuleRules
{
	public UE4TrackSample(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "MovieScene" });

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

PrivateDependencyModuleNamesに"UnrealEd", "Slate", "SlateCore", "Sequencer", "MovieScene", "MovieSceneTools"を追加

UE4TrackSampleEd.Build.cs

using UnrealBuildTool;

public class UE4TrackSampleEd : ModuleRules
{
	public UE4TrackSampleEd(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UE4TrackSample" });

		PrivateDependencyModuleNames.AddRange(new string[] { "UnrealEd", "Slate", "SlateCore", "Sequencer", "MovieScene", "MovieSceneTools"});
	}
}

4.クラスの実装

フォルダ構成は画像のようになっています。
f:id:spark-sato-yosuke:20211109044532p:plain

Runtime

SampleNameableTrack.h

#pragma once

#include "CoreMinimal.h"
#include "MovieSceneNameableTrack.h"
#include "Compilation/IMovieSceneTrackTemplateProducer.h"
#include "SampleNameableTrack.generated.h"

class USampleSection;

UCLASS()
class UE4TRACKSAMPLE_API USampleNameableTrack
	: public UMovieSceneNameableTrack
	, public IMovieSceneTrackTemplateProducer
{
	GENERATED_BODY()
	
public:
	UPROPERTY(Category = "Sequencer", EditDefaultsOnly, AssetRegistrySearchable)
		TArray<TSubclassOf<USampleSection>> SupportedSections;

	UPROPERTY(Category = "Sequencer", EditDefaultsOnly, AssetRegistrySearchable)
		TSubclassOf<USampleSection> DefaultSectionType;

public:
	USampleNameableTrack(const FObjectInitializer& ObjectInitializer);

	// ~UMovieSceneTrack Start
	virtual const TArray<UMovieSceneSection*>& GetAllSections() const override;
	virtual void RemoveAllAnimationData() override;
	virtual void AddSection(UMovieSceneSection& Section) override;
	virtual void RemoveSection(UMovieSceneSection& Section) override;
	virtual void RemoveSectionAt(int32 SectionIndex) override;
	virtual bool IsEmpty() const override;
	virtual bool IsAMasterTrack() const;
	virtual bool PopulateEvaluationTree(TMovieSceneEvaluationTree<FMovieSceneTrackEvaluationData>& OutData) const override;
	// ~UMovieSceneTrack End

	// ~IMovieSceneTrackTemplateProducer interface Start
	virtual bool SupportsType(TSubclassOf<UMovieSceneSection> SectionClass) const override;
	virtual FMovieSceneEvalTemplatePtr CreateTemplateForSection(const UMovieSceneSection& InSection) const override;
	virtual void PostCompile(FMovieSceneEvaluationTrack& OutTrack, const FMovieSceneTrackCompilerArgs& Args) const override;
	// ~IMovieSceneTrackTemplateProducer interface End

protected:
	UPROPERTY()
		TArray<UMovieSceneSection*> Sections;

	/** List of all master audio sections */
	UPROPERTY()
		TArray<UMovieSceneSection*> SampleSections;
};

SampleNameableTrack.cpp

#include "Track/SampleNameableTrack.h"
#include "Track/SampleSection.h"
#include "Track/SampleTrackImplementation.h"

#include "Evaluation/MovieSceneEvaluationTrack.h"



USampleNameableTrack::USampleNameableTrack(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	SupportedBlendTypes.Add(EMovieSceneBlendType::Absolute);
	DefaultSectionType = USampleSection::StaticClass();
#if WITH_EDITORONLY_DATA
	TrackTint = FColor(93, 95, 136);
#endif
}


const TArray<UMovieSceneSection*>& USampleNameableTrack::GetAllSections() const
{
	return SampleSections;
}

bool USampleNameableTrack::IsAMasterTrack() const
{
	UMovieScene* MovieScene = Cast<UMovieScene>(GetOuter());
	return MovieScene ? MovieScene->IsAMasterTrack(*this) : false;
}



void USampleNameableTrack::RemoveAllAnimationData()
{
	SampleSections.Empty();
}


void USampleNameableTrack::AddSection(UMovieSceneSection& Section)
{
	SampleSections.Add(&Section);
}


void USampleNameableTrack::RemoveSection(UMovieSceneSection& Section)
{
	SampleSections.Remove(&Section);
}


void USampleNameableTrack::RemoveSectionAt(int32 SectionIndex)
{
	SampleSections.RemoveAt(SectionIndex);
}


bool USampleNameableTrack::IsEmpty() const
{
	return SampleSections.Num() == 0;
}

FMovieSceneEvalTemplatePtr USampleNameableTrack::CreateTemplateForSection(const UMovieSceneSection& InSection) const
{
	return FMovieSceneEvalTemplatePtr();
}


void USampleNameableTrack::PostCompile(FMovieSceneEvaluationTrack& OutTrack, const FMovieSceneTrackCompilerArgs& Args) const
{
	if (SampleSections.Num() > 0)
	{
		TArray<USampleSection*> CurrentSections;
		for (UMovieSceneSection* SmapleSection : SampleSections)
		{

			USampleSection* SpawnSection = CastChecked<USampleSection>(SmapleSection);
			CurrentSections.Add(SpawnSection);

		}
		OutTrack.SetTrackImplementation(FSampleTrackImplementation(CurrentSections));

	}
}


bool USampleNameableTrack::PopulateEvaluationTree(TMovieSceneEvaluationTree<FMovieSceneTrackEvaluationData>& OutData) const
{
	USampleNameableTrack* This = const_cast<USampleNameableTrack*>(this);
	OutData.Add(TRange<FFrameNumber>::All(), FMovieSceneTrackEvaluationData::FromTrack(This));

	return true;
}

bool USampleNameableTrack::SupportsType(TSubclassOf<UMovieSceneSection> SectionClass) const
{
	return SectionClass == USampleNameableTrack::StaticClass();
}

SampleSection.h

#pragma once

#include "CoreMinimal.h"
#include "MovieSceneSection.h"
#include "Channels/MovieSceneFloatChannel.h"
#include "SampleSection.generated.h"

UCLASS()
class UE4TRACKSAMPLE_API USampleSection : public UMovieSceneSection
{
	GENERATED_BODY()
public:
	USampleSection(const FObjectInitializer& ObjectInitializer);
	virtual EMovieSceneChannelProxyType CacheChannelProxy() override;

protected:
	UPROPERTY(EditAnywhere)
		int32 Index;

public:
	UPROPERTY()
		FMovieSceneFloatChannel TranslationCurve[3];

	UPROPERTY()
		FMovieSceneFloatChannel RotationCurve[3];

	UPROPERTY()
		FMovieSceneFloatChannel ScaleCurve[3];
};

SampleSection.cpp

#include "Track/SampleSection.h"
#include "Channels/MovieSceneChannelProxy.h"
#include "Track/SampleNameableTrack.h"

#define LOCTEXT_NAMESPACE "USampleSection"

#if WITH_EDITOR
struct FSampleChannelEditorData
{
	FSampleChannelEditorData()
	{
		FText LocationGroup = LOCTEXT("LocationGroupText", "Location");
		FText RotationGroup = LOCTEXT("RotationGroupText", "Rotation");
		FText Scale3DGroup = LOCTEXT("Scale3DGroupText", "Scale");

		MetaData[0].SetIdentifiers("LocationX", LOCTEXT("LocationXText", "X"), LocationGroup);
		MetaData[0].SortOrder = 0;

		MetaData[1].SetIdentifiers("LocationY", LOCTEXT("LocationYText", "Y"), LocationGroup);
		MetaData[1].SortOrder = 1;
		
		MetaData[2].SetIdentifiers("LocationZ", LOCTEXT("LocationZText", "Z"), LocationGroup);
		MetaData[2].SortOrder = 2;
		
		MetaData[3].SetIdentifiers("RotationX", LOCTEXT("RotationXText", "X"), RotationGroup);
		MetaData[3].SortOrder = 3;
		
		MetaData[4].SetIdentifiers("RotationY", LOCTEXT("RotationYText", "Y"), RotationGroup);
		MetaData[4].SortOrder = 4;
		
		MetaData[5].SetIdentifiers("RotationZ", LOCTEXT("RotationZText", "Z"), RotationGroup);
		MetaData[5].SortOrder = 5;
		
		MetaData[6].SetIdentifiers("Scale3DX", LOCTEXT("Scale3DXText", "X"), Scale3DGroup);
		MetaData[6].SortOrder = 6;
		
		MetaData[7].SetIdentifiers("Scale3DY", LOCTEXT("Scale3DYText", "Y"), Scale3DGroup);
		MetaData[7].SortOrder = 7;
		
		MetaData[8].SetIdentifiers("Scale3DZ", LOCTEXT("Scale3DZText", "Z"), Scale3DGroup);
		MetaData[8].SortOrder = 8;
	}
	FMovieSceneChannelMetaData MetaData[9];
};
#endif // WITH_EDITOR

USampleSection::USampleSection(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	Index = 0;

	TranslationCurve[0].SetDefault(0.f);
	TranslationCurve[1].SetDefault(0.f);
	TranslationCurve[2].SetDefault(0.f);

	RotationCurve[0].SetDefault(0.f);
	RotationCurve[1].SetDefault(0.f);
	RotationCurve[2].SetDefault(0.f);

	ScaleCurve[0].SetDefault(1.f);
	ScaleCurve[1].SetDefault(1.f);
	ScaleCurve[2].SetDefault(1.f);

}

EMovieSceneChannelProxyType USampleSection::CacheChannelProxy()
{
	// Set up the channel proxy
	FMovieSceneChannelProxyData Channels;
	USampleNameableTrack* SampleMovieSceneTrack = Cast<USampleNameableTrack>(GetOuter());

#if WITH_EDITOR
	FSampleChannelEditorData EditorData;
	// 位置
	Channels.Add(TranslationCurve[0], EditorData.MetaData[0], TMovieSceneExternalValue<float>());
	Channels.Add(TranslationCurve[1], EditorData.MetaData[1], TMovieSceneExternalValue<float>());
	Channels.Add(TranslationCurve[2], EditorData.MetaData[2], TMovieSceneExternalValue<float>());

	// 回転
	Channels.Add(RotationCurve[0], EditorData.MetaData[3], TMovieSceneExternalValue<float>());
	Channels.Add(RotationCurve[1], EditorData.MetaData[4], TMovieSceneExternalValue<float>());
	Channels.Add(RotationCurve[2], EditorData.MetaData[5], TMovieSceneExternalValue<float>());

	// 拡縮
	Channels.Add(ScaleCurve[0], EditorData.MetaData[6], TMovieSceneExternalValue<float>());
	Channels.Add(ScaleCurve[1], EditorData.MetaData[7], TMovieSceneExternalValue<float>());
	Channels.Add(ScaleCurve[2], EditorData.MetaData[8], TMovieSceneExternalValue<float>());

#else

	// 位置
	Channels.Add(TranslationCurve[0]);
	Channels.Add(TranslationCurve[1]);
	Channels.Add(TranslationCurve[2]);

	// 回転
	Channels.Add(RotationCurve[0]);
	Channels.Add(RotationCurve[1]);
	Channels.Add(RotationCurve[2]);

	// 拡縮
	Channels.Add(ScaleCurve[0]);
	Channels.Add(ScaleCurve[1]);
	Channels.Add(ScaleCurve[2]);

#endif

	ChannelProxy = MakeShared<FMovieSceneChannelProxy>(MoveTemp(Channels));

	return EMovieSceneChannelProxyType::Dynamic;
}

#undef LOCTEXT_NAMESPACE

SampleTrackImplementation.h

#pragma once

#include "CoreMinimal.h"
#include "MovieSceneSection.h"
#include "Evaluation/MovieSceneTrackImplementation.h"
#include "SampleTrackImplementation.generated.h"        

class UScriptStruct;
class USampleSection;

USTRUCT()
struct FSampleTrackImplementation : public FMovieSceneTrackImplementation
{
	GENERATED_BODY()

public:
	FSampleTrackImplementation() {};
	FSampleTrackImplementation(TArray<USampleSection*> Sections);

	virtual void SetupOverrides() override
	{
		EnableOverrides(CustomEvaluateFlag);
	}

private:
	virtual UScriptStruct& GetScriptStructImpl() const override { return *StaticStruct(); }
	virtual void Evaluate(const FMovieSceneEvaluationTrack& Track, TArrayView<const FMovieSceneFieldEntry_ChildTemplate> Children, const FMovieSceneEvaluationOperand& Operand, const FMovieSceneContext& Context, const FPersistentEvaluationData& PersistentData, FMovieSceneExecutionTokens& ExecutionTokens) const override;
	
private:
	UPROPERTY()
		TArray<USampleSection*> SampleSections;
};

SampleTrackImplementation.cpp

#include "SampleTrackImplementation.h"
#include "Track/SampleSection.h"
#include "MovieScene.h"
#include "MovieSceneExecutionToken.h"
#include "Evaluation/PersistentEvaluationData.h"
#include "Evaluation/MovieSceneEvaluationTrack.h"

struct FSampleExecutionToken : IMovieSceneExecutionToken
{
	FSampleExecutionToken(const TArray<USampleSection*> InSampleSection, TRange<FFrameNumber> InFrameNumberRange)
		:m_SampleSections(InSampleSection)
		, m_FrameNumberRange(InFrameNumberRange)
	{}


	virtual void Execute(const FMovieSceneContext& Context, const FMovieSceneEvaluationOperand& Operand, FPersistentEvaluationData& PersistentData, IMovieScenePlayer& Player)
	{
		// 登録されたセクションの実行
		for(auto Section : m_SampleSections)
		{
			SampleSectionExecute(*Section, Section, Context, Operand, PersistentData, Player);
		}
	}

	void SampleSectionExecute(const USampleSection& SampleSection, FObjectKey SectionKey,	const FMovieSceneContext& Context, const FMovieSceneEvaluationOperand& Operand, FPersistentEvaluationData& PersistentData, IMovieScenePlayer& Player)
	{
		// 非再生時のステータス
		if ((Context.GetStatus() != EMovieScenePlayerStatus::Playing && Context.GetStatus() != EMovieScenePlayerStatus::Scrubbing)
			|| Context.HasJumped()
			|| Context.GetDirection() == EPlayDirection::Backwards)
		{

		}
		// Master track
		if (!Operand.ObjectBindingID.IsValid())
		{
			// スタートタイム以前
			if (Context.GetTime() < SampleSection.GetInclusiveStartFrame())
			{
			}
			// シーケンスの右端
			else if (Context.GetTime().GetFrame() >= (m_FrameNumberRange.GetUpperBoundValue() - 10))
			{
			}
			// セクション右端
			else if (Context.GetRange().Overlaps(TRange<FFrameTime>(FFrameTime(SampleSection.GetExclusiveEndFrame()))))
			{
			}
			// セクション中
			else if (Context.GetTime() < SampleSection.GetExclusiveEndFrame())
			{
			}
			else
			{
			}
		}
	}
	TArray<USampleSection*> m_SampleSections;
	const TRange<FFrameNumber> m_FrameNumberRange;

};

FSampleTrackImplementation::FSampleTrackImplementation(TArray<USampleSection*> Sections)
	: SampleSections(Sections)
{
}

void FSampleTrackImplementation::Evaluate(const FMovieSceneEvaluationTrack& Track, TArrayView<const FMovieSceneFieldEntry_ChildTemplate> Children, const FMovieSceneEvaluationOperand& Operand, const FMovieSceneContext& Context, const FPersistentEvaluationData& PersistentData, FMovieSceneExecutionTokens& ExecutionTokens) const
{
	const FMovieSceneSequenceInstanceData* data = PersistentData.GetInstanceData();
	ExecutionTokens.SetContext(Context);
	ExecutionTokens.Add(FSampleExecutionToken(SampleSections, Track.GetSourceTrack()->GetTypedOuter<UMovieScene>()->GetPlaybackRange()));
}
Editor

SampleTrackEditor.h

#pragma once

#include "CoreMinimal.h"

#include "Templates/SubclassOf.h"
#include "MovieSceneTrack.h"
#include "MovieSceneTrackEditor.h"

class SWidget;
class ISequencer;
class ISequencerTrackEditor;

class USampleNameableTrack;
class USampleSection;

class FSampleTrackEditor : public FMovieSceneTrackEditor
{
public:
	static TSharedRef<ISequencerTrackEditor> CreateTrackEditor(TSharedRef<ISequencer> InSequencer)
	{
		return MakeShared<FSampleTrackEditor>(InSequencer);
	}

	FSampleTrackEditor(TSharedRef<ISequencer> InSequencer)
		: FMovieSceneTrackEditor(InSequencer)
	{}

	// サポートするトラックだったときのみ true を返す
	virtual bool SupportsType(TSubclassOf<UMovieSceneTrack> Type) const override;
	// サポートする Sequencer だったときのみ true を返す
	virtual bool SupportsSequence(UMovieSceneSequence* InSequence) const override;
	// トラックメニューを構成(+Track ボタンのメニューに追加される)
	virtual void BuildAddTrackMenu(FMenuBuilder& MenuBuilder) override;
	// セクションの生成
	virtual TSharedRef<ISequencerSection> MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override;
	// トラック名横に Widget を追加する
	virtual TSharedPtr<SWidget> BuildOutlinerEditWidget(const FGuid& ObjectBinding, UMovieSceneTrack* Track, const FBuildEditWidgetParams& Params) override;

private:
	// 上記を追加した場合に新しく作成
	void CreateNewSection(USampleNameableTrack* Track, TSubclassOf<USampleSection> ClassType);
	// 新しいマスタートラック作成
	void AddNewMasterTrack();
};

SampleTrackEditor.cpp

#include "SampleTrackEditor.h"

#include "Widgets/SWidget.h"
#include "ISequencer.h"
#include "ISequencerSection.h"
#include "SequencerUtilities.h"

#include "SampleSectionEditor.h"
#include "Track/SampleNameableTrack.h"
#include "Track/SampleSection.h"

#define LOCTEXT_NAMESPACE "SampleTrackEditor"

bool FSampleTrackEditor::SupportsType(TSubclassOf<UMovieSceneTrack> Type) const
{
	return Type == USampleNameableTrack::StaticClass();
}

bool FSampleTrackEditor::SupportsSequence(UMovieSceneSequence* InSequence) const
{
	
	return (InSequence != nullptr) && (InSequence->GetClass()->GetName() == TEXT("LevelSequence"));
}

void FSampleTrackEditor::BuildAddTrackMenu(FMenuBuilder& MenuBuilder)
{
	MenuBuilder.AddMenuEntry(
		FText::FromName("SampleTrack"),
		FText(),
		FSlateIcon(),
		FUIAction(FExecuteAction::CreateSP(this, &FSampleTrackEditor::AddNewMasterTrack))
	);
}

TSharedRef<ISequencerSection> FSampleTrackEditor::MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding)
{
	check(SupportsType(SectionObject.GetOuter()->GetClass()));
	TSharedRef<ISequencerSection> MakeSection = MakeShareable(new FSampleSectionEditor(SectionObject, GetSequencer()));

	TSharedRef<FSampleSectionEditor> SampleSection = StaticCastSharedRef<FSampleSectionEditor>(MakeSection);
	USampleSection* SampleMovieSceneSection= Cast<USampleSection>(&SampleSection->GetSection());

	return MakeSection;
}

TSharedPtr<SWidget> FSampleTrackEditor::BuildOutlinerEditWidget(const FGuid& ObjectBinding, UMovieSceneTrack* Track, const FBuildEditWidgetParams& Params)
{
	// トラックの+ボタンで追加される子トラック
	USampleNameableTrack* CustomTrack = Cast<USampleNameableTrack>(Track);
	if (!CustomTrack)
	{
		return nullptr;
	}

	auto SubMenuCallback = [this, CustomTrack]() -> TSharedRef<SWidget>
	{
		FMenuBuilder MenuBuilder(true, nullptr);
		MenuBuilder.AddMenuEntry(
			FText::FromString(TEXT("NewTrack")),
			FText::FromString(TEXT("")),
			FSlateIcon(),
			FUIAction(
				FExecuteAction::CreateSP(
					this,
					&FSampleTrackEditor::CreateNewSection,
					CustomTrack,
					CustomTrack->DefaultSectionType
				)
			)
		);
		return MenuBuilder.MakeWidget();
	};

	return SNew(SHorizontalBox)
		+ SHorizontalBox::Slot()
		.AutoWidth()
		.VAlign(VAlign_Center)
		[
			FSequencerUtilities::MakeAddButton(LOCTEXT("AddSectionText", "Sample"), FOnGetContent::CreateLambda(SubMenuCallback), Params.NodeIsHovered, GetSequencer())
		];
}

void FSampleTrackEditor::CreateNewSection(USampleNameableTrack* Track, TSubclassOf<USampleSection> ClassType)
{
	TSharedPtr<ISequencer> SequencerPin = GetSequencer();
	UClass* Class = ClassType.Get();

	if (Class && SequencerPin)
	{
		FScopedTransaction Transaction(FText::Format(LOCTEXT("AddCustomSection_Transaction", "Add New Section From Class %s"), FText::FromName(Class->GetFName())));
		USampleSection* NewSection = NewObject<USampleSection>(Track, Class, NAME_None, RF_Transactional);
		
		FQualifiedFrameTime CurrentTime = SequencerPin->GetLocalTime();

		const FFrameNumber Duration = (5.f * CurrentTime.Rate).FrameNumber;
		NewSection->SetRange(TRange<FFrameNumber>(CurrentTime.Time.FrameNumber, CurrentTime.Time.FrameNumber + Duration));
		NewSection->InitialPlacement(Track->GetAllSections(), CurrentTime.Time.FrameNumber, Duration.Value, Track->SupportsMultipleRows());

		Track->AddSection(*NewSection);

		SequencerPin->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemAdded);
	}
}

void FSampleTrackEditor::AddNewMasterTrack()
{
	UMovieScene* MovieScene = GetFocusedMovieScene();
	if (MovieScene == nullptr || MovieScene->IsReadOnly())
	{
		return;
	}

	MovieScene->Modify();

	USampleNameableTrack* NewTrack = MovieScene->AddMasterTrack<USampleNameableTrack>();
	ensure(NewTrack);

	// Editor表示名
	NewTrack->SetDisplayName(LOCTEXT("SampleTrack", "Sample"));

	if (GetSequencer().IsValid())
	{
		GetSequencer()->OnAddTrack(NewTrack, FGuid());
	}
}

#undef LOCTEXT_NAMESPACE

SampleSectionEditor.h

#pragma once

#include "CoreMinimal.h"
#include "Templates/SubclassOf.h"
#include "Templates/SharedPointer.h"
#include "ISequencer.h"
#include "ISequencerSection.h"
#include "ISequencerTrackEditor.h"
#include "Input/Reply.h"
#include "Curves/KeyHandle.h"

class FSequencerSectionPainter;
class UMovieSceneSection;
class IDetailsView;

class FSampleSectionEditor
	: public ISequencerSection
	, public TSharedFromThis<FSampleSectionEditor>
{
public:
	FSampleSectionEditor(UMovieSceneSection& InSection, TWeakPtr<ISequencer> InSequencer);
	~FSampleSectionEditor();
public:
	// ISequencerSection interface Start
	// セクション取得
	virtual UMovieSceneSection* GetSectionObject() override;
	// セクション色設定
	virtual int32 OnPaintSection(FSequencerSectionPainter& Painter) const override;
	// ISequencerSection interface End

	UMovieSceneSection& GetSection();

private:
	UMovieSceneSection& Section;
	TWeakPtr<ISequencer> Sequencer;
};

SampleSectionEditor.cpp

#include "SampleSectionEditor.h"

#include "IDetailsView.h"
#include "SequencerSectionPainter.h"

#define LOCTEXT_NAMESPACE "SampleSectionEditor"

FSampleSectionEditor::FSampleSectionEditor(UMovieSceneSection& InSection, TWeakPtr<ISequencer> InSequencer)
	: Section(InSection)
	, Sequencer(InSequencer)
{
}

FSampleSectionEditor::~FSampleSectionEditor()
{
}

// ISequencerSection interface Start
UMovieSceneSection* FSampleSectionEditor::GetSectionObject()
{
	return &Section;
}
int32 FSampleSectionEditor::OnPaintSection(FSequencerSectionPainter& Painter) const
{
	return Painter.PaintSectionBackground(FLinearColor::Red);
}
// ISequencerSection interface End

UMovieSceneSection& FSampleSectionEditor::GetSection()
{
	return Section; 
}

#undef LOCTEXT_NAMESPACE

5.確認

5-1.トラック追加
  1. トラックで新しいトラックを追加

項目に実装したSmapleTrackを選択
f:id:spark-sato-yosuke:20211109052309p:plain

5-2.子トラック追加

追加したトラックの+ボタンからセクションを追加
f:id:spark-sato-yosuke:20211109054004p:plain

5-3.完成図

f:id:spark-sato-yosuke:20211109055608p:plain

終わりに

トラックは通過しても処理を書いていないので何も起きません。
この記事を読んで頂いた方が自由に実装できる形を目指したので、
このような形になりました。
読んで頂いた方が目指した実装ができるように応援しています。

長文ではありましたが、読んで頂いてありがとうございます。

参考URL

レベルシーケンサー
docs.unrealengine.com

Audioトラック
docs.unrealengine.com

エディターモジュール追加
qiita.com

カスタムトラックUE4.18
negimochi.work