SPARKCREATIVE Tech Blog

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

【UE5】プラグインの開発方法について(前編:開発編) ~プラグインを作成してみよう~

こんにちは!そしてお久しぶりです!
エンジニアの桒村です。
前回書かせていただいたブログから気づけば実に1年半ほどの年月が経っていました。時間が過ぎていくのは早いものですね!
さてさて前置きはこれぐらいにして、今回は前編と後編に分けて、UnrealEngine5におけるプラグインの開発方法について、作成手順から公開までの一連の流れをお話しさせていただきます。

まず初めに...

プラグインとは?...と思われる方がいらっしゃるかもしれないので、軽く説明させていただくと、
ずばり!!!「エンジンの機能を拡張・追加するモジュール」のことを指します。
こちらの画像のようにUnrealEngineにはあらかじめ標準で数多くのプラグインが用意されています。


そして...なんと弊社では「SPCRJointDynamics」と呼ばれる、キャラクターのスカートなどの揺れものの動きをクロスシミュレーションという形で再現することができるプラグインが無料で配布されています!

詳しくは、以下のGitHubよりリポジトリデータを確認することができますのでぜひ覗いてみてください!
可愛い女の子のスカートをひらひらしたい!髪の毛を揺らしたい!などの要求にこたえれるかもしれませんよ(^^♪
github.com

作業環境

windows 10
・UnrealEngine 5.6.0
・VisualStudio 2022

プロジェクト設定

まずは、UnrealEngine5.6.0を起動して、プロジェクトを作成してください。

プロジェクト名は適当で大丈夫ですが、今回はプラグイン開発をメインとするプロジェクトと定義させていただくため、「Projects_DevPlugin」にしました!
今回もコードを参照するということで、BP(ブループリント)ではなく、C++を選択していただけたらと思います。

プラグインの新規作成手順について

次に、プラグインを新規で作成する手順になります。
エディタから編集 < プラグインをクリックして、プラグインのタブを開き、左上に用意されている『+追加』のボタンをクリックします。

すると、新しいプラグインというタブが開きますので、各テンプレートの中から、新規で作成したいプラグインのタイプに合わせて選択します。

今回は「アニメーションシーケンス内のNotify(通知)にフィルターをつけて効率的に管理・確認できるエディタ拡張」といったものを追加するので、テンプレートは、「①エディタ スタンドアロン ウィンドウ」を選択します。

次に、②プラグイン名「NotifyFilterTool」を入力し、「プラグインを作成」のボタンをクリックします。
③の「記述子データ」については、必ずしも設定する必要があるわけではないですが、今回は公開フローまでをお話しするため、一部だけ設定しています。

せっかくなので、記述子データの各項目についても軽く記載しておきます。

Author プラグインの開発者
Description プラグインの説明文
Author URL 開発者の紹介URL
Is Beta Version ベータ版かどうか


プラグインの作成ができたら、プロジェクトのディレクトリ階層に新規でPluginsフォルダが作成され、その中に先ほど作成したプラグインのフォルダが入っている状態になります。
(Pluginsフォルダが元々存在している場合は、そちらのフォルダ内に新規で作成したプラグインのフォルダが入っている状態になります)

ここまで確認できたらプラグインの新規作成は完了となります。

プラグインの機能実装

それでは、アニメーションシーケンス内のNotify(通知)にフィルター機能を実装していきます。

プラグインを新規で作成しただけで何も変更を加えていない状態ですと、レベルエディタ上からウィンドウタブの中に新しく「NotifyFilterTool」という項目が追加されています。

こちらの項目をクリックしてみると、真っ黒のウィンドウが表示されているのみになります。
これからこちらのウィンドウにフィルター機能を実装していきます。


今回作成するプラグインの使用用途としては、アニメーションエディタ側となるため、レベルエディタ上ではなく、アニメーショエディタ上のウィンドウタブから「NotifyFilterTool」という項目が表示されるように修正をしていきます。
NotifyFilterTool.cppを開き、RegisterMenus関数内を以下のコードのように修正していきます。

void FNotifyFilterToolModule::RegisterMenus()
{
	// Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
	FToolMenuOwnerScoped OwnerScoped(this);

	// アニメーションエディタのメインメニュー「Window」に拡張
	UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("AssetEditor.AnimationEditor.MainMenu.Window");
	if (!Menu)
	{
		return;
	}

	// 大項目として「FilterTool」を追加
	const FName SectionName = "NotifyToolSection";

	// すでに存在するか確認してからセクションを追加(重複回避)
	if (!Menu->FindSection(SectionName))
	{
		Menu->AddSection(SectionName, FText::FromString("FilterTool"));
	}

	// コマンド追加(NotifyFilterTool)
	FToolMenuSection& Section = Menu->FindOrAddSection(SectionName);
	Section.AddMenuEntryWithCommandList(FNotifyFilterToolCommands::Get().OpenPluginWindow, PluginCommands);
}


表示結果はこちらのようになっています。
アニメーションエディター上のウィンドウタブから「NotifyFilterTool」が開けるようになっており、更に視認性を上げるために大項目として「FilterTool」という項目を追加しています。


続いて、「NotifyFilterTool」の中身を実装していきますが、まずは先ほどの真っ黒のウィンドウを今回の仕様に合わせて、こちらの見た目になるようにUI部分を実装していきます。


先ほど開いたNotifyFilterToolのOnSpawnPluginTab関数内に、こちらのUIをそれぞれ追加していきます。
AnimNotifyやAnimNotifyStateのリスト表示とチェックボックス
フィルターを反映させるためのボタン
フィルターをクリアにするボタン

TSharedRef<SDockTab> FNotifyFilterToolModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
	FText WidgetText = FText::Format(
		LOCTEXT("WindowWidgetText", "Add code to {0} in {1} to override this window's contents"),
		FText::FromString(TEXT("FNotifyFilterToolModule::OnSpawnPluginTab")),
		FText::FromString(TEXT("NotifyFilterTool.cpp"))
	);

	TArray<FString> NotifyClasses;
	TArray<FString> NotifyStateClasses;

	GetAllAnimNotifyClasses(NotifyClasses, NotifyStateClasses);

	// アニメーションシーケンスに実際に存在するNotifyクラス名セットを作る
	TSet<FString> UsedNotifyClasses;
	TSet<FString> UsedNotifyStateClasses;

	CurrentAnimSequence = GetOpenedAnimSequence();

	if (CurrentAnimSequence)
	{
		GetUsedNotifyClassesFromAnimSequence(CurrentAnimSequence, UsedNotifyClasses, UsedNotifyStateClasses);
	}

	return SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
		[
			SNew(SScrollBox)
				+ SScrollBox::Slot()
				[
					SNew(SVerticalBox)

					// AnimNotify セクション
					+ SVerticalBox::Slot()
						.AutoHeight().Padding(10)
						[
							SNew(STextBlock)
								.Text(FText::FromString(TEXT("AnimNotify")))
								.Font(FCoreStyle::Get().GetFontStyle("HeadingMedium"))
						]

						+ SVerticalBox::Slot().AutoHeight().Padding(10)
						[
							GenerateCheckboxList(NotifyClasses, NotifyCheckBoxes, UsedNotifyClasses)
						]

					// AnimNotifyState セクション
					+ SVerticalBox::Slot()
						.AutoHeight().Padding(10)
						[
							SNew(STextBlock)
								.Text(FText::FromString(TEXT("AnimNotifyState")))
								.Font(FCoreStyle::Get().GetFontStyle("HeadingMedium"))
						]

						+ SVerticalBox::Slot().AutoHeight().Padding(10)
						[
							GenerateCheckboxList(NotifyStateClasses, NotifyStateCheckBoxes, UsedNotifyStateClasses)
						]

					// ボタン群 横並び
					+ SVerticalBox::Slot()
						.AutoHeight().Padding(10)
						[
							SNew(SHorizontalBox)

								+ SHorizontalBox::Slot()
								.AutoWidth().Padding(0, 0, 10, 0) // 右に余白
								[
									SNew(SButton)
										.Text(FText::FromString(TEXT("すべてのフィルターをクリア")))
										.OnClicked_Lambda([this]()
											{
												for (const FCheckBoxWithLabel& Item : NotifyCheckBoxes)
												{
													if (Item.CheckBox.IsValid())
													{
														Item.CheckBox->SetIsChecked(ECheckBoxState::Unchecked);
													}
												}

												for (const FCheckBoxWithLabel& Item : NotifyStateCheckBoxes)
												{
													if (Item.CheckBox.IsValid())
													{
														Item.CheckBox->SetIsChecked(ECheckBoxState::Unchecked);
													}
												}

												ClearNotifyVisibilityFilter();

												return FReply::Handled();
											})
								]

								+ SHorizontalBox::Slot()
								.AutoWidth()
								[
									SNew(SButton)
										.Text(FText::FromString(TEXT("選択した通知のタイプをフィルター")))
										.OnClicked_Lambda([this]()
											{
												ApplyNotifyVisibilityFilter();
												return FReply::Handled();
											})
								]
						]
				]
		];
}


