😊

[UE5][C++] アセットアクションを追加してみる

2023/06/04に公開

概要

本記事は、C++アセットアクション追加チュートリアルになっています。

EditorUtility(BP)を使っても簡単に追加可能ですが、
物足りなくなる場面があった時にC++で解決したい時があります。
そういった来る場面の対処に向けて、事前に慣れておくためのチュートリアルです。

チュートリアル内容としては、
AnimMontageに特定のNotifyを入れ込むためのアセットアクションを追加します。

結構ボリュームのある内容になってますので、
ざっくりとあらかじめどういう実装があるのかを確認されたい方は、
先に下の記事を読んでおくといいかもしれません。
アセットアクション拡張 簡単解説

前提条件

設計の話になりますが、アセットアクションは基本エディタでしか使いません。
そのため、Editorモジュールとして切り分けて実装を行うのがベストです。
Editorモジュールへの切り分けは以下の記事を参考に作成してください。
https://zenn.dev/hakuto_gamedev/articles/a546a185dc02f6
作成したモジュールの下に、この後本記事で作成するソースコードを配置するようにすれば、
Editorモジュールへの切り分けが可能です。

アセットアクション追加クラスの作成

| 目的

アセットアクションを追加するためのクラス、
「AssetDefinition_EnhancedMontage」を作成します。
※エンジン標準クラス(AssetDefinition_AnimSequence)は外部モジュールに公開されていないようなので継承はせず、まったく新しいものを作成します。

| 手順

まずは、AssetDefinitionDefaultのC++クラスを追加します。
クラス名は前述している通り、「AssetDefinition_EnhancedMontage」 とします。

AssetDefinition_EnhancedMontage.h
UCLASS()
class TEST_API UAssetDefinition_EnhancedMontage : public UAssetDefinitionDefault
{
	GENERATED_BODY()

public:

	virtual FText GetAssetDisplayName() const override { return FText(); }
	virtual FLinearColor GetAssetColor() const override { return FLinearColor(FColor(100, 100, 255)); }
	virtual TSoftClassPtr<UObject> GetAssetClass() const override { return UAnimMontage::StaticClass(); }
	virtual bool CanImport() const override { return true; }
};
AssetDefinition_EnhancedMontage.cpp
UCLASS()
class TEST_API UAssetDefinition_EnhancedMontage : public UAssetDefinitionDefault
{
	GENERATED_BODY()

public:

	virtual FText GetAssetDisplayName() const override { return FText(); }
	virtual FLinearColor GetAssetColor() const override { return FLinearColor(FColor(100, 100, 255)); }
	virtual TSoftClassPtr<UObject> GetAssetClass() const override { return UAnimMontage::StaticClass(); }
	virtual bool CanImport() const override { return true; }
};
AssetDefinition_EnhancedMontage.cpp

#define LOCTEXT_NAMESPACE "AssetTypeActions"

namespace MenuExtension_EnhanceMontage
{


	/*************************************************************************************************
	 * @brief	モンタージュにNotifyを入れる実行関数
	*************************************************************************************************/
	void ExecuteAnimNotifyInsertToMontage(const FToolMenuContext& Context)
	{
		const UContentBrowserAssetContextMenuContext* CBContext = UContentBrowserAssetContextMenuContext::FindContextWithAssets(Context);
		TArray<UAnimMontage*> SelectedObjects = CBContext->LoadSelectedObjects<UAnimMontage>();
		for (UAnimMontage* Montage : SelectedObjects)
		{
			if (Montage)
			{
				//新しいNotifyを追加し、インデックスを確保
				int32 NewNotifyIndex = Montage->Notifies.Add(FAnimNotifyEvent());

				//新しいNotifyに各種パラメータの設定やモンタージュとのリンクをする
				FAnimNotifyEvent& NewEvent = Montage->Notifies[NewNotifyIndex];
				NewEvent.NotifyName = *FString("TestNotify");
				NewEvent.Guid = FGuid::NewGuid();

				//モンタージュとのリンク関数。第2引数がNotifyの開始位置
				NewEvent.Link(Montage, 0.f);
				NewEvent.TriggerTimeOffset = GetTriggerTimeOffsetForType(Montage->CalculateOffsetForNotify(0.f));
				NewEvent.TrackIndex = 0;

				UObject* AnimNotifyClass = NewObject<UObject>(Montage, UAnimNotify_PlayParticleEffect::StaticClass(), NAME_None, RF_Transactional);
				NewEvent.Notify = Cast<UAnimNotify>(AnimNotifyClass);



				if (NewEvent.Notify)
				{
					//Notifyオブジェクトから初期のトリガー閾値を設定
					NewEvent.TriggerWeightThreshold = NewEvent.Notify->GetDefaultTriggerWeightThreshold();

					//ExposeOnSpawnに設定されているパラメータチェック・対応
					{
						TArray<FAssetData> SelectedAssets;
						AssetSelectionUtils::GetSelectedAssets(SelectedAssets);

						for (TFieldIterator<FObjectProperty> PropIt(NewEvent.Notify->GetClass()); PropIt; ++PropIt)
						{
							if (PropIt->GetBoolMetaData(TEXT("ExposeOnSpawn")))
							{
								FObjectProperty* Property = *PropIt;
								const FAssetData* Asset = SelectedAssets.FindByPredicate([Property](const FAssetData& Other)
									{
										return Other.GetAsset()->IsA(Property->PropertyClass);
									});

								if (Asset)
								{
									uint8* Offset = (*PropIt)->ContainerPtrToValuePtr<uint8>(NewEvent.Notify);
									(*PropIt)->ImportText_Direct(*Asset->GetAsset()->GetPathName(), Offset, NewEvent.Notify, 0);
									break;
								}
							}
						}
					}

					//エディタでNotifyが作成された時のコールバック呼び出し
					NewEvent.Notify->OnAnimNotifyCreatedInEditor(NewEvent);
				
				}

				//プロパティ変更後処理呼び出し
				Montage->PostEditChange();

				//編集マークを付ける(アセット左上につくアスタリスク)
				Montage->MarkPackageDirty();
			}
		}

	}


	/*************************************************************************************************
	 * @brief  アセットアクションメニューにセクションを追加する
	 *************************************************************************************************/
	void FillCreateMenu(UToolMenu* Menu)
	{
		IAssetTools& AssetTools = IAssetTools::Get();

		FToolMenuSection& Section = Menu->FindOrAddSection("CreateAssetsMenu");
		if (AssetTools.IsAssetClassSupported(UAnimMontage::StaticClass()))
		{
			//アセットアクションの各種表示情報,
			const TAttribute<FText> Label = LOCTEXT("ActionSecondMenuLabel", "InserAnimNotifyToAnimMontage");
			const TAttribute<FText> ToolTip = LOCTEXT("ActionSecondMenuTooltip", "Notify insert to AnimMontage.");
			const FSlateIcon Icon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "ClassIcon.AnimMontage");

			//アセットアクションの関数バインド
			FToolUIAction UIAction;
			UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteAnimNotifyInsertToMontage);

			//メニューに追加
			Section.AddMenuEntry("AnimMontage_NewAnimNotifyInsert", Label, ToolTip, Icon, UIAction);
		}
	}



	/*************************************************************************************************
	 * @brief	前述のセクション追加関数をエンジン初期化後のタイミングで呼ぶように設定
	 *************************************************************************************************/
	static FDelayedAutoRegisterHelper DelayedAutoRegister(EDelayedRegisterRunPhase::EndOfEngineInit, []
		{
			//ツールメニュー登録用コールバックを登録する
			UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateLambda([]()
				{
					FToolMenuOwnerScoped OwnerScoped(UE_MODULE_NAME);
					UToolMenu* Menu = UE::ContentBrowser::ExtendToolMenu_AssetContextMenu(UAnimMontage::StaticClass());

					FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions");
					Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection)
						{
							IAssetTools& AssetTools = IAssetTools::Get();
							if (AssetTools.IsAssetClassSupported(UAnimMontage::StaticClass()))
							{
								InSection.AddSubMenu("AnimMontage_InsertNotify", LOCTEXT("ActionMenuName", "InserNotify"), LOCTEXT("ActionMenu_Tooltip", "Notify insert to montage"),
									FNewToolMenuDelegate::CreateStatic(FillCreateMenu),
									false,
									FSlateIcon(FAppStyle::GetAppStyleSetName(), "Persona.AssetActions.CreateAnimAsset")
								);
							}
						}));
				}));
		});
}


#undef LOCTEXT_NAMESPACE //"AssetTypeActions"

こちらのソースコードを参考に、コーディングしていきます。
ソースコードがそこそこあるので、
テキストエディタ等にコピペしたほうが見やすいかもしれません。
※あえて1つの関数に押し込んでます。

以上で、クラスの追加は完了です。
これだけで、AnimationMontageのアセットアクションに項目が追加されているはずです。

結果

クラス追加後、ビルドを行い成功すると以下のようになります。


AnimMontageのアセットアクションにしっかりと追加されています。


アセットアクションを実行すると、PlayParticleEffectNotifyが追加されています。

まとめ

今回の方法では、メニュー名を日本語にすると文字化けします。
回避策がわかり次第、追記したいと思います。
※英語でも困ることないので、当分は調べなさそうです...

また今回の実装のみを行うと、アセットをダブルクリックしたときに
ペルソナが表示されず、詳細ビューだけが表示されるようになってしまいます。
これは、OpenAssets関数を実装していないためです。
今回の話の主旨とはずれてくるので、詳細は解説しません。
エンジンソースのAssetDefinition_AnimationAssetのOpenAssetsを参考に実装すれば
元通りにペルソナが表示されるように修正可能です。

Discussion