UE4资源热更打包工具HotPatcher

HotPatcher是我最近写的用于打包UE项目资源热更的工具,用于追踪工程版本的资源变动来打出Patch。支持一键Cook多平台,一键打包多平台Patch,编辑器支持Windows和MacOS,再写一套从服务器下载patch的流程就是一套完整的游戏热更方案。HotPatcher在项目里已经使用了一段时间,目前比较稳定,今天整理了一下文档,开源出来,会持续更新,欢迎提issus。

HotPatcher与UnrealFrontEnd中的Patch不同,UE的Patch是基于Cook出的资产作为版本管理的内容,这样在管理工程时存在问题:同样的工程版本,很难在不同的电脑上打出相同的Patch(除非把Cooked同步提交,很难管理),也无法基于Patch的版本再打出一个Patch,而且我们还需要能够方便地能够把外部文件打包到pak中(如lua文件),并且方便管理工程和Patch版本,这个插件就是为了解决上面这样的问题。

注意:该插件只能打出包含UE的资源以及添加的外部文件作为热更的内容(支持lua),修改的C++代码无法热更,需要自己设计好架构。如果是纯蓝图项目则可以完全使用这个工具进行热更。

HotPatcher插件支持根据选择只打包指定的资源(可以指定包含过滤器以及忽略过滤器以及可以指定单个资源),支持资源的递归依赖分析(如只打包某个地图及其所有依赖的资源),打包的Path中可以选择不包含未引用的资源,也不会包含未改动的资源,还会分析项目中的无效资产以及忽略重定向器的资源,实现了基于项目资产的版本追踪而无需管理Cooked的内容,只需要在打Patch之前Cook一遍保证Cook的内容是基于最新工程版本的即可。

除了包含UE的资源文件外,额外的支持:

  • 支持包含Cooked出的非资源文件如AssetRegistry.bin/GlobalShaderCache*.bin/ShaderBytecode*.ushaderbytecode
  • 支持包含引擎、项目、和插件的ini文件;
  • 支持包含外部的文件夹和文件(如lua文件,视频文件等),并且可以自定义挂载点,供运行时访问;
  • 打出的pak中可以不包含任何UE的资源文件(只要在HotPatcher中不添加任何资源即可),方便只更新lua的代码或者只更新Ini之类的配置文件;

插件的其他功能:

  • 支持基于上一个patch版本再打出一个patch版本;
  • 支持检测未cook的资源;
  • 支持版本间的diff,可以看到新增、修改、删除(但是删除的资源在之前的版本的pak中是无法删除的,只是diff展示用)的资源信息;
  • 支持检测重复的文件包含;
  • 支持导出该插件所有的中间生成信息和配置;
  • 支持自定义UnrealPak的参数;
  • 支持同时打出多个平台的patch;

同时,我也写了一个批量Cook的工具,用于一键Cook指定的多个平台(当然使用命令行也可以),目的就是使用最少的步骤完成任务。

插件使用流程

为了方便版本管理,项目一定要使用某种版本控制工具,建议Git.

  1. 打开HotPatcher,选择ByRelease,导出*_Release.json,其中记录了所指定的每个资源的信息;
  2. 使用UE直接打包任意平台的项目(如windows/android/ios)
  3. 在工程中修改/添加/删除资源,修改引擎/项目/插件的设置等;
  4. 打开HotPatch,在Cook项选择你要打Patch的平台并执行Cook;
  5. 打开HotPatcher,选择ByPatch,以上面导出的*_Release.json为基础版本,根据需求选择你需要打到Pak中的内容;
  6. 点击GeneratedPatch,会生成Pak文件和各种信息(其中也包含当前版本的*_Release.json,使Patch可以增量更新)
  7. 生成的Pak中就包含了与上次打出的包中所有差异的内容。

Cook参数说明

HotPatcher中的Cook部分是为了打Patch时方便Cook多个平台,以及方便指定地图(插件会扫描整个项目中的所有地图并列出),不用每次Cook都使用命令行。

Cook可选的分为Platforms/Map(s)/Settings

  • Platforms:选择Cook 的平台,可以多选;
  • Map(s):选择要Cook的Map,该选项下会列出当前工程里所有的Map,可以多选;
  • Filter(s):可以选择只Cook指定的目录,可以添加多个;
  • Settings:选择Cook设置,默认提供了Iterator/UnVersioned/CookAll/Compressed四个选项,我也提供了OtherOptions可以自己指定要Cook 的参数。

注意:Map(s)中的地图和Settings中的CookAll必须要选中其中的一个或者Filters中具有指定的目录才可以执行Cook,如果只选了CookMap,则只会Cook该Map所引用到的资源,没有被引用的不会被Cook,这个需要注意。

