UnrealEngine マップやキャラクターの裏読み方法　～アセットロードのすすめ～

環境：UE4.27.2
# はじめに
裏読みをする前にロードの処理を見直しておきましょう。
C++で組む時、アセットを以下のようにロードしてないですか？

```cpp
StaticLoadObject(USkeletalMesh::StaticClass(), nullptr, TEXT("/Game/Character/Mesh"));
```
アセットをロードしてくれるとても便利な機能ですね。
ですが、このままだと色々問題があります。

- アセットのパスが変わるとコードも書き換えないといけない
- 参照が付かないためパッケージにアセットが含まれずエラーになる場合がある
- **アセットのロードはとても重く、ヒッチの原因になります**

まずはこれらの問題がおきないように対処してみましょう。

# パスが変わっても大丈夫、パッケージにも含まれるようにする
### 絶対に必要なアセットの場合
ハード参照でBPに直接アセットをセットしてしまいましょう。
BPのロードと一緒にロードされるのでStaticLoadObjectする必要がなくなります。

```cpp
UPROPERTY(EditDefaultsOnly)
USkeletalMesh* CustomMesh;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/fa613a68-ec80-b95d-a19c-65b5e474a64c.png)

### 場合によって必要なかったり、使い分ける場合
ソフトオブジェクトポインタTSoftObjectPtr<>を使って必要なものだけLoadSynchronous()してやるとよいでしょう。

```cpp
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<USkeletalMesh> CustomMeshA;

UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<USkeletalMesh> CustomMeshB;
```
```cpp
USkeletalMesh* CustomMesh;
if(条件)
{
    CustomMesh = CustomMeshA.LoadSynchronous();
}
else
{
    CustomMesh = CustomMeshB.LoadSynchronous();
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/2b091252-5281-fd8c-a984-3b708de47af0.png)

### アセットの数が沢山あって手動で設定するの面倒なんだけど
UPrimaryDataAssetを継承したクラスを作り、UpdateAssetBundleData関数で登録してあげると良いでしょう。

```cpp:PrimaryAssetFoobar.h
UCLASS()
class UPrimaryAssetFoobar : public UPrimaryDataAsset
{
	GENERATED_BODY()

public:
	virtual FPrimaryAssetId GetPrimaryAssetId() const override { return FPrimaryAssetId(TEXT("Foobar"), GetFName()); }

#if WITH_EDITORONLY_DATA
	virtual void UpdateAssetBundleData() override;
#endif

public:
    // Keyにそのままアセット名を使っているけど本当はアセットパスの一部を抜き出してId化したほうがいい
	UPROPERTY(VisibleAnywhere)
	TMap<FName, TSoftObjectPtr<USkeletalMesh>> SkeletalMeshes;
};
```
```cpp:PrimaryAssetFoobar.cpp
void UPrimaryAssetFoobar::UpdateAssetBundleData()
{
	Super::UpdateAssetBundleData();
	
	if (!UAssetManager::IsValid()) { return; }

	UAssetManager& Manager = UAssetManager::Get();

	TArray<FAssetData> DirectoryAssets;
	FName PackagePath = FName(*FPackageName::GetLongPackagePath(GetOutermost()->GetName()));
	Manager.GetAssetRegistry().GetAssetsByPath(PackagePath, DirectoryAssets, true);
	
	for (const FAssetData& AssetData : DirectoryAssets)
	{
		FSoftObjectPath AssetRef = Manager.GetAssetPathForData(AssetData);
		if (AssetRef.IsNull()) { continue; }

		// スケルタルメッシュを登録
		if (AssetData.GetClass()->IsChildOf(USkeletalMesh::StaticClass()))
		{
			SkeletalMeshes.Emplace(AssetData.AssetName, AssetRef);
		}
	}
}
```

先程作ったクラスを使ってデータアセットを作ります。
置き場所はロードしたいアセットがあるディレクトリ、またはその上のディレクトリになるように置きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/8bc7ce45-444e-10ca-a91e-a450dd6e5e74.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/8ac2e2a5-eed3-9a2e-6853-9867436419e6.png)

保存ボタンを押すと自動でアセットをセットしてくれます。
以降はアセットが増えてもプレイするときやこのアセットを開いた時に自動的に追加してくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/32ef0350-159f-bf81-fca8-437340bb3d0d.png)

後はこのPrimaryAssetをロードして、その中から必要なアセットをLoadSynchronous()してあげればOKです。

# アセットロード時にヒッチが発生しないように事前に裏読みする
裏読みとはゲーム中に次に必要なデータを事前にロードしておく処理のことです。
今回はキャラとマップについて説明します。

### キャラの場合
AssetManagerの機能を使います。ロードできる単位はUWorldとPrimaryAssetです。
なので、さっき説明したPrimaryAssetを活用することにしましょう！
ただ、今のままだと全キャラロードしてしまうため部分的にロードできるように改造します。
アセットバンドルという機能を使えるように1行追加しましょう。（Unityのそれとは別物ですｗ）

```cpp:PrimaryAssetFoobar.cpp
// スケルタルメッシュを登録
if (AssetData.GetClass()->IsChildOf(USkeletalMesh::StaticClass()))
{
	SkeletalMeshes.Emplace(AssetData.AssetName, AssetRef);

    // 追加！　バンドルにアセットのパスを追加
	AssetBundleData.AddBundleAsset(AssetData.AssetName, AssetRef);
}
```

