Sequencer 構造解説とカスタムトラック追加 (UE4.18版) – カスタムトラック編

Unreal Engine 4 (UE4) Advent Calendar 2017 - Qiita
今年もやります、UE4アドベントカレンダー!Unreal Engine 4 (UE4)に関する知見をみんなで毎日シェアしていきましょう! 即埋まったので、その2も作っちゃいました! 過去のアドベントカレンダー 2016年:

この記事は Unreal Engine 4 (UE4) Advent Calendar 2017 の 17 日目の記事です。

自分でカスタムしたトラックを Sequencer に追加する方法に関して書きます。基本的に C++er 向けの内容になります。
トラックを追加するといっても、ある程度 Sequencer の内部構造に関して理解がないと作ることが難しいため、
以下のような 3 部構成とします。


本題に入る前に

今回、カスタムトラックを作るにあたっての注意点。

1. 今回の内容は、 UE4.18 をベースとしているので、これ以外のバージョンでの動作保証は一切しません。
2. また、エンジンのソースコードは直接編集することはしません。

Sequencer は、ここ最近のバージョンアップで集中的なテコ入れがあった。
C++ の実装レベルでは、バージョンによって実装の差異が大きく、バージョンを挟んだ保守が難しくなっている。
今回の UE4.18 ベースの内容が、そのまま他のバージョンで使えるとは限らないし、今後も実装内容が変わる可能性は十分あることに注意してほしい。

特に注意すべきなのは、UE4.14 ~ UE4.15。内部の評価計算部分にかなり大きな実装変更が入っており、まず同じコードではビルドは通らない。
また、その後の UE4.16, UE4.17 あたりでは、いくつかの機能追加が入っているため、こちらも注意が必要。

また、今回のようなエディタ拡張は独自の機能を収めたプラグインなどでの実装が想定される。
そのため、基本的に、エンジンソースコードは直接いじらないで済む方法で実装を行う。

エディタモジュールを用意

エディタ拡張をするために、まずは、エディタモジュールを用意しておく必要がある。
ここでは、本題からそれるので詳細については省く。
以前、自分が書いた記事の一部では、そのことが書いてあるので参考までに。

[UE4 エディタ拡張] 詳細パネルで UStruct のプロパティカスタマイズ

なお、今回紹介する例では、

  • Runtime 側:CustomTrack
  • Editor 側:CustomTrackEditor

という名前として進める。

Sequencer のエディタ拡張に必要なモジュールを追記

構造解説編で説明したように、4 つのモジュールが必須となる。
Editor 側では、トラックにボタンを追加する場合などに Slate を使用する可能性が高いので、上の 4 つに加えて Slate, SlateCore も追記しておく。
また、アイコン等を設定する場合は EditorStyle も必要となる。

using UnrealBuildTool;

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

		PrivateDependencyModuleNames.AddRange(new string[] {
            "MovieScene", "MovieSceneTracks"
        });
	}
}
using UnrealBuildTool;

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

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

1 種類のトラック作るために必要なクラス

自作のトラックを追加するには、Runtime / Editor のそれぞれで、少なくとも以下のクラスが必要になる。

Runtime 側 (MovieScene モジュール)

Runtime 時にのみ使用されるクラス。
こちらのクラスは、使い方によっては UE4 標準で用意された継承クラスをそのまま流用することも不可能ではない。
元々あるものをうまく流用することで実装がかなり楽になる。

UMovieSceneTrack
  • Runtime 時でのトラックを担うクラス。
  • セクションの管理や次に説明する FMovieSceneEvalTemplate の生成などを行う。
FMovieSceneEvalTemplate
  • Sequencer のタイムラインが動いたときに、具体的にどのような挙動を行うのか定義されたクラス。
  • UE4.15 以降で追加されたクラス。
    UE4.14 までは、IMovieSceneTrackInstance というクラスがこの部分を担っていた。
    UE4.18 ではまだクラスは残っているようだが非推奨。
  • 初期化フェーズ、動作内容を書き込む ExecutionToken を積むフェーズ、積んである ExecutionToken を実行するフェーズの 3 段階で構成されている。
UMovieSceneSection
  • 各トラック内のセクションに対応するクラス。
  • 各セクションを右クリックしたときのパラメータ値は基本的にはここのプロパティで管理される。

Editor 側 (Sequencer モジュール)

Editor 側で必要なクラス。主に Sequencer での見た目やメニューの構成などを設定する。
カスタムトラックを作る場合は、まずは、自分でこちらの継承クラスを作成することになる。

