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 部構成とします。


さらにもうひと工夫したい

アクターの特定関数のみを呼び出す専用トラックにする

自分のプロジェクトで、重要な振る舞いをするアクター AMyActor があったとする。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"

UCLASS()
class CUSTOMTRACK_API AMyActor : public AActor
{
	GENERATED_BODY()
	
public:	

	AMyActor();

	UFUNCTION()
	void CallCppFunction(float Time, bool TestBool, FName TestName);
};

このとき、関数 AMyActor::CallCppFunction だけを Sequencer から呼び出す専用トラックを作りたい。

Event Track のセクションは、イベント名と構造体を毎度設定する必要があるため、専用機能とした場合は逆に操作が煩わしくなってしまう。
そのため、キーの編集時には、引数のみ設定するような形にしたい。
こういった場合、Runtime 側のセクションである UMovieSceneSection 継承クラスを改造する必要が出てくる。

また、この目的の場合には、使いたい関数がはっきりしているので、 ProcessEvent を使うまでもない。
検索コストもなくなるので、直接、関数を呼び出すように書き換えたい。
そのような場合には、Runtime 側 FMovieSceneEvalTemplate 継承クラスも改造しなくてはいけない。

Runtime 側の Event Track のソースコードを全てコピーして新規クラス作成

今度のカスタマイズでは、Runtime 側のクラスも新規でクラスを作る。
まずは、FActorEventTrackSection のときと同様に、ソースごとコピーしてクラス名を変える。
また、各クラスが参照しあっている箇所やヘッダーファイル等は、適宜、今回カスタマイズするクラスに Replace しておく。

カスタムトラック編と合わせて、カスタマイズ用のクラスと、元になる Event Track との対応関係を以下にまとめておく。

基底クラス参考にする Event Trackカスタムトラックで今回作成するクラス
UMovieSceneTrackUMovieSceneEventTrackUMovieSceneActorEventTrack
FMovieSceneEvalTemplateFMovieSceneEventSectionTemplateFMovieSceneActorEventTemplate
UMovieSceneSectionUMovieSceneEventSectionUMovieSceneActorEventSection
FMovieSceneTrackEditorFEventTrackEditorFActorEventTrackEditor
ISequencerSectionFEventTrackSectionFActorEventSection

キー編集時メニューに引数を直接設定できるように変更

UMovieSceneActorEventSection を以下のように改良する。

#pragma once

#include "CoreMinimal.h"
#include "MovieSceneSection.h"
#include "Curves/CurveInterface.h"

#include "MyActor.h"

#if WITH_EDITOR
#include "MovieSceneClipboard.h"
#endif

#include "MovieSceneActorEventSection.generated.h"

USTRUCT()
struct FActorEventPayload
{
	GENERATED_BODY()

	FActorEventPayload() {}

	FActorEventPayload(const FActorEventPayload&) = default;
	FActorEventPayload& operator=(const FActorEventPayload&) = default;

	FActorEventPayload(FActorEventPayload&&) = default;
	FActorEventPayload& operator=(FActorEventPayload&&) = default;

	UPROPERTY(EditAnywhere, Category = Event)
	bool BoolValue = false;

	UPROPERTY(EditAnywhere, Category = Event)
	FName NameValue;
};

using FActorEventPaylaodCurve = TCurveInterface<FActorEventPayload, float> ;

#if WITH_EDITOR

namespace MovieSceneClipboard
{
	template<> inline FName GetKeyTypeName<FActorEventPayload>()
	{
		return "ActorEventPayload";
	}
}

#endif


USTRUCT()
struct FMovieSceneActorEventSectionData
{
	GENERATED_BODY()

	FMovieSceneActorEventSectionData() = default;

	FMovieSceneActorEventSectionData(const FMovieSceneActorEventSectionData& RHS)
		: KeyTimes(RHS.KeyTimes)
		, KeyValues(RHS.KeyValues)
	{
	}

	FMovieSceneActorEventSectionData& operator=(const FMovieSceneActorEventSectionData& RHS)
	{
		KeyTimes = RHS.KeyTimes;
		KeyValues = RHS.KeyValues;
#if WITH_EDITORONLY_DATA
		KeyHandles.Reset();
#endif
		return *this;
	}

	/** Sorted array of key times */
	UPROPERTY()
	TArray<float> KeyTimes;

	/** Array of values that correspond to each key time */
	UPROPERTY()
	TArray<FActorEventPayload> KeyValues;

#if WITH_EDITORONLY_DATA
	/** Transient key handles */
	FKeyHandleLookupTable KeyHandles;
#endif
};