点击CookContent之后会将Cook的log输出到UE的OutputLog中。

ByRelease参数说明

ByRelease操作导出的是一个json的文件,记录了导出时所选择的每个Asset资源的HASH值,基于此HASH值我们可以在后续的Patch中知道该资源是不是被修改了。

  • VersionId:指定当前导出的资源信息是什么版本。
  • IncludeFilter:当前Release扫描哪些目录下的资源。
  • IgnoreFilter:当前Release忽略哪些目录下的资源。
  • IncludeHasRefAssetsOnly:对选中的过滤器中的资源文件进行依赖分析(递归分析至Map),如果资源没有被引用则不会打包到Patch中。
  • IncludeSpecifyAssets:结构的数组,可以指定当前Release中的单个资源,该结构的第一个参数需要指定资源,第二个参数控制是否分析并包含当前指定资源的依赖到pak中。

注意:导出Release时,资源的包含需要与UE直接打包时的设置一致,因为这里导出的Release是记录使用UE直接打出的包所包含的资源。

ByPatch参数说明

ByPatch是真正执行打包出Pak的工具,可以基于之前导出的版本(通过ByRelease导出的Json文件),也可以包含外部文件/文件夹、配置文件(ini)、Cook出的非资源文件(AssetRegistry.bin等),并且可以指定多个平台,支持输入UnrealPak的参数,可以导出当前Patch的各种信息。

支持指定资源过滤器:

HotPatcher打Patch的选项解析:

  • bByBaseVersion:是否是基于某个基础版本的Patch,若为false,则只打包选择的过滤器文件(依然会分析依赖)和添加的外部文件,若为true则必须要指定一个基础版本,否则无法执行Patch。同时该属性也会控制是否生成Diff信息,若为false则不生成Diff(没有基础版本diff也无意义)。
  • BaseVersion:该选项应选择Patch所基于的版本文件,可以ByRelease或者上次一的Patch生成,默认为*_Release.json
  • VersionId:当前Patch的版本的ID
  • IncludeFilter:当前Patch扫描哪些目录下的资源变动。
  • IgnoreFilter:当前Patch忽略哪些目录下的资源变动。
  • IncludeHasRefAssetsOnly:对选中的过滤器中的资源文件进行依赖分析(递归分析至Map),如果资源没有被引用则不会打包到Patch中。
  • IncludeSpecifyAssets:结构的数组,可以指定需要打到Pak中的单个资源,该结构的第一个参数需要指定资源,第二个参数控制是否分析并包含当前指定资源的依赖到pak中。
  • IncludeAssetRegistry:在当前的Patch打出的Pak中包含AssetRegistry.bin文件。
  • IncludeGlobalShaderCache:在当前的Patch打出的Pak中包含GlobalShaderCache-*.bin文件。
  • IncludeShaderBytecode:在当前Patch打出的Pak中包含PROJECT_NAME\Content\ShaderArchive*.ushaderbytecode文件。
  • IncludeEngineIni:在当前打出的Patch中包含引擎目录下的ini,也会包含平台相关的ini
  • IncludePluginIni:在当前打出的Patch中包含所有启用的插件中的ini(引擎目录和项目目录的插件都会包含)
  • IncludeProjectIni:在当前的Patch打出的Pak中包含项目的ini文件(不会包含DefaultEditor*.ini
  • bEnableExternFilesDiff:是否对添加的外部文件进行Diff比对,用于只打包修改或者新增的外部文件。
  • AddExternFileToPak:添加外部的非资源文件到Pak中,如txt、视频。

AddExternFileToPak的元素要求:

  1. FilePath需要指定所选文件的路径。
  2. MountPath为该文件被打包到Pak中的挂载路径,默认是../../../PROJECT_NAME/下。

在游戏运行时可以通过FPaths::ProjectDir来访问。
如,AAAA.json的MountPath../../../HotPatcherExample/AAAAA.json,在运行时加载的路径:

1
FPaths::Combine(FPaths::ProjectDir(),TEXT("AAAAAA.json"));

Pak中的所有文件可以通过IPlatformFile来访问。

  • AddExternDirectoryToPak:结构的数组,添加外部文件夹到Pak中,该结构第一个参数(DirectoryPath)为指定系统中的文件夹路径,第二个参数(Mount Point)指定该路径在Pak中的挂载路径;指定文件夹下的所有文件会被递归包含,并且挂载路径均相对于所指定的MountPoint

  • IncludePakVersionFile:是否在当前打出的Patch中存储版本信息。

  • PakVersionFileMountPoint:由IncludePakVersionFile控制是否可以编辑,用于指定*_PakVersion.json文件在Pak文件中的挂载点,默认为../../../PROJECT_NAME/Extention/Versions

如果选择了在Pak存储版本信息,则可以使用FPakHelper::LoadVersionInfoByPak来读取Pak中的版本信息,可以用在Pak文件验证上。

Pak的版本信息为以下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
USTRUCT(BlueprintType)
struct FPakVersion
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(EditAnywhere,BlueprintReadWrite)
FString VersionId;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
FString BaseVersionId;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
FString Date;
UPROPERTY(EditAnywhere,BlueprintReadWrite)
FString CheckCode;
};

