[UE4] Asset Managerのアセットの非同期ロード機能について その2 ( レベルアセット以外の裏読み編 )

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

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

# はじめに
前回はレベルアセットの非同期ロード（裏読み）について説明しました。しかし、記事の最後で説明した通り、レベルアセットを丸ごと非同期ロードするとメモリ使用量などの問題が発生する場合があります。そこで、今回はもう少し細かいアセット単位で非同期ロードする方法について説明します。

なお、本記事を作成する上で検証・使用したUE4のバージョンはUE4.23.1です。

# アセット単体で非同期ロードをするには
プロジェクトによって色んなケースがあると思いますが、キャラの衣装替え目的などで需要が多そうなSkeletalMeshアセット単体の非同期ロードを例にします。

## 前回の記事で紹介した手順でセットアップ・動作してみると…
まずは前回の記事で紹介した手順でセットアップをしていきましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/38207c4f-99ff-ec84-c4bb-d45e3c73e80d.png)
プロジェクト設定の`Asset Manager/Primary Asset Type to Scan` に SkeletalMeshアセットを追加します。これでSkeletalMeshアセットがAssetManagerの管理対象であるPrimary Assetとして登録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/fdd17760-5b0f-ecdb-eac7-a8f536d209f3.png)
`Async Load Primary Asset`ノードで非同期ロードを行います。今回は`IsValid`ノードを使ってロードが正常に完了したか確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/9238967f-2928-4ef6-6c04-ef2b0ef7aff1.png)
そして、PIEではなくStandalone起動で試すと…無事非同期ロードが完了しました！簡単ですね！
**…が、、いざPackageを作ろうとすると「`PrimaryAssetId SkeletalMesh:SK_Mannequin`が対象のアセットとマッチしてないよ！」というエラーが発生して失敗します。**

```cpp
UATHelper: Packaging (Windows (64-bit)):   LogAssetManager: Error: Registered PrimaryAssetId SkeletalMesh:SK_Mannequin for asset /Game/Mannequin/Character/Mesh/SK_Mannequin.SK_Mannequin does not match object's real id of ! This will not load properly at runtime!
PackagingResults: Error: Registered PrimaryAssetId SkeletalMesh:SK_Mannequin for asset /Game/Mannequin/Character/Mesh/SK_Mannequin.SK_Mannequin does not match object's real id of ! This will not load properly at runtime!
...
UATHelper: Packaging (Windows (64-bit)):   LogInit: Display: LogAssetManager: Error: Registered PrimaryAssetId SkeletalMesh:SK_Mannequin for asset /Game/Mannequin/Character/Mesh/SK_Mannequin.SK_Mannequin does not match object's real id of ! This will not load properly at runtime!
```

これは対象のクラスをPrimaryAssetとして管理する際に必要な`PrimaryAssetId`を返す`GetPrimaryAssetId`関数が`USkeletalMesh`クラスに実装されていないためです。

### 余談：Standalone起動時はエラーが出ずに正常に動くのはなぜ？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/3f0c917a-0e7a-fc86-bdfb-7d51302664f6.png)
プロジェクト設定の`Asset Manager/Should Guess Type and Name in Editor`がデフォルトで有効になっているためです。この機能が有効になっている場合、これから説明する`GetPrimaryAssetId`関数が対象のクラスに実装されていなくても非同期ロードに成功します。しかし、名前の通りエディタでしか動作しない機能なのでPackageでは動作しません。また、内部ではアセットパスからの検索処理を行っているため、中・大規模プロジェクトになるとAsset Managerに関するエディタ操作が遅くなる可能性があります。
いい感じにしてくれる便利な項目ではあるのですが、OFFにしておいた方がトラブルは少なくなるかと思います。具体的な処理内容が気になる方は`UAssetManager::ExtractPrimaryAssetIdFromData`をご確認くださいまし。

## Primary Asset Idに関するエラーを解決するには
この問題を解決するための方法は以下の2つです。

- `USkeletalMesh`に`GetPrimaryAssetId`関数を追加・実装する
- プロジェクト設定のAsset Manager/Should Manager Determine Type and Nameを有効にする


### USkeletalMeshにGetPrimaryAssetId関数を追加実装する
`GetPrimaryAssetId`関数がないというエラーだったので、`GetPrimaryAssetId`関数を実装すれば解決します（そのまま）。