FMovieSceneTrackEditor
  • エディタ用トラック操作用クラス。
  • トラック横のリストボタンなどの Slate 定義など、エディタ側のカスタマイズがこのクラスを継承することで自由に設定できる。
  • Sequencer モジュールに、これを独自に継承したクラスを事前登録することでトラックの追加が可能になる。
ISequencerSection
  • エディタ用のセクション操作用クラス。
  • セクションカラーやセクション内のエディタ描画などの設定ができる。

上記クラスの実装例を知りたい場合、既に UE4 で実装されているトラックを参照すると良い。
ファイル名・クラス名は、以下のように、だいたいは統一されているので検索に引っかかりやすい。

UMovieSceneXXXXTrack: UMovieSceneTrack 継承クラス
FMovieSceneXXXXTemplate: FMovieSceneEvalTemplate 継承クラス
UMovieSceneXXXXSection: UMovieSceneSection 継承クラス
FXXXXTrackEditor: FMovieSceneTrackEditor 継承クラス
FXXXXSection:  ISequencerSection 継承クラス
( XXXX は「構造解説編 – トラックの種類」で取り上げたトラック名 )

Event Track を元にしてカスタマイズする

カスタムトラックを作る上で、非常に参考になるのが Event Track。


(ちょうどタイムリーに話題に上がったので引用させていただきました)

Event Track は、元々レベルブループリントのイベントトリガー機能として作られたトラックである。
だが、最近の更新で、引数を渡す機能任意のアクターのイベントを呼び出すことにも対応するようになった。
(UE4.16 から使用可能とのこと)

Event Track の内部実装( FMovieSceneEventSectionTemplate )をみてみると、実は、ProcessEvent を使用して関数を呼び出していることがわかる。

ProcessEvent は Event と名前はついているものの、イベントだけでなく、そのオブジェクトが所有するリフレクション対象の C++ 関数も呼び出すことができる。
(C++ 関数の場合、UFUNCTION() さえついていれば BlueprintCallable でなくとも呼び出しが可能!)

したがって、カスタムイベントに限らず、BP で定義された関数だろうが、 C++ で定義された関数だろうが、Event Track のみで呼び出しが可能 ということになる。

これだけ汎用的な機能を Event Track が持っていることもあり、Event Track のクラスを元にして改造をしていくと、特定の機能のみに特化したカスタムトラックを比較的容易に実装できる。
今回は、実際に Event Track をベースにしてカスタムトラックを作ってみる例を以下に示す。

Event Track を Object Binding させる

Event Track は Master Track である。
現状のバージョンでは Object Binding ができるようになっていないため、アクター内イベント呼び出しを使用してみるとかなり使いにくいことがわかる。
(Event Track の名前の上で右クリック → Properties → Event Recievers)

特に、どのアクターに対してイベントを呼び出そうとしているのかに関して一覧性が悪く、直感的にわかりにくい見た目となってしまっているのがユーザビリティを阻害している。
そこで、新たに Event Track の機能を Object Binding した上で使えるように改良してみる。

Objectg Binding させるだけであれば、Runtime 側のクラスはそのまま Event Track のものを流用し、Editor 側のクラスのみ自作することで対応が可能だ。

FActorEventTrackEditor (FMovieSceneTrackEditor 継承クラス) 作成
#pragma once

#include "CoreMinimal.h"

#include "Templates/SubclassOf.h"
#include "Widgets/SWidget.h"
#include "ISequencer.h"
#include "MovieSceneTrack.h"
#include "ISequencerSection.h"
#include "ISequencerTrackEditor.h"
#include "MovieSceneTrackEditor.h"

class FMenuBuilder;

class FActorEventTrackEditor : public FMovieSceneTrackEditor
{
public:

	FActorEventTrackEditor(TSharedRef<ISequencer> InSequencer);
	virtual ~FActorEventTrackEditor() { }

	// 自分自身のインスタンスを SharedRef で返す関数。
	// モジュール開始時に TrackEditor を生成する関数を登録する必要があるため、static で定義
	static TSharedRef<ISequencerTrackEditor> CreateTrackEditor(TSharedRef<ISequencer> OwningSequencer);

public:

	// ISequencerTrackEditor interface

	// セクションの生成
	virtual TSharedRef<ISequencerSection> MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override;
	// サポートする Sequencer だったときのみ true を返す
	virtual bool SupportsSequence(UMovieSceneSequence* InSequence) const override;
	// サポートするトラックだったときのみ true を返す
	virtual bool SupportsType(TSubclassOf<UMovieSceneTrack> Type) const override;
	// アイコンを設定する場合に使う
	virtual const FSlateBrush* GetIconBrush() const override;