其中:VersionId为Pak的版本Id,BaseVersionId为该Pak的基础版本ID,Date为Pak创建的日期,CheckCode的值默认为把VersionId_BaseVersionId_Date拼接到一块进行的SHA1编码。

  • UnrealPakOptions:该数组为UnrealPak.exe程序的参数。如果什么都不指定,默认的配置为UnrealPak.exe PAKFILE.pak -create=PAK_LIST.txt.

  • PakTargetPlatforms:该数组为选择要打出的Patch的平台,可以多选,一定要注意所选的平台已经被Cook。

  • SavePakList:是否存储UnrealPak.exe-Create参数文件。

  • SaveDiffAnalysis:是否存储当前的Patch版本与Base版本的差异信息。

  • SavePatchConfig:是否存储当前Patch的所有选项信息。

  • SavePath:本次的Patch信息存储位置。会在当前目录下创建出名字为VersionId的文件夹,所有的文件在此文件夹中。

Update Log

2020.02.14 Update

  • 增加了Patch和Release配置的导入导出
  • 生成Pak的默认命名改成XXX_TARGET_PLATFORM_001_P.pak
  • 修复了一些潜在问题,建议更新。

2019.12.08 Update

  • 新增是否对资源进行依赖扫描的选项,增加diff预览。

2020.02.19 Update

  • 为插件增加了导出release时可以选择添加外部文件和文件夹,对应ue打包时添加的非资源外部文件目录。还支持外部文件的diff(可选),对于指定的外部文件和目录,会检测只有修改或者新增的文件才会打包到pak中。

2020.01.13 Update

  • 新增生成时的错误信息提示,对未Cook资源的扫描以及重复添加的外部文件进行检测,增加可以指定特定资源到Pak中,并且可选是否对该资源进行依赖分析。

2020.01.14 Update

  • 修复会扫描到Redirector会提示Redirector的资源未Cooked,为ExportRelease增加IncludeSpecifyAssets,可以指定某个资源了(如只指定某个地图)。

2020.01.19 Update

  • 增加指定要Cook的目录

Q&A

HotPatcher开源之后有一些朋友陆续找我单独聊到一些问题,有些朋友可能会遇到相同的问题,这里用于收集一些Q&A的内容。

  1. 使用HotPatcher打包出来的pak在挂载时Crash并具有Pak master signature table check failed for pak提示

这是由于打出本体包的时候在项目设置中设置了Signing加密,需要在HotPatcher中的UnrealPak参数中添加相同的加密参数。

IPlatformFilePak.cpp中的RegisterPakFile中,同样做了判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Runtime/PakFile/Private/
uint16* RegisterPakFile(FName File, int64 PakFileSize)
{
uint16* PakIndexPtr = CachedPaks.Find(File);
if (!PakIndexPtr)
{
FString PakFilename = File.ToString();
check(CachedPakData.Num() < MAX_uint16);
IAsyncReadFileHandle* Handle = LowerLevel->OpenAsyncRead(*PakFilename);
if (!Handle)
{
return nullptr;
}
CachedPakData.Add(FPakData(Handle, File, PakFileSize));
PakIndexPtr = &CachedPaks.Add(File, CachedPakData.Num() - 1);
UE_LOG(LogPakFile, Log, TEXT("New pak file %s added to pak precacher."), *PakFilename);

FPakData& Pak = CachedPakData[*PakIndexPtr];

if (SigningKey.IsValid())
{
// Load signature data
FString SignaturesFilename = FPaths::ChangeExtension(*PakFilename, TEXT("sig"));
IFileHandle* SignaturesFile = LowerLevel->OpenRead(*SignaturesFilename);
ensure(SignaturesFile);
FArchiveFileReaderGeneric* Reader = new FArchiveFileReaderGeneric(SignaturesFile, *SignaturesFilename, SignaturesFile->Size());
Pak.Signatures.Serialize(*Reader);
delete Reader;
Pak.Signatures.DecryptSignatureAndValidate(SigningKey, PakFilename);

// Check that we have the correct match between signature and pre-cache granularity
int64 NumPakChunks = Align(PakFileSize, FPakInfo::MaxChunkDataSize) / FPakInfo::MaxChunkDataSize;
ensure(NumPakChunks == Pak.Signatures.ChunkHashes.Num());
}
}
return PakIndexPtr;
}
  1. 在本体包中开启signature后,打包出来的Pak无法被挂载
    同样是pak的signature的错误,是因为没有为pak生成对应的.sig文件。
    Log中的内容如下:
1
2
3
LogPakFile: Warning: Couldn't find pak signature file '../../../Pak/Content/Paks/1.0.3_WindowsNoEditor_P.pak'
LogPakFile: Warning: Unable to create pak "../../../Pak/Content/Paks/1.0.3_WindowsNoEditor_P.pak" handle
LogPakFile: Warning: Failed to mount pak "../../../Pak/Content/Paks/1.0.3_WindowsNoEditor_P.pak", pak is invalid

这是因为打出本体包时Project Setting-Crypto中的bEnablePakSigning被设置成了true,这样对打出来的包里的所有pak都会执行校验,目的就是为了确保只有自己打包的pak才可以被加载

相关的代码处理在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Runtime/PakFile/Private/SignedArchiveReader.cpp
FChunkCacheWorker::FChunkCacheWorker(FArchive* InReader, const TCHAR* Filename)
: Thread(nullptr)
, Reader(InReader)
, QueuedRequestsEvent(nullptr)
, ChunkRequestAvailable(nullptr)
{
FString SigFileFilename = FPaths::ChangeExtension(Filename, TEXT("sig"));
FArchive* SigFileReader = IFileManager::Get().CreateFileReader(*SigFileFilename);

if (SigFileReader == nullptr)
{
UE_LOG(LogPakFile, Fatal, TEXT("Couldn't find pak signature file '%s'"), *SigFileFilename);
}

Signatures.Serialize(*SigFileReader);
delete SigFileReader;
Signatures.DecryptSignatureAndValidate(Filename);

const bool bEnableMultithreading = FPlatformProcess::SupportsMultithreading();
if (bEnableMultithreading)
{
QueuedRequestsEvent = FPlatformProcess::GetSynchEventFromPool();
ChunkRequestAvailable = FPlatformProcess::GetSynchEventFromPool();
Thread = FRunnableThread::Create(this, TEXT("FChunkCacheWorker"), 0, TPri_BelowNormal);
}
}

所以,如果在用HotPatcher打包pak时没有与项目指定相同的加密参数,则导致放入包内的pak会加载失败(因为验证失败了)。
解决的办法就是,在使用HotPatcher时指定与项目相同的加密信息,当直接使用UE打出本体包时,会默认在下列路径中生成一个Crypto.json文件:

1
PROJECT_DIRECTORY\Saved\Cooked\WindowsNoEditor\PROJECT_NAME\Metadata\Crypto.json

它里面的内容是根据Project Setting-Crypto中的选项生产的。
使用方法为:
在HotPatcher的UnrealPak参数项添加参数:-cryptokeys="Crypto.json"(在UE4.23+中还需要添加-sign参数):

重新生成Pak就会在Pak的目录里生成与Pak同名的.sig文件了,把paksig文件一同拷贝到挂载目录里就可以了。

UnrealPak的参数可以看我之前的一篇文章:

更新计划

为了使HotPatcher更易用和更强大,我会在这里记录一些不错的更新建议。

  1. 增加支持多线程扫描资源依赖分析,目前在工程内资源数量相当大的情况下(数万个),资源的递归依赖分析会比较慢,开多个线程可以显著减少耗时。
  2. 在UnrealPak下增加直接指定Pak加密参数的选项(其实开放了可以指定UnrealPak的参数,自己指定加密的参数即可,只是麻烦了一点)
  3. 指定Cook单个资源
  4. 把打包出的Pak命名从XX_TARGET_PLATFORM_P.pak改成XX_TARGET_PLATFORM_0_P或者中间的数字可以指定。
  5. Patch和Release配置文件的导入和导出、以及快速地清理配置。
  6. 为release增加包含外部文件的选项,与UE的打包时包含外部文件同步。
  7. 增加对外部文件的Diff,不用每次打包时把所有的外部文件都打包到pak中。
本文会持续更新HotPatcher的文档。

扫描二维码,分享此文章

本文标题:UE4资源热更打包工具HotPatcher
文章作者:ZhaLiPeng
发布时间:2020年01月15日 09时41分
更新时间:2020年01月28日 23时40分
本文字数:本文一共有3.9k字
原始链接:https://imzlp.me/posts/17590/
专栏链接:https://zhuanlan.zhihu.com/p/103743690/
许可协议: CC BY-NC-SA 4.0
捐赠BTC:1CbUgUDkMdy6YRmjPJyq1hzfcpf2n36avm
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!