検証済バージョン:UE4.27
1. 導入
ゲーム開発において、現在ほとんど必須となっているのがパッチ対応です。パッチはゲームの不具合を修正したり、コンテンツの内容をアップデートする時に利用することを目的としており、ユーザーは更新に必要なデータをダウンロードすることで適用することができます。UE4では以下のドキュメントで示すようにパッチ作成機能をサポートしており、パックファイルとチャンク機能を利用することで実現しています。
[公式ドキュメント] パッチと DLC
https://docs.unrealengine.com/4.27/ja/SharingAndReleasing/Patching/
[公式ドキュメント] パッチの作成方法 (プラットフォーム非依存)
https://docs.unrealengine.com/4.27/ja/SharingAndReleasing/Patching/GeneralPatching/HowToCreatePatch/
上記ではプラットフォームに依存しないパッチの作成方法について記載されていますが、PCで作成する場合は Cooked Platformで指定するだけです。コンソールプラットフォームの場合は独自のアプリケーションパッケージフォーマットで配布する必要があり、プラットフォーム毎に作成方法が異なります。UE4から各プラットフォームのパッケージを作成することができますが、やっていることとしてはSDKに対する仲介を行うだけです。
2. 問題点
パッチに関する良くある問題として「パッチサイズが想定よりも大きい」というものがあります。ここでの「パッチサイズ」とは「パッチとして配布するアプリケーションパッケージ」のことを指します。
パッチはリリース時点から変更が行われた内容を配信するので、"変更が行われた差分のみ"のパッケージが作成できて配信できることが理想的です。以下はそれを図示したものですが、例えば「次のアップデートで新しいステージを追加したい」というようなパッチを作成したい場合、パッチパッケージはリリースからの変更分である「ステージ2のアセットのみ」がパッケージに含まれることを期待します。
一方こちらは問題となるようなケースで、上記と同様に「新しいステージのみを追加したい」という状況において、パッチパッケージには何故か変更を行っていないはずの初期データとステージ1のデータが含まれてしまい、結果としてパッチパッケージのサイズが大きくなることがあります。パッチパッケージのサイズが大きくなってしまうと、ユーザーがパッチをダウンロードする時間が長くなったり、ストレージを圧迫してしまうことによって満足度の低下につながります。
このような事が起きないようにリリース前に確認しておく必要があるのですが、ここからはそれが起きた時の対処方法などについて説明したいと思います。
3. 事前知識
差分パッチ
上記2章でも記載のように、パッチでは最小限の変更点を配信したいため、パッチとして配布するアプリケーションには必然的に差分のデータのみが含まれることになります。プラットフォーム毎にアプリケーションパッケージのフォーマットが異なるため、アプリケーションパッケージとして必要な共通のデータ量も異なりますが、基本的に差分として追加されるのは変更が発生したファイルのみです。これはバイナリレベルで異なるデータを検出してアプリケーションパッケージに含むということを意味しています。UE4の機能を利用してアプリケーションパッケージを作成する場合は、UE4が自動的に差分を検出してパッチに含めるように動作してくれます。
パッケージ作成フロー
アプリケーションパッケージが作成されるまでのビルドフローは以下の通りですが、パッチ作成の際に注目したいのが赤枠で囲っている部分です。パッチ(アプリケーションパッケージ)には最終的に実行ファイルとクックされたコンテンツを格納しますが、"ソースから実行ファイルを作成する処理"がビルド、"アセットをプラットフォームで利用可能なフォーマットにコンバートする処理"がクックで行われます。クックしたアセットを"ファイルアクセスを効率化するためにパックファイルを作成すること"がステージのプロセスで実行されます。
パッチに含まれるデータの基本情報
上記の2項で説明したように、UE4でパッケージを作成する場合は大きく分類して実行ファイルとアセットの2つがパッケージに含まれるデータです。もしコードやアセットを追加してパッチを作成するような場合には、これらのファイルに変更が発生すると認識をしておく必要があります。
以下は具体的なパッチで更新されるデータの例ですが、
ブループリントやマテリアルなどのアセットに変更がある場合はパックファイルが更新されます。
C++などのコードに変更がある場合は実行ファイル(.exe)が更新されます。パックファイルはコンテンツに更新が無くても"_X_P.pak"として更新ファイルを作成しますがアセットに変更が無い場合は更新する必要はありません。
これらは通常、同じプログラム同じアセットをパッケージ作成してもバイナリレベルで全く同じファイルが作成されることが理想的ですが、ごくまれにファイルが同一とならないことがあります。これについて調べる方法を次に示します。
4. 調査
パッチを作成した時にパッケージアプリケーションのサイズが思ったよりも大きい場合は次の事を行って調べる事をおすすめします。もしかしたらパッチ更新には不要なアセットがパッチに含まれたりすることで、パッチのサイズが肥大化してしまっているかもしれません。
方法1: アセットの差分があるかを調べる
2回クックを実行してアセットに差分があるかどうかを調べます。1回目のクックが完了した後に、クックアセットが保管される/Saved/Cooked
フォルダをコピーしておいてから再度クックを実行するだけです。問題がなければ1回目と2回目のクックでは同じ結果となり、差分のファイルが発生することはありません。以下の例は「2回クックした結果をWinMergeで比較したもの」ですが、差分が発生しているような状況です。通常は何度クックしても同じ結果となるべきなので、ここで1回目と2回目で差分が発生しているのは問題です。
方法2: 全アセット差分の詳細情報を取得
この方法は2回クックを行う方法1と似ていますが、「プロジェクト全体でどれくらいのアセットに差分があるかを調べたい時」や「どのようなデータに差分があるかを調べたい時」などに有用です。
UATから実行する場合は-additionalcookeroptions=-DiffOnly
を引数に追加してクックを行います。このオプションは、オプションを付けないケースのクック結果との差分を出力するので、最初に-additionalcookeroptions=-DiffOnly
オプションを付けずにクックを実行し、1回目が終わったら-additionalcookeroptions=-DiffOnly
オプションを続けてクックを行います。以下は.batファイルからプロジェクトをクックする際の差分クックを実行するコマンド例です。ProjectLauncherからクックを行う場合にはAdditional Cooker Option
に-DiffOnly
を付与します。
set ENGINE_PATH=D:\Release-4.27\Engine\Build\BatchFiles\RunUAT.bat
set PROJECT_PATH="C:\Projects\MyProject\MyProject.uproject"
%ENGINE_PATH% BuildCookRun -project=%PROJECT_PATH% -platform=Win64 -configuration=development -cook -additionalcookeroptions=-DiffOnly
2つのクック結果で差分が発生しているアセットのアセット名やプロパティ名をログに出力して表示します。以下は差分が発生した時のログの一例です。1回目のクックと2回目のクックで、GameMode Blueprint の bAllowTickBeforeBeginPlay
フラグを変更しています。ログが示すように1回目と2回目のクック結果に差分がある場合は、プロパティ名、クラス名、変更を検出したコールスタック、バイナリ差分を表示します。
LogArchiveDiff: Warning: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uasset: Size mismatch: on disk: 2732 vs memory: 2787
LogArchiveDiff: Warning: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uasset: 1513 difference(s) not logged (first at absolute offset: 24).
LogArchiveDiff: Warning: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uasset: NameMap is different (47 Names in source package vs 49 Names in dest package):
+[6] bAllowTickBeforeBeginPlay
+[8] BoolProperty
LogArchiveDiff: Warning: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uasset: ExportMap is different (5 Exports in source package vs 5 Exports in dest package):
-[1] ThirdPersonGameMode_C ThirdPersonGameMode.Default__ThirdPersonGameMode_C Super: NULL, Template: GameModeBase /Script/Engine.Default__GameModeBase, Flags: 49, Size: 37, PackageGuid: 00000000000000000000000000000000, PackageFlags: 0, ForcedExport: 0, NotForClient: 0, NotForServer: 0, NotAlwaysLoadedForEditorGame: 0, IsAsset: 0
+[1] ThirdPersonGameMode_C ThirdPersonGameMode.Default__ThirdPersonGameMode_C Super: NULL, Template: GameModeBase /Script/Engine.Default__GameModeBase, Flags: 57, Size: 63, PackageGuid: 00000000000000000000000000000000, PackageFlags: 0, ForcedExport: 0, NotForClient: 0, NotForServer: 0, NotAlwaysLoadedForEditorGame: 0, IsAsset: 0
LogArchiveDiff: Warning: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uexp: Size mismatch: on disk: 504 vs memory: 530
LogArchiveDiff: Warning: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uexp: Difference at offset 0 (absolute offset: 2787): byte 39 on disk, byte 41 in memory, callstack:
UE4Editor-CoreUObject.dll!operator<<() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\PropertyTag.cpp:95]
UE4Editor-CoreUObject.dll!UStruct::SerializeVersionedTaggedProperties() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp:1576]
UE4Editor-CoreUObject.dll!UStruct::SerializeTaggedProperties() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp:1287]
UE4Editor-CoreUObject.dll!UObject::SerializeScriptProperties() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Obj.cpp:1467]
UE4Editor-CoreUObject.dll!UObject::Serialize() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Obj.cpp:1387]
UE4Editor-CoreUObject.dll!UObject::Serialize() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Obj.cpp:1272]
UE4Editor-CoreUObject.dll!UStruct::Serialize() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp:1798]
UE4Editor-CoreUObject.dll!UClass::Serialize() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\Class.cpp:4404]
UE4Editor-Engine.dll!UBlueprintGeneratedClass::Serialize() [D:\Release-4.27\Engine\Source\Runtime\Engine\Private\BlueprintGeneratedClass.cpp:1766]
UE4Editor-CoreUObject.dll!UPackage::Save() [D:\Release-4.27\Engine\Source\Runtime\CoreUObject\Private\UObject\SavePackage.cpp:3357]
Serialized Object: BlueprintGeneratedClass/Game/ThirdPersonBP/Blueprints/ThirdPersonGameMode.ThirdPersonGameMode_C
DebugDataStack:
SerializeScriptProperties: BlueprintGeneratedClass
PropertySerialize: SimpleConstructionScript
LogArchiveDiff: Display: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uexp: Logging 128 bytes around absolute offset: 2732 (0000000000000AAC) in the on disk (existing) package, (which corresponds to offset 2787 (0000000000000AE3) in the in-memory package)
LogArchiveDiff: Display: 0000000000000A6C: FF FF FF FF 01 00 00 00 F4 FF FF FF 01 00 00 00 F7 FF FF FF EF FF FF FF 01 00 00 00 03 00 00 00
LogArchiveDiff: Display: 0000000000000A8C: F7 FF FF FF F6 FF FF FF EE FF FF FF 05 00 00 00 04 00 00 00 F5 FF FF FF ED FF FF FF 01 00 00 00
LogArchiveDiff: Display: 0000000000000AAC: 27 00 00 00 00 00 00 00 22 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 05 00 00 00 20 00 00
LogArchiveDiff: Display: 0000000000000ACC: 00 00 00 00 00 00 00 00 00 F8 FF FF FF 00 00 00 00 01 00 00 00 22 00 00 00 00 00 00 00 19 00 00
LogArchiveDiff: Display: C:/Projects/MyProject/Saved/Cooked/WindowsNoEditor/MyProject/Content/ThirdPersonBP/Blueprints/ThirdPersonGameMode.uexp: Logging 128 bytes around absolute offset: 2787 (0000000000000AE3) in the in memory (new) package
LogArchiveDiff: Display: 0000000000000AA3: FF FF FF FF 01 00 00 00 F4 FF FF FF 01 00 00 00 F7 FF FF FF EF FF FF FF 01 00 00 00 03 00 00 00
LogArchiveDiff: Display: 0000000000000AC3: F7 FF FF FF F6 FF FF FF EE FF FF FF 05 00 00 00 04 00 00 00 F5 FF FF FF ED FF FF FF 01 00 00 00
LogArchiveDiff: Display: 0000000000000AE3: 29 00 00 00 00 00 00 00 24 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 05 00 00 00 22 00 00
LogArchiveDiff: Display: 0000000000000B03: 00 00 00 00 00 00 00 00 00 F8 FF FF FF 00 00 00 00 01 00 00 00 24 00 00 00 00 00 00 00 1B 00 00
方法3: 1アセット差分の詳細情報を取得
この方法でも2回クックを行いますが、「特定のアセットに差分が発生している状態で他のファイルに依存しないことを確認したい時」や「全てのクックは時間が掛かるので1つのアセットに絞って調べる時」、「クック問題を修正した後に確認したい時」などに有用です。
UATから実行する場合は-additionalcookeroptions=-cooksinglepackage -diffonly
を引数に追加してクックを行います。以下はバッチファイルから1つのアセットだけの差分を比較して詳細情報を表示する例です。ASSET_PATH
には再クック時に変更を加えていなくても変更に含まれてしまうファイルを指定します。
set ENGINE_PATH=D:\Release-4.27\Engine\Build\BatchFiles\RunUAT.bat
set PROJECT_PATH="C:\Projects\TP_427\TP_427.uproject"
set ASSET_PATH=/Game/Mannequin/Animations/ThirdPersonRun
%ENGINE_PATH% BuildCookRun -Project=%PROJECT_PATH% -platform=Win64 -clientconfig=development -cook -ini:Engine:[ConsoleVariables]:cook.displaymode=2 -additionalcookeroptions="-cooksinglepackage -diffonly" -map=%ASSET_PATH%
方法4: Visual Studioでデバッグしながら詳細情報を追跡する
この方法は「Visual Studioから起動しながらクックを実行して差分が発生したタイミングでブレークを張りたい時」などに有用です。これを利用することでより具体的なコールスタックや条件を絞ることができます。
Visual Studioから実行する場合は-diffonlybreakoffset
を引数に追加してクックを行います。以下はVisual Studioから起動してクックを行い差分が発生したポイントでブレークする例です。Visual Studioから実行する際には以下のようなコマンドを引数として追加して起動します。
C:\Projects\TP_427\TP_427.uproject -run=cook -targetplatform=WindowsNoEditor -ini:Engine:[ConsoleVariables]:cook.displaymode=2 -cooksinglepackage -diffonly -diffonlybreakoffset=<AbsoluteOffset> -map=/Game/Mannequin/Animations/ThirdPersonRun
方法5: パックファイルから差を特定する
IoStoreを利用していない場合
ゲーム中に使用するアセットは.pakファイルに纏められます。そのためUnrealPakを使用して.pakファイルの比較を行います。
@rem UnrealPakパス
set UNREAL_PAK_PATH=D:\UnrealEngine-4.27-plus\Engine\Binaries\Win64\UnrealPak.exe
@rem 比較するパックA(.pak)ファイルパス
set PAK_A="C:\Projects\TP_427\PakA\WindowsNoEditor\TP_427\Content\Paks\TP_427-WindowsNoEditor.pak"
@rem 比較するパックB(.pak)ファイルパス
set PAK_B="C:\Projects\TP_427\PakB\WindowsNoEditor\TP_427\Content\Paks\TP_427-WindowsNoEditor.pak"
%UNREAL_PAK_PATH% -diff %PAK_A% %PAK_B%
差分情報は \Engine\Programs\UnrealPak\Saved\Logs
に出力されます。以下は WB_Test
というアセットに差分があることを示しています。
LogPakFile: FileEventType, FileName, Size1, Size2
LogPakFile: FilesizeDifferent, TP_427/Content/Widget/WB_Test.uasset, 6271, 6019
LogPakFile: FilesizeDifferent, TP_427/Content/Widget/WB_Test.uexp, 4297, 3931
LogPakFile: Comparison complete
IoStoreを利用している場合
ゲーム中に使用するアセットは.utocファイルに纏められます。そのためCommandletを使用して.utocファイルの比較を行います。
@rem UnrealPakパス
set UNREAL_PAK_PATH=D:\UnrealEngine-4.27-plus\Engine\Binaries\Win64\UE4Editor-Cmd.exe
@rem 比較するパックAまでのディレクトリパス
set UTOC_A="C:\Projects\TP_427\PakA\WindowsNoEditor\TP_427\Content\Paks"
@rem 比較するパックBまでのディレクトリパス
set UTOC_B="C:\Projects\TP_427\PakB\WindowsNoEditor\TP_427\Content\Paks"
@rem 結果の出力先
set OUT_PATH="C:\Projects\TP_427\Pak\DumpLog.log"
%UNREAL_PAK_PATH% -run=IoStore -diff -list -Source=%UTOC_A% -Target=%UTOC_B% -DumptoFile=%OUT_PATH%
出力結果は指定のファイルとして保存されます。こちらの場合、パックファイル毎の統計のみを出力し、差分が発生したファイルについては出力しません。
------------------------------ Container Diff Summary ------------------------------
Source path 'C:\Projects\TP_427\PakA\WindowsNoEditor\TP_427\Content\Paks'
Target path 'C:\Projects\TP_427\PakB\WindowsNoEditor\TP_427\Content\Paks'
Source container file(s):
Container Size (MB) Chunks
-------------------------------------------------------------------------
global 1.01 3
TP_427-WindowsNoEditor 36.46 362
-------------------------------------------------------------------------
Total of 2 container file(s) 37.47 365
Target container file(s):
Container Size (MB) Chunks Unmodified Unmodified (MB) Modified Modified (MB) Added Added (MB) Removed Removed (MB)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
global 1.01 3 3 (100.00%) 1.01 (100.00%) 0 (0.00%) 0.00 (0.00%) 0 (0.00%) 0.00 (0.00%) 0/3 (0.00%) 0.00 (0.00%)
TP_427-WindowsNoEditor 36.46 362 360 (99.45%) 36.45 (99.98%) 2 (0.55%) 0.01 (0.02%) 0 (0.00%) 0.00 (0.00%) 0/362 (0.00%) 0.00 (0.00%)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Total of 2 container file(s) 37.47 365 363 37.46 2 0.01 0 0.00 0 0.00
5. 原因と対策
この問題が発生すると考えられる主な原因と対処方法です。これらについては新しい情報があれば追加したいと思います。
原因 | 対策 |
---|---|
アセットバージョンによる差分 | サンプルコンテンツなどアセットが古いバージョンから更新されないようなケースで稀に見られます。バージョン更新と共にシリアライズデータの変更などが反映されておらず変更が発生することがあります。アセットの再保存で対応可能です。 |
コンストラクターのランダム処理 | コンストラクター(Construction Scriptも含む)でランダム値を使用してデータを保存するようなケースでは発生することがあります。これはクック時にコンストラクタが実行されてクック毎に保存するデータが変わるためです。ランダム性を削除するか、ランダムシードに基づいたランダム性を使用して、常に同じになるようにします。 |
アセットの格納 | アセットの格納される順番が不均一なことによって変更が発生する可能性があります。オープンオーダーを使用することで、アセットがコンテナに格納される順番を固定することで解決することがあります。 |
エンジンまたはプロジェクトの不具合 | 何らかの要因でクック毎にデータが不一致となることがあります。殆どがシリアライズデータの不一致によるものですが、プロジェクト側での問題ではない場合はスタッフにご報告ください。 |
6. その他
差分処理
パックファイルの差分比較PakFileUtilities.cpp
の RemoveIdenticalFiles()
で実行されます。LogPakFile
のログを有効にしておくことで、パッケージの時点で差分があった際にファイルを確認することができます。これは圧縮したパックファイルを解凍する手間を省略することができます。
UE_LOG(LogPakFile, Display, TEXT("Source file %s matches dest file %s and will not be included in patch"), *SourceFilename, *DestFilename);
UE_LOG(LogPakFile, Display, TEXT("Source file size for %s %d bytes doesn't match %s %d bytes, did find %d"), *SourceFile, SourceTotalSize, *DestFilename, DestTotalSize, Hash ? 1 : 0);
7. 既知の問題
AssetRegistry.bin に差分が発生する
現在以下の変更で対応が可能です。
https://github.com/EpicGames/UnrealEngine/commit/6b69c8a5d92754689e6fe516c7ae4b38df090b3a
BulkDataInfo.ubulkmanifest に差分が発生する
現在以下のチケット内の回避策が利用可能です。
https://issues.unrealengine.com/issue/UE-138846
xxx.ushaderbytecode, xxx.assetinfo.json に差分が発生する
現在 bShareMaterialShaderCode=False (デフォルトTrue)に変更することで対応可能です。