	// ObjectBinding 用のトラックメニューに項目追加したい場合に使用
	virtual void BuildObjectBindingTrackMenu(FMenuBuilder& MenuBuilder, const FGuid& ObjectBinding, const UClass* ObjectClass) override;

	// Master Tarck からトラックメニューを項目追加したい場合に使用
	//virtual void BuildAddTrackMenu(FMenuBuilder& MenuBuilder) override;
	// トラック名の上で右クリックした際に表示されるメニューを追加する場合に使用
	//virtual void BuildTrackContextMenu( FMenuBuilder& MenuBuilder, UMovieSceneTrack* Track ) override;
	// トラック名の横に Widget を追加するする場合に使用
	//virtual TSharedPtr<SWidget> BuildOutlinerEditWidget(const FGuid& ObjectBinding, UMovieSceneTrack* Track, const FBuildEditWidgetParams& Params) override;

private:
	void FindOrCreateTrack(FGuid ObjectHandle);
};
#include "ActorEventTrackEditor.h"

#include "Misc/Guid.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "EditorStyleSet.h"
#include "SequencerUtilities.h"

#include "Tracks/MovieSceneEventTrack.h"
#include "ActorEventSection.h"

#include "MovieScene.h"


#define LOCTEXT_NAMESPACE "FActorEventTrackEditor"

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


TSharedRef<ISequencerTrackEditor> FActorEventTrackEditor::CreateTrackEditor(TSharedRef<ISequencer> InSequencer)
{
	return MakeShareable(new FActorEventTrackEditor(InSequencer));
}

bool FActorEventTrackEditor::SupportsSequence(UMovieSceneSequence* InSequence) const
{
	// Level Sequence のみ許可する。
	// 他のコードをみても、こんな感じで GetName で判定している。
	// クラスで比較しないのはおそらくモジュール依存関係を増やさないためだと思われる。
	return (InSequence != nullptr) && (InSequence->GetClass()->GetName() == TEXT("LevelSequence"));
}

bool FActorEventTrackEditor::SupportsType(TSubclassOf<UMovieSceneTrack> Type) const
{
	// UMovieSceneEventTrack に対して許可
	return Type == UMovieSceneEventTrack::StaticClass();
}

TSharedRef<ISequencerSection> FActorEventTrackEditor::MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding)
{
	// Editor 用 Section の作成 Event Track の ISequencerSection は直接使えないので自作する。
	// 中身は FEventTrackSection のコピーで構わない
	return MakeShareable(new FActorEventSection(SectionObject, GetSequencer()));
}

const FSlateBrush* FActorEventTrackEditor::GetIconBrush() const
{
	// イベントトラックと同じアイコンにしておく
	return FEditorStyle::GetBrush("Sequencer.Tracks.Event");
}

void FActorEventTrackEditor::BuildObjectBindingTrackMenu(FMenuBuilder& MenuBuilder, const FGuid& ObjectBinding, const UClass* ObjectClass)
{
	// 念のためアクター限定としておく
	if (ObjectClass->IsChildOf(AActor::StaticClass()))
	{
		MenuBuilder.AddMenuEntry(
			NSLOCTEXT("Sequencer", "AddActorEvent", "Actor Event"),
			NSLOCTEXT("Sequencer", "AddActorEventTooltip", "アクター用 Event Track を追加する"),
			FSlateIcon(),
			FUIAction(
				FExecuteAction::CreateSP(this, &FActorEventTrackEditor::FindOrCreateTrack, ObjectBinding)
			)
		);
	}
}

void FActorEventTrackEditor::FindOrCreateTrack(FGuid ObjectHandle)
{
	// トラックがなかったら作成する
	FFindOrCreateTrackResult TrackResult = FindOrCreateTrackForObject(ObjectHandle, UMovieSceneEventTrack::StaticClass(), TEXT("Actor Event"));
	auto* NewTrack = CastChecked<UMovieSceneEventTrack>(TrackResult.Track, ECastCheckedType::NullAllowed);

	if (TrackResult.bWasCreated)
	{
		// トラック作成成功

		// トラック側に BindingID をセット
		FMovieSceneObjectBindingID BindingID(ObjectHandle, GetSequencer()->GetFocusedTemplateID());
		NewTrack->EventReceivers.Add(BindingID);

		// セクション作成
		UMovieSceneSection* NewSection = NewTrack->CreateNewSection();
		check(NewSection);

		// セクション追加
		NewTrack->AddSection(*NewSection);
		NewTrack->SetDisplayName(LOCTEXT("TrackName", "Actor Events"));
	}

	// 構成に変更が起きたことを Sequencer に伝える(トラックが再描画される)
	GetSequencer()->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemAdded);
}


