[UE4] Asset Managerのアセットの非同期ロード機能について その3 (PrimaryDataAsset, PrimaryAssetLabelはいいぞ編)


# 前回・前々回の記事：
[[UE4] Asset Managerのアセットの非同期ロード機能について その1 ( 非同期ロードの解説 & レベルの裏読み編 )](https://qiita.com/EGJ-Kaz_Okada/items/f18bca3fb5c8fc1aea9c)
[[UE4] Asset Managerのアセットの非同期ロード機能について その2 ( レベルアセット以外の裏読み編 )](https://qiita.com/EGJ-Kaz_Okada/items/7dba130c3641aa456b73)

以降は、↑の記事を読んでいる前提で進みます。

# はじめに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/9c9fca72-6523-75da-772d-31ff9ce45f71.png)
前回はレベル以外のアセットをPrimary Assetとして認識させることでAssetManagerによる非同期ロードする方法について説明しました。しかし、エンジンコードを編集する必要があったり処理負荷が増えたりと面倒な方法でした。

そんな時に便利なのが今回ご紹介する **PrimaryDataAsset** と **PrimaryAssetLabel** です！これらをPrimary Assetとし、実際にロードしたいアセットを紐付けることで間接的にロードを行う形になります。こうすることで、前回の記事で説明したようなエンジンコードの改造をしなくても、簡単にAsset Managerによる非同期ロードを実行する事が可能になります！

それではまずはPrimaryDataAssetについて解説し、次にその派生クラスであるPrimaryAssetLabelについて説明します。
# PrimaryDataAssetについて
**PrimaryDataAssetは、AssetManagerによる管理に特化したDataAsset**です。
DataAssetについて簡単に説明しますと…DataTable (https://docs.unrealengine.com/ja/Gameplay/DataDriven/index.html) のようなエクセルベースの比較的規模のでかい管理とは異なり、キャラクタ・アイテム単位などのより細かい粒度で管理するためのデータセットというのが分かり…やすい…かも…しれません…？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/233de918-d04c-56ca-d693-948bcc2c5ab7.png)
例えばActionRPGサンプルでは以下のように武器・スキル・アイテムのデータ・アセットを個別に管理しています。DataTableでも同様の管理は可能ではありますが、視認性が悪い・分業化がしづらい・カスタマイズしづらいなどのデメリットがあるため、個別に用意したDataAssetで管理した方が適してるケースがあります（逆にDataTableの方がやりやすいケースもあるので注意）。

また、各データのチェック・変換などの処理を組み込めるという点も大きなメリットとして持っています。人類は間違いを起こす生き物なのでヒューマンエラーを防止するための仕組みは非常に重要です！より具体的なDataAssetの機能・メリットに関してはヒストリア様の以下の記事・スライドが参考になるかと思います。

- [[UE4][C++] ユーザー定義オブジェクトのアセット化で楽をする](http://historia.co.jp/archives/7564/)
- [カリギュラオーバードーズにおけるUE4へのデータ移植の手引き p57より](https://www.slideshare.net/EpicGamesJapan/ue4festeast2018-caligula/57)

そんな便利なDataAssetに対してAsset Managerによる管理を行うための関数を追加したのが Primary Data Assetです。

```cpp:DataAsset.h
UCLASS(abstract, MinimalAPI, Blueprintable)
class ENGINE_VTABLE UPrimaryDataAsset : public UDataAsset
{
	GENERATED_BODY()

public:
	// UObject interface
	ENGINE_API virtual FPrimaryAssetId GetPrimaryAssetId() const override;
	ENGINE_API virtual void PostLoad() override;

#if WITH_EDITORONLY_DATA
	/** This scans the class for AssetBundles metadata on asset properties and initializes the AssetBundleData with InitializeAssetBundlesFromMetadata */
	ENGINE_API virtual void UpdateAssetBundleData();

	/** Updates AssetBundleData */
	ENGINE_API virtual void PreSave(const class ITargetPlatform* TargetPlatform) override;

protected:
	/** Asset Bundle data computed at save time. In cooked builds this is accessible from AssetRegistry */
	UPROPERTY()
	FAssetBundleData AssetBundleData;
#endif
};
```
詳細は``DataAsset.cpp``における実装をご確認いただくということで割愛しますが、前回の記事で紹介した``GetPrimaryAssetId関数``の組み込みや、アセットがロード・セーブされたタイミングでAsset Managerに対して更新リクエストを送る処理が追加されています。これらの処理が追加されたことで、Asset Managerによる管理対象であるPrimary Assetとして認識される準備が整った状態になっています。

しかし、PrimaryDataAssetはそのままでは全く役に立ちません！何故なら**ロードしたいアセットを登録する仕組み**が用意されていないからです。そのため、実際にPrimaryDataAssetを継承したC++クラスを作成し、その作成したC++クラスに対してロード対象のアセットを登録できるようにする必要があります。と言ってもやることは簡単です。

```cpp
UCLASS()
class ASSETMANAGERTEST0423_API UMyPrimaryDataAsset : public UPrimaryDataAsset
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere)
	USkeletalMesh* SkeletalMesh;
};
```

例えば前回の記事で苦労したSkeletalMeshアセットの非同期ロードを行う場合はこれだけでC++側の準備はOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/b754b932-0038-2d81-5968-b19a4f892062.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/8d288f37-07a8-ee9b-05cc-be0901f3f1d6.png)
そして、エディタ側で新規作成したC++クラスをベースに``MyPrimaryDataAsset``を作成し…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/e16f5888-414b-f5ee-8000-17ed12518c28.png)
作成した``MyPrimaryDataAsset``にロードしたいSkeletalMeshアセットを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/7af05371-e940-80e3-004a-82cdeb21f082.png)
``MyPrimaryDataAsset``がSkeletalMeshアセットを直接参照するようになりました（ここ大事！）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/87221ae2-f423-7abd-b54c-e60cf98f2586.png)
次に、プロジェクト設定のAsset Managerカテゴリに今回作成したクラスを追加します。ここで重要なのが一番下のCook Ruleです。**このCook RuleをAlways Cookにしないとパッケージに含まれないため、パッケージ起動時にAsset Managerによる非同期ロードが正常に動作しません** 。なお、これは対象のDataAssetがどこからも直接参照されていない場合の対策なので、対象のDataAssetがレベルなどから直接参照されている場合は少し話が変わってきます。これで追加した``MyPrimaryDataAsset``をAsset Managerが認識するようになりました。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/16ba95c8-3a77-5c4d-2476-06d4b15a8206.png)
そして、上記のようにロード・アンロード処理を実装して試した所…(stat LLMで計測)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/cd4fc2f1-e230-95fe-c1e1-6b2ad3be0fc2.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/c1e8621d-61b0-0c4e-c884-ebd06ad494ae.png)
このように``MyPrimaryDataAsset``を非同期ロードしたことで、``MyPrimaryDataAsset``が直接参照しているSkeletalMeshが連鎖的にロードされました！やったね！
…ただアンロード処理を走らせてもメモリ使用量が変化しません！なぜ！？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/80fb4efd-b586-377c-bfbc-71fc46cd340f.png)
実はUnload Primary AssetはあくまでPrimary Assetに対して行われるものなので、Primary Assetに参照されているアセット( **Secondary Asset** )に対して直接アンロード処理は呼ばれません。そのため、上図のようにガベージコレクションを走らせることで、どこからも参照されなくなったSecondary Assetをメモリから破棄する必要があります。

