SPARKCREATIVE Tech Blog

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

【UE5】UEC++ Sequencerの独自チャンネルをエンジン改良せずに追加する方法

こんにちは!!
エンジニアの竹野です!
今回はUE5のシーケンサーのチャンネルをエンジン改良せずに追加する方法を説明したいと思います。

作業環境

windows 10
visual studio 2019
・UnrealEngine 5.2.0

プロジェクト設定

・名前     ChannelTest
・テンプレート サードパーソン
・BPかC++   C++

前置き

今回C++で作成していますが、独自のトラックやセクション、モジュール追加などの説明はしません。
トラック、セクション、モジュール追加に関しましては以下URLを参考に作成してください。

↓ 参考サイト
tech.spark-creative.co.jp
negimochi.work

ちなみに自分の環境ではランタイム側は、トラック、セクション、再生処理、
エディター側はトラックエディター、セクションエディターの処理が終わっていて、カスタムトラックの追加が完了しています。
ファイル構成は以下写真になります。

実装内容について

チャンネルとはセクションに配置したキーフレームごとに、チャンネル情報を設定できるものです。
上記参考サイト(一個目)では、位置、回転、拡大の情報を設定できるように
floatチャンネル情報を3つずつを追加していました。

そしてチャンネルには複数の型が使用でき、FName型やアタッチ情報を設定できる構造体もあります。
下記サイトのUE公式にチャンネル構造体の一覧があります。
docs.unrealengine.com


ですが独自のチャンネル構造体を作成し、登録しようとするとエンジンエラーが出てしまい、登録できません!
原因は登録の際にチャンネル構造体の名前をエンジン内で確認しているのですが、既存のチャンネル構造体以外を
登録しようとすると名前が見つからない!という結果が返ってしまうためです...

エンジン改良しないと無理...?と思われるのですが、エンジン改良せずに解決する方法があります!
そちらを説明したいと思います。

独自チャンネル構造体作成、設定

まずはランタイムのセクション処理を行っているヘッダに以下コードを追加します。
内容は独自チャンネル構造体の作成です。

ChannelTestSection.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "MovieSceneSection.h"
#include "Channels/MovieSceneFloatChannel.h"
#include "Channels/MovieSceneChannel.h"
#include "ChannelTestSection.generated.h"

// 独自キー
USTRUCT()
struct FChannelTestKey
{
	GENERATED_BODY()

		FChannelTestKey()
	{}


	friend bool operator==(const FChannelTestKey& A, const FChannelTestKey& B)
	{
		return A.TestName == B.TestName && A.TestInt == B.TestInt && A.TestFloat == B.TestFloat;
	}

	friend bool operator!=(const FChannelTestKey& A, const FChannelTestKey& B)
	{
		return A.TestName != B.TestName && A.TestInt != B.TestInt && A.TestFloat != B.TestFloat;
	}

	// 追加したいチャンネルの要素
	UPROPERTY(EditAnywhere, Category = "Key")
		FName TestName;

	UPROPERTY(EditAnywhere, Category = "Key")
		int TestInt;

	UPROPERTY(EditAnywhere, Category = "Key")
		float TestFloat;
};

// 独自チャンネル
USTRUCT()
struct CHANNELTEST_API FChannelTest : public FMovieSceneChannel
{
	GENERATED_BODY()

		FChannelTest()
		: testValue()
	{
	}

	// このチャネルのデータのミュータブルインターフェースにアクセスします。
	FORCEINLINE TMovieSceneChannelData<FChannelTestKey> GetData()
	{
		return TMovieSceneChannelData<FChannelTestKey>(&testKeyTimes, &testKeyValues, &testKeyHandles);
	}

	// このチャンネルのデータの定数インターフェースにアクセスします。
	FORCEINLINE TMovieSceneChannelData<const FChannelTestKey> GetData() const
	{
		return TMovieSceneChannelData<const FChannelTestKey>(&testKeyTimes, &testKeyValues);
	}

	// このチャンネルを登録します。
	bool Evaluate(FFrameTime InTime, FChannelTestKey& OutValue) const;

public:

	// ~ FMovieSceneChannel Interface
	virtual void GetKeys(const TRange<FFrameNumber>& WithinRange, TArray<FFrameNumber>* OutKeyTimes, TArray<FKeyHandle>* OutKeyHandles) override;
	virtual void GetKeyTimes(TArrayView<const FKeyHandle> InHandles, TArrayView<FFrameNumber> OutKeyTimes) override;
	virtual void SetKeyTimes(TArrayView<const FKeyHandle> InHandles, TArrayView<const FFrameNumber> InKeyTimes) override;
	virtual void DuplicateKeys(TArrayView<const FKeyHandle> InHandles, TArrayView<FKeyHandle> OutNewHandles) override;
	virtual void DeleteKeys(TArrayView<const FKeyHandle> InHandles) override;
	virtual void DeleteKeysFrom(FFrameNumber InTime, bool bDeleteKeysBefore) override;
	virtual void ChangeFrameResolution(FFrameRate SourceRate, FFrameRate DestinationRate) override;
	virtual TRange<FFrameNumber> ComputeEffectiveRange() const override;
	virtual int32 GetNumKeys() const override;
	virtual void Reset() override;
	virtual void Offset(FFrameNumber DeltaPosition) override;
	virtual void ClearDefault() override;

public:

	// キーが存在しない場合に使用される、このチャンネルのデフォルト値を設定します。
	FORCEINLINE void SetDefault(FChannelTestKey InDefaultValue)
	{
		testValue = InDefaultValue;
	}

	// キーが存在しない場合に使用される、このチャネルのデフォルト値を取得します。
	FORCEINLINE FChannelTestKey GetDefault() const
	{
		return testValue;
	}

	// 設定されているキー情報すべてを取得します。
	TArray<FChannelTestKey> GetKeyArray()
	{
		return testKeyValues;
	}

	// 指定した要素のキーフレームを取得します。
	FFrameNumber GetBinderKeyTime(int Element)
	{
		return testKeyTimes[Element];
	}

	/**
	 * 配列の末尾に追加することで、レガシーデータをアップグレードすることができます。ソートされたデータを想定しています
	 */
	void UpgradeLegacyTime(UObject* Context, double Time, FChannelTestKey Value)
	{
		FFrameRate LegacyFrameRate = GetLegacyConversionFrameRate();
		FFrameNumber KeyTime = UpgradeLegacyMovieSceneTime(nullptr, LegacyFrameRate, Time);

		check(testKeyTimes.Num() == 0 || KeyTime >= testKeyTimes.Last());

		testKeyTimes.Add(KeyTime);
		testKeyValues.Add(Value);
	}

private:

	/** キータイムの並び替え配列 */
	UPROPERTY(meta = (KeyTimes))
		TArray<FFrameNumber> testKeyTimes;

	/** キーがない場合に使用されるデフォルト値 */
	UPROPERTY()
		FChannelTestKey testValue;

	/** 各キータイムに対応する値の配列 */
	UPROPERTY(meta = (KeyValues))
		TArray<FChannelTestKey> testKeyValues;

	/** キーハンドル*/
	FMovieSceneKeyHandleMap testKeyHandles;
};


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

public:
	//----------既存チャンネル----------------------
	// 位置チャンネル
	UPROPERTY()
		FMovieSceneFloatChannel TranslationCurve[3];

	// 回転チャンネル
	UPROPERTY()
		FMovieSceneFloatChannel RotationCurve[3];

	// 拡大チャンネル
	UPROPERTY()
		FMovieSceneFloatChannel ScaleCurve[3];
	//----------------------------------------------


	//----------独自チャンネル----------------------
	UPROPERTY()
		FChannelTest ChannelTest;
	//----------------------------------------------

};


次にセクション処理のCpp側で使用したいチャンネルの設定や、独自チャンネル作成にあたっての必要処理を記述しています。

ChannelTestSection.cpp

#include "Sequencer/ChannelTestSection.h"
#include "Channels/MovieSceneChannelProxy.h"
#include "Sequencer/ChannelTestTrack.h"

#define LOCTEXT_NAMESPACE "UChannelTestSection"

#if WITH_EDITOR
struct FChannelTestEditorData
{
	FChannelTestEditorData()
	{
		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;
		//---------------------------------------------------------------------------------------

		//-----独自チャンネル--------------------------------------------------------------------
		MetaData[9].SetIdentifiers("ChannelTest", LOCTEXT("ChannelTestText", "ChannelTest"));
		MetaData[9].bCanCollapseToTrack = 0;
		MetaData[9].SortOrder = 9;
		//---------------------------------------------------------------------------------------

	}
	FMovieSceneChannelMetaData MetaData[10];
};
#endif // WITH_EDITOR

UChannelTestSection::UChannelTestSection(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	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 UChannelTestSection::CacheChannelProxy()
{
	// Set up the channel proxy
	FMovieSceneChannelProxyData Channels;
	UChannelTestTrack* SampleMovieSceneTrack = Cast<UChannelTestTrack>(GetOuter());

#if WITH_EDITOR
	FChannelTestEditorData 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>());
	//---------------------------------------------------------------------------------------


	//-----独自チャンネル--------------------------------------------------------------------

	//作成した独自チャンネル構造体を設定
	Channels.Add(ChannelTest, EditorData.MetaData[9]);
	//---------------------------------------------------------------------------------------

#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


//-------独自チャンネル必要情報---------------------------------------------------------------------

/*チャンネルのキーについての設定を行っています*/

bool FChannelTest::Evaluate(FFrameTime InTime, FChannelTestKey& OutValue) const
{
	if (testKeyTimes.Num())
	{
		const int32 Index = FMath::Max(0, Algo::UpperBound(testKeyTimes, InTime.FrameNumber) - 1);
		OutValue = testKeyValues[Index];
		return true;
	}

	OutValue = testValue;
	return true;
}

void FChannelTest::GetKeys(const TRange<FFrameNumber>& WithinRange, TArray<FFrameNumber>* OutKeyTimes, TArray<FKeyHandle>* OutKeyHandles)
{
	GetData().GetKeys(WithinRange, OutKeyTimes, OutKeyHandles);
}

void FChannelTest::GetKeyTimes(TArrayView<const FKeyHandle> InHandles, TArrayView<FFrameNumber> OutKeyTimes)
{
	GetData().GetKeyTimes(InHandles, OutKeyTimes);
}

void FChannelTest::SetKeyTimes(TArrayView<const FKeyHandle> InHandles, TArrayView<const FFrameNumber> InKeyTimes)
{
	GetData().SetKeyTimes(InHandles, InKeyTimes);
}

void FChannelTest::DuplicateKeys(TArrayView<const FKeyHandle> InHandles, TArrayView<FKeyHandle> OutNewHandles)
{
	GetData().DuplicateKeys(InHandles, OutNewHandles);
}

void FChannelTest::DeleteKeys(TArrayView<const FKeyHandle> InHandles)
{
	GetData().DeleteKeys(InHandles);
}

void FChannelTest::DeleteKeysFrom(FFrameNumber InTime, bool bDeleteKeysBefore)
{
	// Insert a key at the current time to maintain evaluation
	if (GetData().GetTimes().Num() > 0)
	{
		FChannelTestKey Value;
		Evaluate(InTime, Value);
		GetData().UpdateOrAddKey(InTime, Value);
	}

	GetData().DeleteKeysFrom(InTime, bDeleteKeysBefore);
}

void FChannelTest::ChangeFrameResolution(FFrameRate SourceRate, FFrameRate DestinationRate)
{
	GetData().ChangeFrameResolution(SourceRate, DestinationRate);
}

TRange<FFrameNumber> FChannelTest::ComputeEffectiveRange() const
{
	return GetData().GetTotalRange();
}

int32 FChannelTest::GetNumKeys() const
{
	return testKeyTimes.Num();
}

void FChannelTest::Reset()
{
	testKeyTimes.Reset();
	testKeyValues.Reset();
	testKeyHandles.Reset();
	testValue = FChannelTestKey();
}

void FChannelTest::Offset(FFrameNumber DeltaPosition)
{
	GetData().Offset(DeltaPosition);
}

void FChannelTest::ClearDefault()
{
	testValue = FChannelTestKey();
}
//--------------------------------------------------------------------------------------------------


よし!登録したやろ!シーケンサーをいじってみよう!とやってみるのですが、
シーケンサーを開くとエラーが出て落ちてしまいます…

原因は作成した独自チャンネル構造体をチャンネルとして登録していないから!ということです。

独自チャンネル登録、エラー対処法

なのでエンジン内でどうやって登録してるんだろう?と探ってみたら...

SequencerModule.RegisterChannelInterface<チャンネル名>();

という処理をエディターモジュール内で行っていて、こちらの関数で作成したチャンネルを登録してるんだ!
ということが分かりました。
なのでエディター側のモジュール処理に以下を記述します。

ChannelTestEd.cpp

#include "ChannelTestEd.h"
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "ISequencerChannelInterface.h"
#include "SequencerChannelInterface.h"
#include "ISequencerModule.h"
#include "Sequencer/ChannelTestSection.h"
#include "Sequencer/ChannelTestTrack.h"
#include "ChannelTestTrackEd.h"

class FChannelTestEd : public IChannelTestEd
{
	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;

public:
	FDelegateHandle ChannelTestTrackEd;

};

IMPLEMENT_MODULE(FChannelTestEd, ChannelTestEd);


void FChannelTestEd::StartupModule()
{
	// Sequencerトラック処理登録
	ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked<ISequencerModule>("Sequencer");
	ChannelTestTrackEd = SequencerModule.RegisterTrackEditor(
	FOnCreateTrackEditor::CreateStatic(&FChannelTestTrackEd::CreateTrackEditor));

	// 作成したチャンネル構造体を登録
	SequencerModule.RegisterChannelInterface<FChannelTest>();
}

void FChannelTestEd::ShutdownModule()
{	
	ISequencerModule& SequencerModule = FModuleManager::Get().GetModuleChecked<ISequencerModule>("Sequencer");
	SequencerModule.UnRegisterTrackEditor(ChannelTestTrackEd);
}

さぁーすがに登録できたでしょう!と思いビルドをすると...

エラーが出てしまいました…

なんでやねん!ということで処理を見ていったら
登録の際にチャンネル構造体の名前をエンジン内で確認しているのですが、既存のチャンネル構造体以外を登録しようとすると
名前が見つからない!という結果が返っていました。
なので解決するために以下のコードをエディターモジュールのヘッダ側に記述します!

ChannelTestEd.h

#pragma once

#include "MovieSceneClipboard.h"
#include "Sequencer/ChannelTestSection.h"

// こちらを追加したらチャンネル構造体登録でエラーを解消できます。
namespace MovieSceneClipboard
{
					  // ↓独自チャンネルのキー構造体
	template<> inline FName GetKeyTypeName<FChannelTestKey>()
	{
			// ↓セクションで作成した独自チャンネルの変数名
		return "ChannelTest";
	}
}

class IChannelTestEd : public IModuleInterface
{
public:
	static inline IChannelTestEd& Get()
	{
		return FModuleManager::LoadModuleChecked< IChannelTestEd >("ChannelTestEd");
	}

	static inline bool IsAvailable()
	{
		return FModuleManager::Get().IsModuleLoaded("ChannelTestEd");
	}
};

ビルドしてシーケンサーを開くと、

作成した独自チャンネルが追加されていて、キーを配置してキープロパティを開くとキー構造体に設定した情報を
いじることができるようになっています!

最後に

今回のような独自チャンネルの作成はキーフレームごとに設定、複数配置可能なので
シーケンサーの作業するにあたって、とても便利なため是非やってみてください!
ありがとうございました!!