#undef LOCTEXT_NAMESPACE

FActorEventTrackEditor::BuildObjectBindingTrackMenu を override すると、Object Binding 用のトラックメニューに項目を追加できる。
MenuBuilder.AddMenuEntry でメニューを登録。ここでは、選択した際に FActorEventTrackEditor::FindOrCreateTrack 関数がコールされるようにしてある。

そして、FActorEventTrackEditor::FindOrCreateTrack で Object Binding の実装をしている。
FMovieSceneTrackEditor::FindOrCreateTrackForObject は、Object Binding するトラックを検索、なければ新規作成してくれる。

FMovieSceneTrackEditor::FindOrCreateTrackForObject

トラックが新規追加されたら、EventReceivers への登録と、同時にセクションも新規作成する。
このあたりは、使用するトラック(今回の場合は UMovieSceneEventTrack, UMovieSceneEventSection)の実装をよく見て確認しながら実装する。

最後に、ISquencer::NotifyMovieSceneDataChanged で Sequencer 側に変更を通知する。
どこに対して変更が起きたのかを EMovieSceneDataChangeType で適切なタイプを選択して呼び出す。
ここでは、トラックの追加なので EMovieSceneDataChangeType::MovieSceneStructureItemAdded を引数にする。

EMovieSceneDataChangeType
Defines different types of movie scene data changes.
なお、Master Track を追加する場合は、FMovieSceneTrackEditor::FindOrCreateMasterTrack で簡単にトラック追加ができる。

FMovieSceneTrackEditor::FindOrCreateMasterTrack
Find or add a master track of the specified type in the focused movie scene. The track results.
FEventTrackSection をコピーして FActorEventSection にクラス名変更

ISequencerSection を継承した UE4 標準のクラスは、モジュールで Private 設定になっていたり、 cpp 側に隠蔽して書かれてる。
そのため、外のモジュールから直接参照することができない。

そこで、Event Track 用の FEventTrackSection のソースコードをまるっとコピーして名前だけ FActorEventSection に変更したもので定義する。
ソースコードの中身は一緒なのでコードの詳細は省略する。

カスタムトラックの登録

上記のようにして作成したカスタムトラックをエディタ上に反映させるには、Sequencer モジュールに対して FMovieSceneTrackEditor 継承クラスを登録する必要がある。
具体的には、以下のように、Editor モジュールの StartupModule 時に登録、ShutdownModule 時に破棄するように実装をおこなう。

#pragma once

#include "CoreMinimal.h"

#include "UnrealEd.h"

class FCustomTrackEditor: public IModuleInterface
{
public:

	void StartupModule() override;
	void ShutdownModule() override;

private:

	FDelegateHandle ActorEventTrackEditorHandle;
};
#include "CustomTrackEditor.h"
#include "Modules/ModuleManager.h"

#include "ISequencerModule.h"

#include "ActorEventTrackEditor.h"

void FCustomTrackEditor::StartupModule()
{
	ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked<ISequencerModule>("Sequencer");
	ActorEventTrackEditorHandle = SequencerModule.RegisterTrackEditor(
		FOnCreateTrackEditor::CreateStatic(&FActorEventTrackEditor::CreateTrackEditor));

}

void FCustomTrackEditor::ShutdownModule()
{
	if (!FModuleManager::Get().IsModuleLoaded("Sequencer"))
	{
		return;
	}

	ISequencerModule& SequencerModule = FModuleManager::Get().GetModuleChecked<ISequencerModule>("Sequencer");
	SequencerModule.UnRegisterTrackEditor(ActorEventTrackEditorHandle);
}

IMPLEMENT_PRIMARY_GAME_MODULE(FCustomTrackEditor, CustomTrackEditor, "CustomTrackEditor" );

上記のように、ISequencerModule::RegisterTrackEditor で登録、ISequencerModule::UnRegisterTrackEditor で登録解除する。

実行結果

アクターのトラック側にある「+Track」ボタンを押すと「Actor Event」が追加されている。
ポップアップする文字列も設定したものが出ていることが確認できる。

「Actor Event」を選択すると、通常の Event Track と同様の形のトラックが Object Binding される形で追加される。

もちろん、通常の Event Track と同じく、キーフレームごとにイベント名と引数の指定が可能。
どのアクターに対してイベントが設定されているか、とても把握しやすくなった。


基本的なカスタマイズの方法こんな感じ。
さらに応用編に続きます ↓

Sequencer 構造解説とカスタムトラック追加 (UE4.18版) - 応用編
この記事は Unreal Engine 4 (UE4) Advent Calendar 2017 の 17 日目の記事です。 自分でカ...