UE4 PrimaryDataAssetを使ってみる

# 概要
UnrealEngine のプライマリデータアセット(UPrimaryDataAsset)を使ってみるメモ書きです。
プライマリデータアセットを管理するアセットマネージャーの差し替えも試してみます。


## 更新履歴
| 日付 | 内容 |
|:----|:-----|
|2020/03/17| 4.24での不具合と回避方法と追記 |
|2020/11/14| ボタン機能について追記 |
|2021/03/16| ロードバンドルについて追記 |

## 環境
Windows10
Visual Studio 2017
UnrealEngine 4.22, 4.24, 4.25


## 参考
以下を参考にさせて頂きました、ありがとうございます。

[UE4 アセットマネージメントフレームワークについて](http://unrealengine.hatenablog.com/entry/2017/08/24/235153)
[UnrealEngineドキュメント-アセット管理](https://api.unrealengine.com/JPN/Engine/Basics/AssetsAndPackages/AssetManagement/index.html)
https://www.slideshare.net/EpicGamesJapan/ue4action-rpg
[[UE4] Asset Managerのアセットの非同期ロード機能について その3 (PrimaryDataAsset, PrimaryAssetLabelはいいぞ編)](https://qiita.com/EGJ-Kaz_Okada/items/4616c0a21b8e9842a14a)


# 関連用語
自分なりの（ざっくりした）理解で関連用語を最初に整理します。

## アセットマネージャ(AssetManager)
アセットの非同期読み込みを処理、管理する機能。エンジン側でシングルトンが用意されているようです。

## プライマリデータアセット(PrimaryDataAsset)
アセットマネージャで管理されるデータアセットで、それ以外はセカンダリデータアセットと呼ばれ、StreamableManagerを使って自前でロード管理をする必要があるようです（プライマリーアセットが参照を持っている場合を除く）。

## アセットオーディット(AssetAudit)
アセットを監査することができるUEエディタ付属のデベロッパツール。
ファイルサイズ、メモリ使用量、プロパティなどが一覧で見ることができるようです。
[ウィンドウ] -> [デベロッパーツール] -> [アセットの監査]


# PrimaryAssetLabelを使う
エンジン側で用意されている UPrimaryDataAssetクラスを継承した PrimaryAssetLabelクラスを使ってみます。

## データアセットの用意
コンテンツブラウザで右クリック -> 「その他」 -> 「データアセット」を選択し、PrimaryAssetLabelを選びます。

![primarydataasset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/f36ce53b-a162-665b-00c5-f6e270dc9381.png)


作られたデータアセットの詳細は以下の様になります。
![DataAssetDetail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/70981f8b-be74-30b0-5ceb-98a1dd32502a.png)

[Explicit Assets]や[Explicit Blueprints]にアセットが設定できます。これがロードされるデータとなります。
テストでDataTableアセットを１つ登録しています。


## ロード処理
[Async Load Primary Asset]ノードで非同期ロードができます。解放は[Unload Primary Asset]で行うようです。
複数アセットの場合は[Async Load Primary Asset List]が使えます。
以下はアクターの BeginPlay と EndPlay にてロード、アンロードを行った例です。

![load.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/f310062b-9eaf-940b-99d4-a1c0117a2086.png)

ロードメソッドの実体は
Engine/Source/Runtime/Engine/Private/AsyncActionLoadPrimaryAsset.h
の UAsyncActionLoadPrimaryAssetクラスです。
アンロードは UKismetSystemLibraryクラスにあるようです。


## ロード済データアセットの扱い例
C++側で UDataTableを持って、そちらに参照をセットすることでC++にて扱うこともできます。

```cpp:.h
UPROPERTY()
UDataTable* Data;
```

PrimaryDataAssetの ExplicitAssets に仕込んだ DataTable（FTestParam構造体）を取得する例です。

```cpp:.cpp
UPrimaryAssetLabel* _pAsset = Cast<UPrimaryAssetLabel>(Data);
if( _pAsset != nullptr){
	// データテーブル取得	
	UDataTable* _Data = Cast<UDataTable>(_pAsset->ExplicitAssets[0].Get());

	// データテーブル展開
	TArray<FTestParam*> OutAllRows;
	FString _Context;		// エラー時用
	_Data->GetAllRows<FTestParam>(_Context, OutAllRows);
	for (FTestParam* _It : OutAllRows)
	{
		// データ表示
		UE_LOG(LogTemp, Log, TEXT("%d, %f"), _It->Param1, _It->Param2);
	}
		
}

```

PrimaryAssetLabel は汎用につくられているようなので、クラスの型などが UObject のみとなりいろいろなクラスを扱うにはキャストなどが必要なため、やや面倒になると思われます。
その代わり面倒な設定は一切不要で使うことができます。


# UPrimaryDataAssetを継承して使う
データアセットで設定できる項目をカスタマイズしたい場合は自前でUPrimaryDataAssetを継承したクラスをつくる必要があるので試してみます。更にAssetManagerクラスも継承してみます。


## C++クラスの用意
### PrimaryDataAssetクラスを継承したクラスを用意
試しにアイテムデータを想定してみます。アイテム名やアイコンデータ等を持つクラスというイメージです。

![primarydataassetclass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/d0469f69-da75-62ff-603c-d585d332a0bc.png)

[DataAsset]ディレクトリに ItemDataAsset.h というファイルを作成します。UPrimaryDataAssetを継承して、メンバを持つ基底クラスとします。

```cpp:ItemDataAsset.h
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Styling/SlateBrush.h"
#include "ItemDataAsset.generated.h"


// アイテムデータアセット基底クラス
UCLASS(Abstract, BlueprintType)
class TEST_API UItemDataAsset : public UPrimaryDataAsset
{
	GENERATED_BODY()
	
public:
	// コンストラクタ
	UItemDataAsset()
		:LiveTime(5.0f)
	{}

	// プライマリアセットID名を取得する
	UFUNCTION(BlueprintCallable, Category = Item)
		FString GetIdentifierString() const;

	// プライマリアセットIDを取得する（要オーバーライド）
	virtual FPrimaryAssetId GetPrimaryAssetId() const override;


	// アイテムタイプ
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UItemDataAsset")
		FPrimaryAssetType	ItemType;

	// アイテム名
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UItemDataAsset")
		FText		ItemName;
	// アイテムアイコン
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UItemDataAsset")
		FSlateBrush	ItemIcon;
	// 生存時間
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UItemDataAsset")
		float		LiveTime;

};

```

GetPrimaryAssetId()での ItemType は継承先のクラスで設定します。

```cpp:ItemDataAsset.cpp
#include "ItemDataAsset.h"

// プライマリアセットID名を取得する
FString UItemDataAsset::GetIdentifierString() const
{
	return GetPrimaryAssetId().ToString();
}

// プライマリアセットIDを取得する
FPrimaryAssetId UItemDataAsset::GetPrimaryAssetId() const
{
	return FPrimaryAssetId(ItemType, GetFName());
}
```

### アセットマネージャクラスの用意
自前のアセットマネージャクラスを用意する事は今回のコード例程度では必要ありませんが、追加したFPrimaryAssetType定義をまとめるためと、将来的な拡張のため定義しておきます。（複雑なデータアセット構成になると必要になるはず。。）

![assetmanager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/2b4a3253-3914-2df8-8e74-490b6d1b668f.png)

AssetManagerを継承した自前のMyAssetManagerクラスを用意します。

プライマリデータアセットのタイプをstaticで定義します。


```cpp:MyAssetManager.h
#include "CoreMinimal.h"
#include "Engine/AssetManager.h"
#include "MyAssetManager.generated.h"


// アセットマネージャ
UCLASS()
class TEST_API UMyAssetManager : public UAssetManager
{
	GENERATED_BODY()
	
public:
	// コンストラクタ
	UMyAssetManager(){}

	// 初期ロード開始時
	virtual void StartInitialLoading() override;

	// アイテムタイプ定義
	static const FPrimaryAssetType	DropItemType;

	// アセットマネージャオブジェクトの取得
	static UMyAssetManager& GetObj();
};
```

アイテムタイプ定義の "DropItem" はプロジェクト設定で設定する必要があります、他のプライマリデータアセットと被るのはNGです。

```cpp:MyAssetManager.cpp
#include "MyAssetManager.h"
#include "./ItemDataAsset.h"

const FPrimaryAssetType	UMyAssetManager::DropItemType = TEXT("DropItem");

// アセットマネージャオブジェクトの取得
UMyAssetManager& UMyAssetManager::GetObj()
{
	UMyAssetManager* This = Cast<UMyAssetManager>(GEngine->AssetManager);

	if (This)
	{
		return(*This);
	}
	else
	{
		UE_LOG(LogTemp, Fatal, TEXT("DefaultEngine.ini でのAssetManagerの設定が不正です。"));
		return(*NewObject<UMyAssetManager>());	// 呼ばれない
	}
}

// 初期ロード開始時(InitializeObjectReferencesから呼ばれる)
void UMyAssetManager::StartInitialLoading()
{
	Super::StartInitialLoading();

}
```

### 実際のデータアセットクラスを用意
先ほど作成した UItemDataAsset を継承したクラスを作成します。これが実際のデータを設定するクラスとなります。
基底クラスからアクターを追加したものにしています。

```cpp:ItemDataAsset_DropItem.h
#include "CoreMinimal.h"
#include "DataAsset/ItemDataAsset.h"
#include "./MyAssetManager.h"
#include "ItemDataAsset_DropItem.generated.h"

// ドロップアイテム
UCLASS()
class TEST_API UItemDataAsset_DropItem : public UItemDataAsset
{
	GENERATED_BODY()
	
public:
	// コンストラクタ
	UItemDataAsset_DropItem()
	{
		// アセットマネージャで定義しているタイプを設定する
		ItemType = UMyAssetManager::DropItemType;
	}

	// アクター
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DropItem")
	TSubclassOf<AActor> Actor;
};

```

C++クラスは以上になります。
![cpp_files.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/a8f143f2-ec0e-fbc1-b7aa-2d8ddb1fb904.png)
アセットブラウザで見ると上記のようになりました。


## 各種設定
### AssetManagerの設定
[プロジェクト設定] -> [ゲーム] -> [Asset Manager] -> [Primary Asset Type to Scan] に設定を追加します。

![AssetManagerSetting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/6d5361ba-835e-4eab-8bdb-26524f87171b.png)



[Primary Asset Type]には MyAssetManager で記述した "DropItem" を記載します。ここの文字列とGetPrimaryAssetId()で返す文字列の一致が必須のためです。
不一致や名称が被る場合は、アウトプットログに警告 **[LogLinker : Warning: ～]** がでます。

[Asset Base Class]には作成したクラス [ItemDataAsset_DropItem]を入れます。
[Directories]には実際作成したデータアセットが入っているパスを指定します。もしくは[Specific Assets]でディレクトリ単位ではなく、直接ファイル指定も可能なようです。

<font color="red">[Should Guess Type and Name in Editor]のチェックを外さないとパッケージ化の時に不具合が発生するようです。</font>


### DefaultEngine.iniの設定
アセットマネージャを作成したクラスに差し替えるため、[Config]フォルダにある DefaultEngine.iniに以下の設定を追加します。

```ini:DefaultEngine.ini
[/Script/Engine.Engine]
AssetManagerClassName=/Script/Test.MyAssetManager
```

[Test]がプロジェクト名、[MyAssetManager]がアセットマネージャクラス名になります。


## データアセットの用意
自作したクラスを使用したデータアセットを作ります。
アセットブラウザにて右クリック -> [データアセット]を選択し、先ほど作成した ItemDataAsset_DropItem クラスを指定します。

![DataAssetClass.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/670f8735-2eae-1278-fc47-335f6ea9519c.png)

作成したデータアセットは以下の様になります。先に定義したItemDataAssetクラスと ItemDataAsset_DropItemクラスのプロパティが設定できます。
![DropItem_Coin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/d80439e0-f990-da98-ac4f-bc14e5733b7a.png)

必要に応じて複数のデータアセットを作成し、データを切り分けることができます。


## ロード処理
ロード処理は PrimaryAssetLabel の時と同じように扱うことができます。

![loadprimaryasset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/adf3c1fc-05b3-1a35-27cd-9b60c4071092.png)

ブラウズに出てこない場合は、プロジェクト設定のAssetManagerの項目を再確認してください。

ロード処理クラスのC++ソースファイルは 
Engine/Source/Runtime/Engine/Private/AsyncActionLoadPrimaryAsset.h
です。


# アセットバンドルについて
ロード時に `Load Bundles`にて追加でロードするアセットを指定できます。

![AsyncLoad.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/f35ab616-bee0-d1dd-b6a2-a12e3e581ec7.jpeg)

バンドルでロードする場合、以下の様に `meta=(AssetBundles = タグ名)` を指定することで、このタグを `Load Bundles`を渡して追加でロードすることができます。
また普通にアセットのリファレンスパス(` SkeletalMesh'/Game/Mesh/TestMesh.TestMesh'` など)を指定することもできます。


```cpp:MyPrimaryDataAsset.h
UCLASS(BlueprintType)
class TEST_API UMyPrimaryDataAsset : public UPrimaryDataAsset
{
	// 省略

	// バンドルでロードするメッシュ１
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AssetBundles = "Option1")
	TSoftObjectPtr<USkeletalMesh> OptionMesh1;
	// バンドルでロードするメッシュ２
	UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (AssetBundles = "Option2")
	TSoftObjectPtr<USkeletalMesh> OptionMesh2;
};
```

上記の場合、引数`Load Bundles`に `Option1`を指定すると `OptionMesh1`が、`Option2`を指定すると `OptionMesh2`が、両方指定すると両方のメッシュがバンドルとしてロードされます。


## バンドルの変更
プライマリデータアセットをロード後、追加でロードしたバンドルを細かく変更することができます。対応メソッドとして、`Async Change Bundle State for Matching Primary Assets` と`Async Change Bundle State for Primary Asset List` が用意されています。
個別に追加バンドルと削除バンドルを指定する下のノードを使うのがいいと思います。

![ChabgeBundle.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/e8faeacc-6c9f-5b7e-d8df-8b649ea56168.jpeg)



# 追加機能
## イベントボタンの付与
データアセット定義に以下のようなメタ情報をつけたメソッドを追加するとデータアセットにボタンを付与でき、任意の処理を実行することができます。

```cpp:.h
	
// 任意の処理をする
UFUNCTION(Category="Execute", meta = (CallInEditor = "true", ToolTip = "任意処理をする"))
  void Proc();
```

```cpp:.cpp
void UMyDataAsset::Proc()
{
	UE_LOG(LogTemp, Log, TEXT("Proc!"));
}
```

![Button.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/1f61eece-d6d2-288e-2abb-2357bfd605dc.jpeg)

値のチェックなどの処理を書いたり、別データからパラメータを取得したりなどの処理を書くことができるかと思います。


# その他
## アセットマネージャからプライマリデータアセットのオブジェクトを取得する

AssetManagerクラスのGetPrimaryAssetObject()でロード済のオブジェクトを取得することができるようです。

```cpp
// ヘッダファイル
UMyDataAsset* DataFile;

// コンストラクタ
static ConstructorHelpers::FObjectFinder<UMyDataAsset> DataFile(TEXT("MyDataAsset'/Game/TestAsset00.TestAsset00'"));
if (DataFile.Object) {
	DataAsset = DataFile.Object;
}

...

UMyAssetManager& _Mgr = UMyAssetManager::Get();
if (DataAsset) {
	// アセットマネージャから取得する
	if( _Mgr.GetPrimaryAssetObject(DataAsset->GetPrimaryAssetId())){
		UE_LOG(LogTemp, Log, TEXT("Get UObject"));
	} else {
		UE_LOG(LogTemp, Log, TEXT("nullptr")); // メモリにない
	}
}

```

プライマリアセットIDが分かっている場合はそのままID指定でも取得できます。

```cpp:.cpp
auto& _Mgr = UMyAssetManager::Get();
	
// プライマリアセットIDを直接指定して取得
auto _Asset = Cast<UMyDataAsset>(_Mgr.GetPrimaryAssetObject( FPrimaryAssetId(UMyAssetManager::DropItemType, TEXT("TestAsset00")) ) );
if( _Asset ){
	UE_LOG(LogTemp, Log, TEXT("Get![%d]"), _Asset->ParamInt );
} else {
	UE_LOG(LogTemp, Log, TEXT("nullptr")); // メモリにない
}
```


## PrimaryDataAssetをパッケージに含める
PrimaryDataAsset は PrimaryAssetIDで指定するため、通常のアセットと異なり参照がつかないようです、そのためそのままパッケージングを行うとパッケージに含まれません。
対策は以下のようなものがあります。

### CookRuleで指定する
プロジェクト設定の [AssetManager] で対象のデータアセットから
[Rules] -> [Cook Rule] で [Always Cook]などを指定する。

![CookRule.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/46c9e1b9-c0f2-24e3-8225-8cd54324db02.jpeg)

**プライマリアセットタイプ単位の指定なのでパッケージに含みたくないデバッグ用アセットなどは別のデータタイプとして指定しなければなりません。**

### パッケージングで設定する
プロジェクト設定の [パッケージング] -> [Additional Asset Directories to Cook] 
にフォルダ単位でパッケージに含むように指定をする。

![AdditionalAsset.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/7ce54b55-a516-8506-b1f8-197907cfabca.jpeg)


**フォルダ単位での指定なのでフォルダ構成を考慮しておかないと余計なファイルが入ってしまいます。**

### 4.24での不具合？と回避方法
[プロジェクト設定] -> [アセットマネージャ] -> [スキャンするプライマリアセットタイプ(Primary Asset Type to Scan)]にてアセットタイプを追加や削除する場合、そのまま追加or削除するとパッケージ化する時にエラーが出ます。

回避するには以下の設定を追加する必要があります。

```DefaultGame.ini
[/Script/Engine.AssetManagerSettings]
-PrimaryAssetTypesToScan=(PrimaryAssetType="Map",AssetBaseClass=/Script/Engine.World,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game/Maps")))
-PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",AssetBaseClass=/Script/Engine.PrimaryAssetLabel,bHasBlueprintClasses=False,bIsEditorOnly=True,Directories=((Path="/Game")))
```

この設定は4.22の時点では自動で追加されていたのですが、4.24ですと付与されません。(4.23では未確認)


### ロードキャンセル
非同期ロードのキャンセルは以下のノードで。個別のキャンセルは見当たりません。

![CancelASyncLoad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/0686a967-2913-f4d3-7288-b955116b6c91.png)

```cpp:Kismet/GameplayStatics.h
/** 現在キューイングされたストリーミングパッケージを全てキャンセルします */
UFUNCTION(BlueprintCallable, Category = "Game")
static void CancelAsyncLoading();
```



# まとめ
プライマリデータアセットは最小単位でデータがまとまっているように作成するとロード効率が良いと思います。リスト表示のようにパラメータを一気に見るためにはパラメータ部とアセット部を別途切り分けをするようなデータアセット設計を行うと可能かと思います。（あるいはスクリプトで検索して読み込むようにする等）