UCLASS()
class CUSTOMTRACK_API UMovieSceneActorEventSection
	: public UMovieSceneSection
{
	GENERATED_BODY()

	UMovieSceneActorEventSection();

public:

	const FMovieSceneActorEventSectionData& GetEventData() const { return EventData; }

	FActorEventPaylaodCurve GetCurveInterface() { return CurveInterface.GetValue(); }

public:

	//~ UMovieSceneSection interface

	virtual void DilateSection(float DilationFactor, float Origin, TSet<FKeyHandle>& KeyHandles) override;
	virtual void GetKeyHandles(TSet<FKeyHandle>& KeyHandles, TRange<float> TimeRange) const override;
	virtual void MoveSection(float DeltaPosition, TSet<FKeyHandle>& KeyHandles) override;
	virtual TOptional<float> GetKeyTime(FKeyHandle KeyHandle) const override;
	virtual void SetKeyTime(FKeyHandle KeyHandle, float Time) override;

private:

	UPROPERTY()
	FMovieSceneActorEventSectionData EventData;

	TOptional<FActorEventPaylaodCurve> CurveInterface;
};
  • L15-33: Event Track 版では、FEventPayload だった箇所。
    専用関数の引数向けにプロパティを変更したクラスを定義する。
    ここに定義した UPROPERTY が自動的にキー編集時のプロパティに追加される。
  • L9-11, L39-45 : TCurveInterface を使用する際は、この記述を入れないと必ずビルドエラーになる
  • L79: FMovieSceneActorEventSectionData は、Event Track 版で FMovieSceneEventSectionData だった箇所。
    FActorEventPayload クラスを保持できるようにするだけ。
  • L115-117: 上記で定義し直したクラスを UMovieSceneActorEventSection が保持できるように元のコードから Replace する
#include "MovieSceneActorEventSection.h"

#include "EngineGlobals.h"
#include "LinkerLoad.h"
#include "Curves/KeyFrameAlgorithms.h"

//// UMovieSceneActorEventSection

UMovieSceneActorEventSection::UMovieSceneActorEventSection()
#if WITH_EDITORONLY_DATA
	: CurveInterface(FActorEventPaylaodCurve(&EventData.KeyTimes, &EventData.KeyValues, &EventData.KeyHandles))
#else
	: CurveInterface(FActorEventPaylaodCurve(&EventData.KeyTimes, &EventData.KeyValues))
#endif
{
	SetIsInfinite(true);
}

/***********************************************
 以下のソースコードは元コードと変更点がないので省略 
 ***********************************************/
  • L11-13: cpp 側はここだけ変更したものに置き換える。それ以外のコードはそのまま。

ProcessEvent 使わずに直接関数を呼び出すように書き換え

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "MovieSceneActorEventSection.h"
#include "Evaluation/MovieSceneEvalTemplate.h"
#include "MovieSceneObjectBindingID.h"

#include "MovieSceneActorEventTemplate.generated.h"

class UMovieSceneActorEventTrack;
struct EventData;

USTRUCT()
struct FMovieSceneActorEventTemplate : public FMovieSceneEvalTemplate
{
	GENERATED_BODY()
	
	FMovieSceneActorEventTemplate() {}
	FMovieSceneActorEventTemplate(const UMovieSceneActorEventSection& Section, const UMovieSceneActorEventTrack& Track);

/***********************************************
 以下のソースコードは元コードと変更点がないので省略 
 ***********************************************/
  • L19-20: 使用するクラスを UMovieSceneActorEventSection, UMovieSceneActorEventTrack に変更しておく。

#include "MovieSceneActorEventTemplate.h"

#include "MovieSceneActorEventTrack.h"

#include "MovieSceneSequence.h"
#include "Evaluation/MovieSceneEvaluationTemplateInstance.h"
#include "EngineGlobals.h"
#include "MovieScene.h"
#include "MovieSceneEvaluation.h"
#include "IMovieScenePlayer.h"

#include "MyActor.h"


struct FMovieSceneEventData
{
	FMovieSceneEventData(const FActorEventPayload& InPayload, float InGlobalPosition) : Payload(InPayload), GlobalPosition(InGlobalPosition) {}