ここまでがPrimaryDataAssetの基本的な使い方でした。もう少し実践的な使い方を知りたい場合は ActionRPGサンプルの``RPGItem.cpp/h``が参考になるかと思います！

# PrimaryAssetLabelについて
ここまでPrimaryDataAssetの使い方について説明してきましたが、C++が必須になるため非エンジニアには少しハードルが高いかと思います…。そんな方に便利なのがこのPrimaryAssetLabelです！

PrimaryAssetLabelはPrimaryDataAssetの派生クラスであり、Secondary Assetを管理するための仕組みがデフォルトで用意されています。そのため、単純に特定のアセットを非同期ロードの対象にしたいというケースの場合はPrimaryAssetLabelを使えばC++を使わなくても大丈夫です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/6d05da28-3ac0-ca82-3970-1c81cfdac6f9.png)
PrimaryDataAssetの時と同様にPrimaryAssetLabelをベースにDataAssetを作成し…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/1ccf6d6d-fb5f-a426-bbf8-af0a002bd193.png)
作成したアセットを開くと上図のようなウィンドウが立ち上がります。今回は非同期ロードに関してなので、基本的にPrimary Asset Labelカテゴリ以下の各項目を編集することになります。

- Label Assets in My Directory
    - Primary Asset Labelが配置されているフォルダとそのサブフォルダに格納されているアセットを Secondary Asset に登録
- Is Runtime Label
    - 非同期ロードで使い場合は必ずONにする必要があります
    - ``IsEditorOnly(){return !bIsRuntimeLabel;}``
- Explicit Assets
    - 個別にアセットを Secondary Asset に登録
- Explicit Blueprints
    - 個別にBlueprintを Secondary Asset に登録
- Asset Collection
    - 指定のアセットコレクションに登録されているアセットを Secondary Asset に登録
    - https://docs.unrealengine.com/ja/Engine/Content/Browser/UserGuide/Collections/index.html

…結構充実しているかと思います！これを0から作るのは少し大変なので非常に助かる機能です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/404bfc85-2dea-8247-63fc-280077bf39de.png)
プロジェクト設定での設定に関しては、お気づきの方も多いと思いますが、PrimaryAssetLabelはデフォルトで登録されています！ただし、パッケージ上で非同期ロードを行う場合はCook Ruleの設定にご注意ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/10883dd7-a6e2-128a-4090-1562413e998a.png)
最後にロード・アンロード処理に関してです。ここはPrimaryDataAssetの場合と少し変わり、**Load Bundles**ピンに特定の文字列を含む配列を渡す形になります。具体的には以下のルールで文字列を渡す必要があります。 

- Label Assets in My Directoryで指定したSecondary Assetをロードする場合は``Directory``を含む文字列配列を渡す
- Explicit Assets/Blueprints で指定したSecondary Assetをロードする場合は``Explicit``を含む文字列配列を渡す
- Asset Collection で指定したSecondary Assetをロードする場合は``Collection``を含む文字列配列を渡す

```cpp:PrimaryAssetLabel.h
/** List of manually specified assets to label */
UPROPERTY(EditAnywhere, Category = PrimaryAssetLabel, meta = (AssetBundles = "Explicit"))
TArray<TSoftObjectPtr<UObject>> ExplicitAssets;

/** List of manually specified blueprint assets to label */
UPROPERTY(EditAnywhere, Category = PrimaryAssetLabel, meta = (AssetBundles = "Explicit", BlueprintBaseOnly))
TArray<TSoftClassPtr<UObject>> ExplicitBlueprints;
```
これは上記のように、TSoftObjectPtrを使った間接参照で管理されている上に**Asset Bundles**というMetaTagが登録されているからです。このようにすることで、AssetManagerによる非同期ロードの管理をより細かく行うことができます。
![Picture1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/55609f60-b1b4-6d99-fd3f-39aec89373ca.png)
例えば「勇者の服を装備しているときだけ、キャラクタのグラフィックに勇者の服のモデルを反映する」という仕様だったとします。このとき、勇者の服に関するアセットは必要ないときはロードしない（メモリに載せない）ようにしたい所です。でも、一つのDataAssetでキャラクタに関するアセットは全て管理したいケースもあるかと思います。そんなときに便利なのがこのAsset Bundles機能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/35cdc0f6-3571-a486-e8ee-53a7aa866ddc.png)
普段の服はCollectionで、勇者の服はExplicit Assetsで管理することで、必要に応じてロード・アンロード状態を切り替えることができます。ただ正直な所…これは説明用の極端な例です。実際にこのような細かい管理をしたい場合は…

```cpp
UCLASS()
class ASSETMANAGERTEST0423_API UMyPrimaryDataAsset : public UPrimaryDataAsset
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere)
	USkeletalMesh* SkeletalMesh;

	UPROPERTY(EditAnywhere, Category = PrimaryAssetLabel, meta = (AssetBundles = "Normal"))
	TArray<TSoftObjectPtr<UObject>> NormalAssets;

	UPROPERTY(EditAnywhere, Category = PrimaryAssetLabel, meta = (AssetBundles = "Hero"))
	TArray<TSoftObjectPtr<UObject>> HeroAssets;	
};
```

このようにUPrimaryDataAssetの派生クラスを用意した方がいいかとは思います。AssetBundles はPrimaryAssetLabel限定の機能ではないので、このようにPrimaryDataAssetの派生クラスでも使用することができます。

また、``UPrimaryAssetLabel::UpdateAssetBundleData()``における、特定のルールに従って特定のアセットを自動的にSecondary Assetに追加する仕組みは非常に参考になります。DirectoryとCollectionの項目はこの関数で実装されています。独自にPrimaryDataAssetを作る際はぜひ真似してみてください！

少し長くなりましたが、ひとまず…PrimaryAssetLabelで非同期ロード管理をしたい場合は Load/Add/Remove Bundlesに文字列配列を渡すことで Secondary Assetのロード・アンロード管理を行うことを覚えていってくださいまし！

# ここまでのまとめ
- 非同期ロードしたいアセットをPrimary Assetにするのは大変なので、PrimaryDataAsset/PrimaryAssetLabelのSecondary Assetに登録することで間接的に非同期ロードするようにしましょう！
- PrimaryDataAssetをカスタマイズすることで非同期ロードを効率よく管理できます。しかし、C++必須です。
- PrimaryAssetLabelを使えばC++は不要です。またAsset Bundle機能により細かくロード・アンロード管理ができます。

# 次回予告
BPアセットを非同期ロードしようとした際のトラブルについてご質問を頂くことは増えてきたので、そのあたりについて説明できればと思います！

おしまい