```cpp:Engine\Source\Runtime\Engine\Classes\Engine\SkeletalMesh.h
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
```

```cpp:Engine\Source\Runtime\Engine\Private\SkeletalMesh.cpp
FPrimaryAssetId USkeletalMesh::GetPrimaryAssetId() const
{
	return FPrimaryAssetId(GetClass()->GetFName(), GetOutermost()->GetFName());
}
```
上記の対応を入れることで、各SkeletalMeshアセットがPrimaryAssetIdをAssetManagerに返すようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/5dffcd86-d2f4-2222-ef7e-282e901d1d5d.png)
ちなみに、PrimaryAssetIdは`PrimaryAssetType`, `PrimaryAssetName`から構成されており、`FPrimaryAssetId`の各引数ではそれらを設定しています。アセットにマウスを重ねることで、どんな文字列が設定されているか確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/ce11b39c-6e8e-b5a2-aef3-1ecb8caf5281.png)
そして、ここで設定した`PrimaryAssetType`はプロジェクト設定の`Asset Manager/Primary Asset Type to Scan`における`Primary Asset Type`と一致させる必要があります。もし一致していない場合はAsset Managerが認識できなくなります。

これでAsset ManagerがSkeletal Meshアセットを正しく認識・管理できるようになりました。プロジェクト側のクラスであれば上記のような対応で済むのですが、今回のようにエンジン側のクラスの場合はエンジンコードを編集することになります。

#### 余談：Asset Managerがデフォルトでレベルアセットを認識できているのはなぜ？
```cpp:Engine\Source\Runtime\Engine\Private\World.cpp
FPrimaryAssetId UWorld::GetPrimaryAssetId() const
{
	UPackage* Package = GetOutermost();

	if (!Package->HasAnyPackageFlags(PKG_PlayInEditor))
	{
		// Return Map:/path/to/map
		return FPrimaryAssetId(UAssetManager::MapType, Package->GetFName());
	}

	return FPrimaryAssetId();
}
```
理由は単純で、レベルアセットを管理する`UWorld`クラスにて`GetPrimaryAssetId`関数が実装されているからです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/9b783fb7-db85-d751-181b-403cb546a582.png)
ちなみに、ここで使用されているUAssetManager::MapTypeの中身は`const FPrimaryAssetType UAssetManager::MapType = FName(TEXT("Map"));`です。そのため、`Should Manager Determine Type and Name`におけるレベルアセットを管理する項目の`Primary Asset Type`はMapに設定されています。

### プロジェクト設定のAsset Manager/Should Manager Determine Type and Nameを有効にする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/6317dcf6-ac65-402d-8f5e-b3e0e8782237.png)
上図の赤枠の項目を有効にすることでもエラーを回避することができます。この項目を有効にすることで、先程の`GetPrimaryAssetId`関数が実装されていなくても、Asset Managerによる非同期ロードが動作するようになります。

**しかし、アセットの検索処理が追加されるため、`GetPrimaryAssetId`関数が実装されている場合に比べると処理負荷が増加します。** どのような処理が走っているか気になる方は `UAssetManager::DeterminePrimaryAssetIdForObject`をご確認くださいまし。

# ここまでのまとめ

- Asset ManagerによってPrimary Assetとして正しく管理されるためには、対象のクラスに`GetPrimaryAssetId`関数が実装されている必要がある
- `GetPrimaryAssetId`関数を追加するにはエンジンコードを編集する必要がある場合がある
- `GetPrimaryAssetId`関数を実装しなくても「いい感じに」してくれる機能はあるが、その分の負荷が加算されてしまう。

# 次回予告
「Asset Managerで非同期ロードするには対象のクラスに`GetPrimaryAssetId`関数が必要なのは分かったけど、対応するの凄く大変そう…」と感じていただけたかと思います…

そんなあなたのために、**`GetPrimaryAssetId`関数を実装しなくても
各アセットを効率よく非同期ロードできる仕組み「PrimaryDataAsset, PrimaryAssetLabel」が用意されています！** 
（この子達の良さを感じてもらうために、活用しない場合はどれ程大変なのかが今回の記事の趣旨でした）

次回の記事ではそれらの機能の使い方について解説しようと思います。

おわり