	FActorEventPayload Payload;
	float GlobalPosition;
};

/** A movie scene execution token that stores a specific transform, and an operand */
struct FEventTrackExecutionToken
	: IMovieSceneExecutionToken
{
	FEventTrackExecutionToken(TArray<FMovieSceneEventData> InEvents, const TArray<FMovieSceneObjectBindingID>& InEventReceivers) : Events(MoveTemp(InEvents)), EventReceivers(InEventReceivers) {}

	/** Execute this token, operating on all objects referenced by 'Operand' */
	virtual void Execute(const FMovieSceneContext& Context, const FMovieSceneEvaluationOperand& Operand, FPersistentEvaluationData& PersistentData, IMovieScenePlayer& Player) override
	{	
		TArray<float> PerformanceCaptureEventPositions;

		// Resolve event contexts to trigger the event on
		TArray<UObject*> EventContexts;

		// If we have specified event receivers, use those
		if (EventReceivers.Num())
		{
			EventContexts.Reserve(EventReceivers.Num());
			for (FMovieSceneObjectBindingID ID : EventReceivers)
			{
				// Ensure that this ID is resolvable from the root, based on the current local sequence ID
				ID = ID.ResolveLocalToRoot(Operand.SequenceID, Player.GetEvaluationTemplate().GetHierarchy());

				// Lookup the object(s) specified by ID in the player
				for (TWeakObjectPtr<> WeakEventContext : Player.FindBoundObjects(ID.GetGuid(), ID.GetSequenceID()))
				{
					if (UObject* EventContext = WeakEventContext.Get())
					{
						EventContexts.Add(EventContext);
					}
				}
			}
		}
		else
		{
			// If we haven't specified event receivers, use the default set defined on the player
			EventContexts = Player.GetEventContexts();
		}

		for (UObject* EventContextObject : EventContexts)
		{
			auto MyActor = Cast<AMyActor>(EventContextObject);
			if (!MyActor)
			{
				continue;
			}

			for (FMovieSceneEventData& Event : Events)
			{
				MyActor->CallCppFunction(Event.GlobalPosition, Event.Payload.BoolValue, Event.Payload.NameValue);
			}
		}
	}


	TArray<FMovieSceneEventData> Events;
	TArray<FMovieSceneObjectBindingID, TInlineAllocator<2>> EventReceivers;
};

FMovieSceneActorEventTemplate::FMovieSceneActorEventTemplate(const UMovieSceneActorEventSection& Section, const UMovieSceneActorEventTrack& Track)
	: EventData(Section.GetEventData())
	, EventReceivers(Track.EventReceivers)
	, bFireEventsWhenForwards(Track.bFireEventsWhenForwards)
	, bFireEventsWhenBackwards(Track.bFireEventsWhenBackwards)
{
}

/***********************************************
 以下のソースコードは元コードと変更点がないので省略 
 ***********************************************/
  • L17-20: FActorEventPayload が受け取れるようにクラス名を変更する
  • L62-74: ProcessEvent を使用していた箇所をごそっと削除して、UObject をキャスト、AMyActor だったら直接関数をコールするように変更する
  • L82: コンストラクタ引数を変更したので、cpp 側も合わせて変更する

その他の変更ポイント

  • UMovieSceneActorEventTrack
    • CreateNewSection で UMovieSceneActorEventSection を NewObject するように変更
  • FActorEventTrackEditor
    • BuildObjectBindingTrackMenu で AMyActor のときだけメニューが出るように UClass をチェックする。
    • SupportsType を UMovieSceneActorEventTrack にする
  • FActorEventSection
    • GenerateSectionLayout で FEventPayload を自作した FActorEventPayload に Replace

念のため、github にもソースコードをあげてあるので、興味がある方はどうぞ。

GitHub - negimochi/SequencerCustomTrackExample

実行結果

終わりに

アドベントカレンダーに初めて参加しましたが、ちょっと張り切りすぎました。
そのせいか、題材が半端なくニッチすぎたと反省しています。

今回 Sequencer を取り上げたのは、元々、Sequencer のエディタ拡張をする必要に迫られていて調べていた時期があり、最新版ではどうなっているのかまとめておきたかったというのが動機です。

ここ最近の Sequencer は標準でも自由度が高くかつ安定してきており、そもそもエディタ拡張に手を出さなくてもなんとかなるケースは多いと思われます。
(それこそ、プラグイン用の機能拡張とか、C++をメインにした開発をしている状況でないと、まずいじることはないでしょう)

今回参加してみて、他の方のアドベントカレンダーの投稿を見ても、それほど気張らなくてもよかったなぁと感じました。
次回もし参加することがあれば、もう少しわかりやすくサクッと書ける内容にしようかなと思います。