この記事は Unreal Engine 4 (UE4) Advent Calendar 2017 の 17 日目の記事です。
自分でカスタムしたトラックを Sequencer に追加する方法に関して書きます。基本的に C++er 向けの内容になります。
トラックを追加するといっても、ある程度 Sequencer の内部構造に関して理解がないと作ることが難しいため、
以下のような 3 部構成とします。
- Sequencer 構造解説とカスタムトラック追加 (UE4.18版) – 構造解説編
- Sequencer 構造解説とカスタムトラック追加 (UE4.18版) – カスタムトラック編
- Sequencer 構造解説とカスタムトラック追加 (UE4.18版) – 応用編 [この記事]
さらにもうひと工夫したい
アクターの特定関数のみを呼び出す専用トラックにする
自分のプロジェクトで、重要な振る舞いをするアクター AMyActor
があったとする。
// MyActor.h
#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 | カスタムトラックで今回作成するクラス |
---|---|---|
UMovieSceneTrack | UMovieSceneEventTrack | UMovieSceneActorEventTrack |
FMovieSceneEvalTemplate | FMovieSceneEventSectionTemplate | FMovieSceneActorEventTemplate |
UMovieSceneSection | UMovieSceneEventSection | UMovieSceneActorEventSection |
FMovieSceneTrackEditor | FEventTrackEditor | FActorEventTrackEditor |
ISequencerSection | FEventTrackSection | FActorEventSection |
キー編集時メニューに引数を直接設定できるように変更
UMovieSceneActorEventSection
を以下のように改良する。
// MovieSceneActorEventSection.cpp
#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 する
// MovieSceneActorEventSection.cpp
#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 使わずに直接関数を呼び出すように書き換え
// MovieSceneActorEventTemplate.h #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
に変更しておく。
// MovieSceneActorEventTemplate.cpp
#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 にもソースコードをあげてあるので、興味がある方はどうぞ。
実行結果
終わりに
アドベントカレンダーに初めて参加しましたが、ちょっと張り切りすぎました。
そのせいか、題材が半端なくニッチすぎたと反省しています。
今回 Sequencer を取り上げたのは、元々、Sequencer のエディタ拡張をする必要に迫られていて調べていた時期があり、最新版ではどうなっているのかまとめておきたかったというのが動機です。
ここ最近の Sequencer は標準でも自由度が高くかつ安定してきており、そもそもエディタ拡張に手を出さなくてもなんとかなるケースは多いと思われます。
(それこそ、プラグイン用の機能拡張とか、C++をメインにした開発をしている状況でないと、まずいじることはないでしょう)
今回参加してみて、他の方のアドベントカレンダーの投稿を見ても、それほど気張らなくてもよかったなぁと感じました。
次回もし参加することがあれば、もう少しわかりやすくサクッと書ける内容にしようかなと思います。
コメント