UE4のアセットアクションを拡張したい

#UE4のアセットアクションを拡張したい

※この記事の対象のUnreal Engine4のバージョンは4.22です。

##アセットアクション
　アセットアクションというのはコンテントブラウザでアセットを右クリックすると出てくるメニューです。アセットごとにいろいろ操作ができる便利なヤツ。Static Meshだとこんなメニューが出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/1d13d455-d171-a6d9-f702-71b06944e82e.png)

##動機
　UE4 4.22からStatic Meshの最小LODをプラットフォームごとに設定できる機能が追加されました。
　これがなかなか便利です。
[UE4.22リリースノート](https://api.unrealengine.com/JPN/Support/Builds/ReleaseNotes/4_22/index.html)の「新機能：プラットフォームごとのプロパティの改善」に説明があります。

　なにが便利かと言うと、モバイルむけのビルドではLOD0を捨てて、LOD1以降だけを使うといった指定ができます。頂点が減れば描画負荷もロード時間も下がるのでとても効果的です。
　とはいえ、ひとつずつのStatic Meshをダブルクリックして開いて、Static Meshエディタで最小LODの＋ボタンクリックして数値を入れて・・・という作業はなかなか面倒です。この作業をアセットアクションでやりたいなと思ったのがこの記事の動機です。

##アセットアクションの作成
　新規のアセットタイプに対するアセットアクションを追加するには、FAssetTypeActions_Baseの継承クラスを実装すれば良いです。
FAssetTypeActions_PlatformMinLodというクラスを作成してみました。

```
	PlatformMinLodTypeActions = MakeShared<FAssetTypeActions_PlatformMinLod>();
	FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get().RegisterAssetTypeActions(PlatformMinLodTypeActions.ToSharedRef());
```
こんな感じで作成したアクションをエディタに登録します。

アクション側ではGetActionsメソッドをオーバーライドして独自のアクションをメニューに追加します。

```AssetTypeActions_PlatformMinLod.cpp
void FAssetTypeActions_PlatformMinLod::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
	ParentAction->GetActions(InObjects, MenuBuilder);

	auto PlatformMinLodImports = GetTypedWeakObjectPtrs<UStaticMesh>(InObjects);
	MenuBuilder.AddMenuEntry(
		LOCTEXT("PlatformMinLod_SetMinLod", "Set Platform minLod"),
		LOCTEXT("PlatformMinLod_SetMinLodToolTip", "Set Platform minLod settings."),
		FSlateIcon(),
		FUIAction(
			FExecuteAction::CreateSP(
				this, &FAssetTypeActions_PlatformMinLod::SetPlatformMinLod, PlatformMinLodImports),
			FCanExecuteAction()
		)
	);

}
```
するとメニューに**Set Platform minLod**という項目が追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/1c177acd-9b88-2287-55b2-488872fda42c.png)

選択すると、PlatformMinLod::SetPlatformMinLodというメソッドが呼ばれます。

```PlatformMinLod.cpp
void UPlatformMinLod::SetPlatformMinLod(UStaticMesh* InMesh)
{
	if (IsValid(InMesh))
	{
		if (InMesh->GetNumLODs() >= MinNumLod)
		{
			for (auto& plod :PlatformMinLodSettings)
			{
				InMesh->MinLOD.PerPlatform.Add(FName(*plod.PlatformName), plod.minLod);
			}
			InMesh->MarkPackageDirty();
		}
	}
}
```
こんな感じになっていて、LOD数が一定数あるStatic Meshの場合に、設定を追加します。
設定はDefaultEditor.iniから取得するようにしました。

```DefaultEditor.ini
[/Script/PlatformMinLod.PlatformMinLod]
MinNumLod=3
+PlatformMinLodSettings=(PlatformName="Console",minLod=1)
+PlatformMinLodSettings=(PlatformName="Mobile",minLod=2)
```
この場合はLod数が3以上のアセットにConsoleは最小LODを1に、Mobileは最小LODを2に設定します。

こんなふうにUPROPERTYにconfig指定をしておくと、初期値にiniファイルに設定した値を設定できます。

```PlatformMinLod.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PlatformMinLod.generated.h"

/**
 * 
 */

class UStaticMesh;

USTRUCT(BlueprintType)
struct FPlatformMinLodSettings
{
	GENERATED_USTRUCT_BODY()

		FPlatformMinLodSettings()
		: PlatformName(TEXT(""))
		, minLod(0)
	{}

	UPROPERTY(config, EditAnywhere, Category = "PlatformMinLod")
	FString PlatformName;

	UPROPERTY(config, EditAnywhere, Category = "PlatformMinLod")
	int32 minLod;
};

UCLASS(config = Editor, defaultconfig)
class PLATFORMMINLOD_API UPlatformMinLod : public UObject
{
	GENERATED_UCLASS_BODY()
	
public:
	UPROPERTY(Config, BlueprintReadOnly, Category = "PlatformMinLod")
	int MinNumLod;

	UPROPERTY(Config, BlueprintReadOnly, Category = "PlatformMinLod")
	TArray<FPlatformMinLodSettings> PlatformMinLodSettings;

	UFUNCTION(BlueprintCallable, Category = "PlatformMinLod")
	void SetPlatformMinLod(UStaticMesh* staticMesh);
};
```
#勝った！！！
　・・・と思ったのも束の間。実行して状態を確認しようとアセットをダブルクリックすると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/ec4deb46-53de-8e90-28fe-3ab78f7ff238.png)
　うん？なんだこの画面？