このようにバンドル名を設定してロードするとそのバンドルのアセットだけを非同期でロードしてくれます。
```cpp
const FPrimaryAssetId AssetId(TEXT("Foobar"), TEXT("Primaryアセット名"));

TArray<FName> LoadBundles;
LoadBundles.Add(TEXT("SK_Mannequin"));

UAssetManager::Get().LoadPrimaryAsset(AssetId, LoadBundles);
```

おっと、プロジェクト設定に追加するのを忘れてはいけません。
スキャンするプライマリアセットタイプに新しいタイプを追加しましょう。
- PrimaryAssetTypeはGetPrimaryAssetId()で作った第一引数の文字（例だと"Foobar"）を指定
- Directorysは検索するディレクトリ
- CookRuleは確実にパッケージに含めてほしいのでAlwaysCookにしておいたほうが安全

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/5b779943-7e7f-8a63-abce-875c6d1069bd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/c37163e9-0585-1410-fe39-92aec69c99be.png)

ロードされてるかどうかは以下のコマンドでチェックできます。
AssetManager.DumpLoadedAssets
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/60fdb685-98ac-4a5a-2a9e-38d37acca1fd.png)

### マップの場合
スキャンするプライマリアセットタイプのMapのところを修正する必要があります。
- **Is Editor Onlyのチェックを外す！！**
- Directoriesにマップを置いてあるディレクトリを追加（必要あれば）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/c84e5f89-9e0c-9fd0-ab92-4d995d664206.png)

後はロードする処理を書きます。
これだけ！アセット名はフルパスでないと駄目っぽいので注意
```cpp
UAssetManager::Get().LoadPrimaryAsset(FPrimaryAssetId(TEXT("Map"), TEXT("/Game/Maps/NewWorld")));
```

ですが、これだとパーシスタントレベルはロードされるのですがサブレベルがロードされません。
サブレベルもロードされるようにAssetManagerを改良していきましょう。

```cpp:CustomAssetManager.h
UCLASS()
class UCustomAssetManager : public UAssetManager
{
	GENERATED_BODY()

public:
	static UCustomAssetManager& Get();

	void LoadLevel(FName AssetName);
private:
	void LoadSubLevel(TSharedPtr<FStreamableHandle> Handle);
};
```
```cpp:CustomAssetManager.cpp
UCustomAssetManager& UCustomAssetManager::Get()
{
	if (UCustomAssetManager* Singleton = Cast<UCustomAssetManager>(GEngine->AssetManager)) { return *Singleton; }
	return *NewObject<UCustomAssetManager>();
}

void UCustomAssetManager::LoadLevel(FName AssetName)
{
	const TSharedPtr<FStreamableHandle>& Handle = LoadPrimaryAsset(FPrimaryAssetId(TEXT("Map"), AssetName));
	if (Handle.IsValid() == false) return;

    // パーシスタントレベルのロードが終わったらサブレベルをロードする
	if (Handle->HasLoadCompleted())
	{
		LoadSubLevel(Handle);
	}
	else
	{
		Handle->BindCompleteDelegate(FStreamableDelegate::CreateUObject(this, &UCustomAssetManager::LoadSubLevel, Handle));
	}
}

void UCustomAssetManager::LoadSubLevel(TSharedPtr<FStreamableHandle> Handle)
{
	const UWorld* PersistentLevel = Cast<const UWorld>(Handle->GetLoadedAsset());
	if (PersistentLevel == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("パーシスタントレベルの裏読みに失敗") );
		return;
	}

	for (const ULevelStreaming* SubLevel : PersistentLevel->GetStreamingLevels())
	{
		LoadPrimaryAsset(FPrimaryAssetId(TEXT("Map"), SubLevel->GetWorldAssetPackageFName()));
	}
}
```
次にプロジェクト設定で先程作ったクラスを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2778561/714e24c1-420a-a7d5-200c-dcfa56cc515b.png)

使うときはこう
```
UCustomAssetManager::Get().LoadLevel(TEXT("/Game/Maps/NewWorld"));
```

ロードの完了イベントが必要な場合はUCustomAssetManagerを適時改造してやると良いでしょう。
# 最後に
裏読み中にブロックロードすると非同期の裏読みも含めてブロックロードしてめちゃくちゃ長時間固まります。その場合は

"FlushAsyncLoading:なんとか"

というログが出るのでログが出てる所にブレークを貼り原因をとなっているブロックロードを探します。
そのアセットも裏読み対応するか、個別にAsyncLoadにしてブロックロードが同時に発生しないように頑張りましょう！

（追記）
後で分かったのですがPIEの"Play Standarone"で起動した後、別プロセスのDedicatedServerに接続して
上記のやりかたでレベルを裏読みしてSeamlessTravelするとクラッシュするようです。多分エンジンのバグ。

以上。

# 参考記事
https://qiita.com/EGJ-Kaz_Okada/items/f18bca3fb5c8fc1aea9c

https://qiita.com/EGJ-Kaz_Okada/items/7dba130c3641aa456b73

https://qiita.com/EGJ-Kaz_Okada/items/4616c0a21b8e9842a14a