上記のコードを見てみると、別の関数を呼び出している部分もありますが、UIの実装は一旦これで完成となります。
こちらで呼び出している各関数については、この後に記載しています。


続いて、追加したUIに対して中身の実装についてです。
実装の中身をひとつひとつ説明していくと長くなるため関数ごとに機能概要の説明までとさせていただきます。
今回作成したプラグインについては、後日公開予定なので詳細を知りたい方は、直接プラグインを手元で開いていただければと思います!(o^―^o)ニコ

Unreal Engineのクラスリフレクション機能を用いて、全ての通知クラスを列挙

アセットエディタ上で開いている UAnimSequence を探索

アニメーションシーケンス内で実際に使われている通知のクラス名を取得

通知クラス名をもとに、チェック可能なリストを作成

選択された通知名だけをハイライト表示し、それ以外は色を黒にする

「Notify_1」「Notify_2」など、同じクラスの通知名に付いた数字を排除

通知の色を元の状態に復元し、フィルタリングを解除

一度だけ実行される初期キャッシュ用

チェックされている通知だけを集めて、フィルター対象として返す

通知名がチェックされたリストに含まれていれば表示対象とし、含まれていなければ非表示とする

AnimNotify / AnimNotifyState の表示色を変更

AnimNotify / AnimNotifyState の表示色を復元

あとから追加されたAnimNotify / AnimNotifyStateの色補完

実装結果

youtu.be

最後に

プラグインの作成については以上で終わりとなります。お疲れ様でした。
このまま公開に向けたお話しをすると長くなりそうなので、一旦ここで切らさせていただきます。
次回は、今回作成したプラグインを公開するところまでお話しできればと思いますので、そちらも公開されたらぜひチェックしていただければ嬉しいです...!! (*^^)v

©2025 SPARKCREATIVE Inc.