　そう、Static Meshに対するActionを上書きしてしまったので、もともとの機能が使えなくなってしまってます。メニューを良くみると、もともとのStatic Meshのアクションも消えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/ec4573c2-3094-7edb-f748-4d46b6501bf1.png)

　Static Meshエディターすら開かなくなってしまいました。
　これではさすがに使い物になりません。さてどうしようか。
　エンジン内部のコードを眺めたり、いろいろ考えたりした結果、解決方法を思いつきました！
　そう、もともとのStatic Meshへのアクションを新しく作ったアクションから呼び出してやれば良いのです。クラスをオーバーライドするのと同じですね。

ポイントは大本のアクションをParentActionという変数に保存しておき、それぞれのメソッドから呼びだしてやります。

```
	PlatformMinLodTypeActions->SetParent(FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get().GetAssetTypeActionsForClass(UStaticMesh::StaticClass()).Pin());
```
こんな感じでUStaticMeshに対するアクションを取得しておきます。

Static Meshエディターが開くようにするには、OpenAssetEditorから下の様に呼び出してやれば良いのです。

```
	virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;

void FAssetTypeActions_PlatformMinLod::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor)
{
	ParentAction->OpenAssetEditor(InObjects, EditWithinLevelEditor);
}
```
メニューも

```
void FAssetTypeActions_PlatformMinLod::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
	ParentAction->GetActions(InObjects, MenuBuilder);

	auto PlatformMinLodImports = GetTypedWeakObjectPtrs<UStaticMesh>(InObjects);
	MenuBuilder.AddMenuEntry(
		LOCTEXT("PlatformMinLod_SetMinLod", "Set Platform minLod"),
		LOCTEXT("PlatformMinLod_SetMinLodToolTip", "Set Platform minLod settings."),
		FSlateIcon(),
		FUIAction(
			FExecuteAction::CreateSP(
				this, &FAssetTypeActions_PlatformMinLod::SetPlatformMinLod, PlatformMinLodImports),
			FCanExecuteAction()
		)
	);

}
```
　こんなふうにもとのアクションのGetActionsを呼び出してから、自分のメニューを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/9ab8d1d3-9ca0-9379-568a-84ea466aec14.png)

　実行した結果、LOD設定の最小LODが設定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/fb1352b2-01e9-8ed1-6d9c-d678fb676461.png)

　もともとのStatic Meshのアクションに"Set Platform minLod"が追加されました。

#今度こそ勝った！！！

　ようやくやりたいことができるようになりました。めでたしめでたし。

　性能の低いコンソールやモバイルでは最小LODでLODのレベルを下げてやるのはけっこう有効です。描画負荷もさがるしロードも少し速くなります。

#余談

　まあ目的を達したのですが、その後気づいたことがあります。Apex Destructionのプラグインを有効にすると、Static Meshのアクションに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147177/f4ae8886-6529-744c-0c69-219a93e622c1.png)
Creative Destructible Meshというメニューが追加されます。
あれ？これはどうやってるんだろ？と調べた結果

```
	if (!IsRunningCommandlet())
	{
		FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
		TArray<FContentBrowserMenuExtender_SelectedAssets>& CBMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();

		CBMenuExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateStatic(&FDestructibleMeshEditorModule::OnExtendContentBrowserAssetSelectionMenu));
		ContentBrowserExtenderDelegateHandle = CBMenuExtenderDelegates.Last().GetHandle();
	}
```

```ApexDestructionEditorModule.cpp
TSharedRef<FExtender> FDestructibleMeshEditorModule::OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets)
{
	TSharedRef<FExtender> Extender(new FExtender());

	// Run thru the assets to determine if any meet our criteria
	bool bAnyStaticMesh = false;
	for (auto AssetIt = SelectedAssets.CreateConstIterator(); AssetIt; ++AssetIt)
	{
		const FAssetData& Asset = *AssetIt;
		bAnyStaticMesh = bAnyStaticMesh || (Asset.AssetClass == UStaticMesh::StaticClass()->GetFName());
	}

	if (bAnyStaticMesh)
	{
		// Add the sprite actions sub-menu extender
		Extender->AddMenuExtension("GetAssetActions", EExtensionHook::After, nullptr, FMenuExtensionDelegate::CreateLambda([SelectedAssets](FMenuBuilder& MenuBuilder)
		{
			MenuBuilder.AddMenuEntry(
				NSLOCTEXT("AssetTypeActions_StaticMesh", "ObjectContext_CreateDestructibleMesh", "Create Destructible Mesh"),
				NSLOCTEXT("AssetTypeActions_StaticMesh", "ObjectContext_CreateDestructibleMeshTooltip", "Creates a DestructibleMesh from the StaticMesh and opens it in the DestructibleMesh editor."),
				FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.DestructibleComponent"),
				FUIAction(FExecuteAction::CreateStatic(&FAssetTypeActions_DestructibleMesh::ExecuteCreateDestructibleMeshes, SelectedAssets), FCanExecuteAction()));
		}));
	}

	return Extender;
}
```
なんてやり方があるみたいです。こっちの方がスマートというか正攻法だったかな？
コンテントブラウザのメニューを拡張するインタフェースを使用しているようです。

僕のやり方だとアクションそのものや、アセットエディタも乗っ取る事ができるので、状況によって使い分けるのが良いかと思います。


#最後に
　ということで、アセットアクションを拡張する方法と、プラットフォームごとの最小LODを設定する方法でした。
　今回作成したものはプラグインにしてGitHubに置いておきます。

https://github.com/dgtanaka/UE4_PlatformMinLod_Plugin