UE4 チャンク機能(pak分割)についてのメモ

# 概要
UnrealEngine4のコンテンツpakファイルを分割するチャンク機能についてのメモです。
モバイルアプリだけではなくDLCとかでも多分使います。

## 更新履歴
| 日付 | 内容 |
|:----|:-----|
|2021/07/13| 手動設定について記述修正 |

## 参考
以下の記事を参考にいたしました、ありがとうございます。
[UE4公式：クック処理とチャンク化](https://docs.unrealengine.com/ja/SharingAndReleasing/Patching/GeneralPatching/CookingAndChunking/index.html)
[UE4公式：チャンク用のアセットを準備する](https://docs.unrealengine.com/ja/SharingAndReleasing/Patching/GeneralPatching/ChunkingExample/index.html)
[UE4公式：チャンク化による高速ダウンロードを可能にするツールと最適化の考察](https://www.unrealengine.com/ja/tech-blog/optimizing-battle-breakers-for-chunked-downloading)
[UE4のモバイル開発におけるコンテンツアップデートの話　- Chunk IDとの激闘編 - ](https://www.slideshare.net/EpicGamesJapan/ue4-chunk-id)
[[UE4] uasset以外のアセットにChunkIDを設定する(ue4.22以降)](https://qiita.com/EGJ-Takashi_Suzuki/items/d7e980d7a71e9adb6857)


## 環境
Windows10
Visual Studio 2017
UnrealEngine 4.26


# Chunkについて
DLCなどアセットの大きな単位をチャンク(Chunk)と呼んでいて、ChunkIDを設定したものが同じパックファイル(.pak)としてクック時にまとめられます。
ChunkID = 0 はデフォルトパッケージでDLCなどの追加分はChunkIDを1以降に設定します。

## プロジェクト設定
[プロジェクト設定] → [プロジェクト/パッケージ化]
で [pakファイルを使用][チャンクの生成]を有効にする必要があります。

![ProjectSetting.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/90f8cf29-0699-951e-3c1a-ab2d865f1f2a.jpeg)


## パッケージ作成時にpakファイルができるフォルダ
以下のフォルダに
Saved\StagedBuilds\{Platform}\{ProjectName}\Content\Paks\

pakchunk0-{Platform}.pak
pakchunk1-{Platform}.pak
pakchunk2-{Platform}.pak
...

という名前で作成されるようです。

## pakファイルのファイルリスト確認
以下のフォルダに
Saved\TmpPackaging\{Platform}\

pakchunk0.txt
pakchunk1.txt
pakchunk2.txt
...

という名前でchunk毎にファイルリストが作成されるようです。


# ChunkIDの設定方法
## 手動設定
[エディタの環境設定] -> [実験段階] -> [ユーザーインターフェース] -> [チャンクIDの割り当てを許可]
にチェックを入れます。
![AllowChunkIDAssignments.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/ad410a67-7a76-f989-bca4-42ecb5335d33.png)

アセットを右クリックから
[アセットアクション] -> [チャンクに割り当てる] -> [チャンクIDを入力]

![ChunkId_Manual.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/afd3a9f8-00bb-720c-0d9d-e3004e544612.png)

複数まとめての設定もできます。


## PrimaryAssetLabel を使った設定
コンテンツブラウザから右クリック -> [その他] -> [データアセット] を選択し、[データアセットクラスを選択]にて `PrimaryAssetLabel` を選択することでアセットラベルを作成でき、個のアセットにてチャンクIDの設定をすることができます。

![Sel_PrimaryAssetLabel.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/4c1a2093-27a6-630e-f327-dd8f27bb5c27.jpeg)

▼ 関連ソース
"Engine\Source\Runtime\Engine\Classes\Engine\DataAsset.h"
"Engine\Source\Runtime\Engine\Private\DataAsset.cpp"
"Engine\Source\Runtime\Engine\Classes\Engine\PrimaryAssetLabel.h"
"Engine\Source\Runtime\Engine\Private\PrimaryAssetLabel.cpp"

![PrimaryAssetLabel.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/3bc485fc-d3f0-dd0e-9622-b824941ff67c.jpeg)

上記のように`Chunk ID`を `1`にして、`Label Asset in My Directory` のチェックを入れるとこのアセットが置かれたフォルダ以下全てのアセットに対しチャンクID=1 が設定されます。（重複がない場合）


## PrimaryAssetLabel を自前で改造して使う例
`UPrimaryAssetLabel` は `UPrimaryDataAsset` を継承したクラスなので、更に継承して（あるいは`UPrimaryDataAsset`を継承して）プロパティを追加したり処理を修正することが可能です。
以下のクラスは `bLabelAssetsInMyDirectory` をチェックすると自身のアセットが置かれているフォルダ以下全てのチャックIDを設定するところを更に `ExcludeMyDirectory` を設定することによって対象を除去する機能をつけてみたクラスです。

`UPrimaryAssetLabel`クラスとの違いは、
1.対象外ディレクトリ指定用の`ExcludeMyDirectory`を追加
2.プライマリアセットタイプの追加(`AssetType`)とその関連メソッド(`GetIdentifierString()`, `GetPrimaryAssetId()`)の追加
です。


```cpp:MyPrimaryAssetLabel.h
#pragma once

#include "Engine/DataAsset.h"
#include "Engine/AssetManagerTypes.h"
#include "Engine/EngineTypes.h"
#include "MyPrimaryAssetLabel.generated.h"

// 参照されたアセットをこのプライマリアセットの一部としてマークするために作成されるシードファイル
UCLASS()
class TESTAPP_API UMyPrimaryAssetLabel : public UPrimaryDataAsset
{
	GENERATED_BODY()

public:
	// ディレクトリタグ付きアセットに使用されるバンドル名
	static const FName DirectoryBundle;

	// コレクションアセットに使用されるバンドル
	static const FName CollectionBundle;

public:
	// コンストラクタ
	UMyPrimaryAssetLabel();
	
	// プライマリアセットID名を取得する
	UFUNCTION(BlueprintCallable, Category = "MyPrimaryAssetLabel")
		FString GetIdentifierString() const;

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

	// プライマリアセットタイプ
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "MyPrimaryAssetLabel")
		FPrimaryAssetType AssetType;

	// この特定のアセットの管理ルールが設定されている場合、タイプルールが上書きされます
	UPROPERTY(EditAnywhere, Category = "Rules", meta = (ShowOnlyInnerProperties))
	FPrimaryAssetRules Rules;

	// このディレクトリとサブディレクトリ内のすべてにラベルを付けるにはTrue
	UPROPERTY(EditAnywhere, Category = "PrimaryAssetLabel")
	uint32 bLabelAssetsInMyDirectory : 1;

	// このディレクトリからラベルを付けないディレクトリ
	UPROPERTY(EditAnywhere, Category = "PrimaryAssetLabel", meta = (EditCondition=bLabelAssetsInMyDirectory, RelativeToGameContentDir, LongPackageName))
	TArray<FDirectoryPath> ExcludeMyDirectory;

	// ラベルアセット自体をクックして実行時に使用できるようにする必要がある場合は、trueに設定します。
	// これは、ラベルが付けられているアセットには影響しません。クックルールで設定されます
	UPROPERTY(EditAnywhere, Category = "PrimaryAssetLabel")
	uint32 bIsRuntimeLabel : 1;

	// ラベル付けする手動で指定されたアセットのリスト
	UPROPERTY(EditAnywhere, Category = "PrimaryAssetLabel", meta = (AssetBundles = "Explicit"))
	TArray<TSoftObjectPtr<UObject>> ExplicitAssets;

	// ラベルを付けるために手動で指定したブループリントアセットのリスト
	UPROPERTY(EditAnywhere, Category = "PrimaryAssetLabel", meta = (AssetBundles = "Explicit", BlueprintBaseOnly))
	TArray<TSoftClassPtr<UObject>> ExplicitBlueprints;

	// アセット参照をロードするためのコレクション
	UPROPERTY(EditAnywhere, Category = "PrimaryAssetLabel")
	FCollectionReference AssetCollection;

	// これがクックされたビルドで利用できない場合にのみエディターに設定します
	virtual bool IsEditorOnly() const
	{
		return !bIsRuntimeLabel;
	}

#if WITH_EDITORONLY_DATA
	// これにより、クラスでアセットプロパティのAssetBundlesメタデータがスキャンされ、InitializeAssetBundlesFromMetadataを使用してAssetBundleDataが初期化されます。
	virtual void UpdateAssetBundleData();
#endif

};
```

　
`UPrimaryAssetLabel`クラスとの違いは、
1. `DirectoryAssets` を回すfor文の中で対象外のディレクトリをチェックする処理を追加（除外パスのチェック処理が文字列を含むか`Contains`のチェックしかやっていないので厳密なチェックではないことに注意）
2. コンストラクタでプライマリアセットタイプの設定
3. プライマリアセットID関連メソッドの追加
です。

```cpp:MyPrimaryAssetLabel.cpp
#include "MyPrimaryAssetLabel.h"
#include "Engine/DataAsset.h"
#include "Engine/EngineTypes.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
#include "Engine/AssetManager.h"
#include "AssetRegistryModule.h"

#if WITH_EDITOR
#include "CollectionManagerTypes.h"
#include "ICollectionManager.h"
#include "CollectionManagerModule.h"
#endif

const FName UMyPrimaryAssetLabel::DirectoryBundle = FName("Directory");
const FName UMyPrimaryAssetLabel::CollectionBundle = FName("Collection");

// コンストラクタ
UMyPrimaryAssetLabel::UMyPrimaryAssetLabel()
{
	bLabelAssetsInMyDirectory = false;
	bIsRuntimeLabel = false;

	// By default have low priority and don't recurse
	Rules.bApplyRecursively = false;
	Rules.Priority = 0;

	AssetType = TEXT("MyPrimaryAssetLabel");
}

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

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

#if WITH_EDITORONLY_DATA
// アセットバンドルデータの更新
void UMyPrimaryAssetLabel::UpdateAssetBundleData()
{
	Super::UpdateAssetBundleData();

	if (!UAssetManager::IsValid())
	{
		return;
	}

	UAssetManager& Manager = UAssetManager::Get();
	IAssetRegistry& AssetRegistry = Manager.GetAssetRegistry();

	// アセットが置かれているディレクトリ以下を含むか？
	if (bLabelAssetsInMyDirectory)
	{
		FName PackagePath = FName(*FPackageName::GetLongPackagePath(GetOutermost()->GetName()));

		TArray<FAssetData> DirectoryAssets;
		AssetRegistry.GetAssetsByPath(PackagePath, DirectoryAssets, true);
		
		TArray<FSoftObjectPath> NewPaths;

		for (const FAssetData& AssetData : DirectoryAssets)
		{
			// 除去ディレクトリチェック
			// ＃除外パス文字列を含むかどうかしか調べていないのでフォルダ構成によっては問題がある
			bool _IsExclude = false;
			FString _PakPath = AssetData.PackagePath.ToString();
			for(auto& _ExcludeDir : ExcludeMyDirectory){
				if( _PakPath.Contains(_ExcludeDir.Path) ){
					_IsExclude = true;
					break;
				}
			}
			if(_IsExclude)continue;
			
			FSoftObjectPath AssetRef = Manager.GetAssetPathForData(AssetData);

			if (!AssetRef.IsNull())
			{
				NewPaths.Add(AssetRef);
			}
		}

		// Fast set, destroys NewPaths
		AssetBundleData.SetBundleAssets(DirectoryBundle, MoveTemp(NewPaths));
	}

	if (AssetCollection.CollectionName != NAME_None)
	{
		TArray<FSoftObjectPath> NewPaths;
		TArray<FName> CollectionAssets;
		ICollectionManager& CollectionManager = FCollectionManagerModule::GetModule().Get();
		CollectionManager.GetAssetsInCollection(AssetCollection.CollectionName, ECollectionShareType::CST_All, CollectionAssets);
		for (int32 Index = 0; Index < CollectionAssets.Num(); ++Index)
		{
			FAssetData FoundAsset = Manager.GetAssetRegistry().GetAssetByObjectPath(CollectionAssets[Index]);

			FSoftObjectPath AssetRef = Manager.GetAssetPathForData(FoundAsset);

			if (!AssetRef.IsNull())
			{
				NewPaths.Add(AssetRef);
			}
		}

		// Fast set, destroys NewPaths
		AssetBundleData.SetBundleAssets(CollectionBundle, MoveTemp(NewPaths));
	}
	
	// Update rules
	FPrimaryAssetId PrimaryAssetId = GetPrimaryAssetId();
	Manager.SetPrimaryAssetRules(PrimaryAssetId, Rules);
}
#endif

```

また作成したクラスは、[プロジェクト設定] -> [ゲーム/アセットマネージャ―] -> [スキャンするプライマリアセットタイプ]へ`PrimaryAssetLabel`と同じように追加登録が必要です。
![PrimaryAssetType_Setting.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/35eec631-82a8-9d68-7057-bbc8f476571b.jpeg)


アセットは以下の様になります。`ExcludeMyDirectory`設定が追加されています。
![MyPrimaryAssetLabel.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/64be74db-4d70-5ceb-ef6a-5466a26abe6a.jpeg)

不要な設定を除去するとすっきりしていいかもしれません。



## ChunkIDの親子設定
`DefaultEngine.ini`に対して親子関係を設定することができます。
チャンク1を親として、チャンク100と101が子になっている設定例

```ini:DefaultEngine.ini
[/Script/UnrealEd.ChunkDependencyInfo]
+DependencyArray=(ChunkID=100,ParentChunkID=1)
+DependencyArray=(ChunkID=101,ParentChunkID=1)
```


# ChunkIDの確認方法
## AssetAuditを使う
[ウィンドウ] -> [デベロッパーツール] -> [アセットの監査]
で AssetAudit ウィンドウが開き、[チャンクを追加]にてチャンクIDとディスクサイズ等が確認できるようです。
![AssetAudit.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303806/745f8055-226c-f4d3-f483-3058795a24db.jpeg)

右クリックから[参照ビューワ]も使えますが、アセット数が多いとわけわからないのでちょっと使いにくいと思います。

パッケージ作成した場合に `Saved\TmpPackaging\{Platform}\` フォルダにチャンクごとのファイルリストが作られるのでこれを見るのが一番確実かもしれません。


# まとめ
アセットが大量だとチャンクIDが正しく設定されているのかどうかの確認がかなり大変でフォルダ分けなどが考慮されているといいのですが、そうはいかないケースが多いと思います。（途中で入れる内容が変わったり）
なのでチャンクIDの確認が（AssetAuditよりも）より簡単にできるといいのですが、例えばコンテンツブラウザなどでフィルター表示などができれば便利かもしれません。:smile: