发表于 2020-01-15 09:41 | 更新于 2020-06-04 23:49 | 分类于 UE4 , HotPatch | 字数统计 6.2k
HotPatcher是我最近写的用于打包UE项目资源热更的工具,用于追踪工程版本的资源变动来打出Patch。支持一键Cook多平台,一键打包多平台Patch,编辑器支持Windows和MacOS,再写一套从服务器下载patch的流程就是一套完整的游戏热更方案。HotPatcher在项目里已经使用了一段时间,目前比较稳定,今天整理了一下文档,开源出来,会持续更新,欢迎提issus。
HotPatcher与UnrealFrontEnd中的Patch不同,UE的Patch是基于Cook出的资产作为版本管理的内容,这样在管理工程时存在问题:同样的工程版本,很难在不同的电脑上打出相同的Patch(除非把Cooked同步提交,很难管理),也无法基于Patch的版本再打出一个Patch,而且我们还需要能够方便地能够把外部文件打包到pak中(如lua文件),并且方便管理工程和Patch版本,这个插件就是为了解决上面这样的问题。
目前支持的引擎版本为UE4.21-UE4.25,最近有很多朋友私信来问插件相关的问题,我创建了个群来讨论UE4热更新和HotPatcher插件的问题(QQ群958363331),欢迎加入。
注意:该插件只能打出包含UE的资源以及添加的外部文件作为热更的内容(支持lua),修改的C++代码无法热更,需要自己设计好架构。如果是纯蓝图项目则可以完全使用这个工具进行热更。
HotPatcher插件支持根据选择只打包指定的资源(可以指定包含过滤器以及忽略过滤器以及可以指定单个资源),支持资源的递归依赖分析(如只打包某个地图及其所有依赖的资源),打包的Path中可以选择不包含未引用的资源,也不会包含未改动的资源,还会分析项目中的无效资产以及忽略重定向器的资源,实现了基于项目资产的版本追踪而无需管理Cooked的内容,只需要在打Patch之前Cook一遍保证Cook的内容是基于最新工程版本的即可。而且还支持Chunk、支持bMonilithic
模式,配置化的方式支持随意组合,支持多线程打包、支持导出资源的依赖关系,而且还支持通过Commandlet来执行Cook和Patch的生成,可以随意组合自己的出包流程。
除了包含UE的资源文件外,额外的支持:
插件的其他功能:
UrealPak.exe
,提高执行效率。同时,我也写了一个批量Cook的工具,用于一键Cook指定的多个平台(当然使用命令行也可以),目的就是使用最少的步骤完成任务。
我录了一个插件使用说明的视频(汗,第一次录视频还有点紧张):
如果不能翻墙可以看B站的链接:UE4热更新:HotPatcher插件使用教程
为了方便版本管理,项目一定要使用某种版本控制工具,建议Git.
*_Release.json
,其中记录了所指定的每个资源的信息;*_Release.json
为基础版本,根据需求选择你需要打到Pak中的内容;GeneratedPatch
,会生成Pak文件和各种信息(其中也包含当前版本的*_Release.json
,使Patch可以增量更新)HotPatcher中的Cook部分是为了打Patch时方便Cook多个平台,以及方便指定地图(插件会扫描整个项目中的所有地图并列出),不用每次Cook都使用命令行。
Cook可选的分为Platforms
/Map(s)
/Settings
:
Platforms
:选择Cook 的平台,可以多选;Map(s)
:选择要Cook的Map,该选项下会列出当前工程里所有的Map,可以多选(代码里我也提供了列出引擎和插件目录下Map的选项,如果需要可以在HotPatcherEditor.build.cs
中通过控制ENABLE_COOK_ENGINE_MAP
和ENABLE_COOK_PLUGIN_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操作导出的是一个json的文件,记录了导出时所选择的每个Asset资源的HASH值,基于此HASH值我们可以在后续的Patch中知道该资源是不是被修改了。
Release Settings
PakList*.txt
文件。在不通过指定
PakList*.txt
导出Release的情况下,导出的release.json中只包含项目中有引用的资源,没有引用的不会包含。但是引擎默认打包时会把一些没有引用的资源也打包到里面,这就会导致一些资源在基础包中事实上已经存在了,但是在release.json中没有记录,有用到这些资源的话会导致重复包含(使用时没有问题,只是这些资源在pak中会有两份)。使用PakList*.txt
就不会存在这个问题。
namespace EAssetRegistryDependencyType { enum Type { // Dependencies which don't need to be loaded for the object to be used (i.e. soft object paths) Soft = 0x01, // Dependencies which are required for correct usage of the source asset, and must be loaded at the same time Hard = 0x02, // References to specific SearchableNames inside a package SearchableName = 0x04, // Indirect management references, these are set through recursion for Primary Assets that manage packages or other primary assets SoftManage = 0x08, // Reference that says one object directly manages another object, set when Primary Assets manage things explicitly HardManage = 0x10, // Note: Also update FAssetRegistryDependencyOptions when adding more flags }; static const Type None = (Type)(0); static const Type All = (Type)(Soft | Hard | SearchableName | SoftManage | HardManage); static const Type Packages = (Type)(Soft | Hard); static const Type Manage = (Type)(SoftManage | HardManage); } |
Soft
是具有FSoftObjectReference
的资源引用,Hard
是直接的资源引用,如果想要Soft和Hard都包含,可以选择Package
,这个选项默认情况下选Package
就好,除非你自己来处理资源引用关系。
AddExternFileToPak
和AddExternDirectoryToPak
对应的是Project Settings
-Packing
中的Additional Non-Asset Directories To Package
等设置,用于标记本地包中所包含的非资源文件。
SaveTo
VersionId
的文件夹,所有的文件在此文件夹中。注意:导出Release时,资源的包含需要与UE直接打包时的设置一致,因为这里导出的Release是记录使用UE直接打出的包所包含的资源。
ByPatch是真正执行打包出Pak的工具,可以基于之前导出的版本(通过ByRelease导出的Json文件),也可以包含外部文件/文件夹、配置文件(ini)、Cook出的非资源文件(AssetRegistry.bin等),并且可以指定多个平台,支持输入UnrealPak的参数,可以导出当前Patch的各种信息。
支持指定资源过滤器:
HotPatcher打Patch的选项解析:
BaseVersion
false
,则只打包选择的过滤器文件(依然会分析依赖)和添加的外部文件,若为true
则必须要指定一个基础版本,否则无法执行Patch。同时该属性也会控制是否生成Diff
信息,若为false
则不生成Diff
(没有基础版本diff也无意义)。ByRelease
或者上次一的Patch生成,默认为*_Release.json
。PatchSettings
Release
中描述的含义相同。Release
中描述的含义相同。。AssetRegistry.bin
文件。GlobalShaderCache-*.bin
文件。PROJECT_NAME\Content\ShaderArchive*.ushaderbytecode
文件。DefaultEditor*.ini
)AddExternFileToPak的元素要求:
FilePath
需要指定所选文件的路径。MountPath
为该文件被打包到Pak中的挂载路径,默认是../../../PROJECT_NAME/
下。在游戏运行时可以通过FPaths::ProjectDir
来访问。
如,AAAA.json的MountPath
为../../../HotPatcherExample/AAAAA.json
,在运行时加载的路径:
FPaths::Combine(FPaths::ProjectDir(),TEXT("AAAAAA.json")); |
Pak中的所有文件可以通过IPlatformFile
来访问。
AddExternDirectoryToPak:结构的数组,添加外部文件夹到Pak中,该结构第一个参数(DirectoryPath
)为指定系统中的文件夹路径,第二个参数(Mount Point
)指定该路径在Pak中的挂载路径;指定文件夹下的所有文件会被递归包含,并且挂载路径均相对于所指定的MountPoint
。
IncludePakVersionFile:是否在当前打出的Patch中存储版本信息。(已废弃)
IncludePakVersionFile
控制是否可以编辑,用于指定*_PakVersion.json
文件在Pak文件中的挂载点,默认为../../../PROJECT_NAME/Extention/Versions
。(已废弃)ChunkOptions
我为HotPatch增加了Chunk的功能,支持把一个Patch中的资源分别打包到不同的Pak中去。如把一个Patch中的所有Package
打包到一个pak里(或者把当前Patch的指定地图打包到某个pak里),所有的lua打包到另一个pak里。
Chunk支持的参数如下:
bEnableChunk
:是否对当前的Patch执行ChunkChunkInfos
:Chunk信息的列表,用于指定Chunk中应该包含哪些资源。ChunkInfos
的是一个FChunkInfo
的结构数组,其结构的成员为:
ChunkName
:当前Chunk的名字bMonolithic
:是否为当前Chunk里的每个资源打出Pak(已支持),在开启bMonolithic
模式时,会禁止添加外部文件和ini/Cook的内部数据,不然存放路径会有些歧义。MonolithicPathMode
:单片模式的pak存储路径模式,MountPath
是Mount
的方式,PackagePath
是资源的LongPackageName
的路径。bSavePakCommands
:是否存储当前Chunk的PakCommand.txt
AssetIncludeFilters
:指定Chunk中的资源过滤器(注意所选过滤器中的资源不会进行依赖分析)AssetIgnoreFilters
:指定Chunk的忽略过滤器AssetRegistryDependencyTypes
:与Release
中描述的含义相同。IncludeSpecifyAssets
:指定Chunk中包含哪些单个资源,支持进行依赖分析AddExternFileToPak
:添加外部文件到ChunkAddExternDirectoryToPak
:添加外部目录到Chunk,支持递归分析InternalFiles
:用于指定Chunk中是否包含AssetRegistry.bin
和Ini
等文件基本上Chunk的参数和Patch选择的参数的一致,可以按照需求配置。
注意:Chunk的原理是:先在Patch的设置中选择资源,会执行一遍整个Patch的资源分析,分析出来的结果用于Chunk配置的过滤,意思就是Chunk中所包含的资源必须是本次Patch的中具有的,一定要注意AssetIncludeFilters不会进行依赖分析,如果你Patch中的过滤器中的资源引用到了其他模块,比如引擎或者插件,在Chunk中也需要指定相应的目录到过滤器。
每个Chunk中所指定的资源可以重复,一份资源可以在ChunkA中包含也可以被ChunkB包含。
而且我还给Chunk增加了错误提示,如果开启了Chunk模式,但是Patch中的所有资源没有在Chunk中全部指定,会在HotPatcher底部的Infomations
中提示有哪些资源没有在Chunk中被包含,方便进行错误处理。
bMonolithic模式:
mount point
路径PakOptions
*PakCommand.txt
中每条要打包到pak中资源的的参数。
UnrealPak.exe
程序的参数。如果什么都不指定,默认的配置为UnrealPak.exe PAKFILE.pak -create=PAK_LIST.txt
。SaveTo
UnrealPak.exe
的-Create
参数文件。VersionId
的文件夹,所有的文件在此文件夹中。我给插件提供了两个Commandlet,分别是HotCooker
和HotPatcher
,用于Cook和打Patch,使用方法:
Cooker使用方法:
UE4Editor.exe PROJECT.uproject -run=HotCooker -config="cook-config.json" |
在编辑器中导出cook配置的时候如果选中了所有地图,会在导出的配置文件里会把bCookAllMap为true,在Cook的Commandlet里执行就会Cook所有地图了,而且只要bCookAllMap为true就会Cook所有地图,和CookMaps里的地图数量没关系。
Patcher使用方法:
UE4Editor.exe PROJECT.uproject -run=HotPatcher -config="patch-config.json" |
他们的-config
参数所接收的文件都可以从编辑器中通过插件导出。
2020.06.04 Update
d1194b , 12 commits behind
87 commits
d1194b, referenced in this article
11 commits
3abfaa, latest
bCookAllMap
属性,当为true时,在Commandlet模式下会Cook工程目录下中所有的地图。PakList*.txt
来生成配置,从而可以完全匹配基础包中的所有资源。2020.06.03 Update
2d9552 , 18 commits behind
81 commits
2d9552, referenced in this article
17 commits
3abfaa, latest
2020.04.27 Update
f660157 , something is wrong
N/A commits
f660157, referenced in this article
N/A commits
N/A, latest
Up-to-date
bMonolithic
模式下的MonolithicPathMode
,用于指定单片模式下的pak存储路径规则。2020.04.24 Update
2020.04.23 Update
2020.04.18 Update
UnrealOptions
默认添加-compressionformats
参数。PreviewChunk
的选项,在开启Chunk的情况下可以方便地知道哪个chunk中有哪些资源。2020.04.10 Update
ReplacePakCommandTexts
选项,可以自己根据需求替换PakCommand.txt中的文本,如替换打到pak中的资源的MountPath
2020.04.09 Update
PakCommand
参数的选项2020.04.08 Update
ExecuteUnrealPak
而不是启动UnrealPak.exe
,极大提升效率。2020.04.07 Update
bMonolithic
模式,可以把所有引用每个资源单独打到一个pak中。
2020.04.03 Update
IncludePakVersionFile
和PakVersionFileMountPoint
选项2020.04.02 Update
FPatcherSpecifyAsset
序列化与反序列化的问题2020.03.28 Update
*Release.json
不全的问题。2020.03.05 Update
HotPatcherEditor.build.cs
中通过控制ENABLE_COOK_ENGINE_MAP
和ENABLE_COOK_PLUGIN_MAP
的值来自行开启。2020.02.14 Update
2019.12.08 Update
2020.02.19 Update
2020.01.13 Update
2020.01.14 Update
IncludeSpecifyAssets
,可以指定某个资源了(如只指定某个地图)。
2020.01.19 Update
HotPatcher开源之后有一些朋友陆续找我单独聊到一些问题,有些朋友可能会遇到相同的问题,这里用于收集一些Q&A的内容。
这是由于打出本体包的时候在项目设置中设置了Signing
加密,需要在HotPatcher中的UnrealPak参数中添加相同的加密参数。
在IPlatformFilePak.cpp
中的RegisterPakFile
中,同样做了判断:
// 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; } |
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才可以被加载。
相关的代码处理在:
// 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
文件:
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
文件了,把pak
和sig
文件一同拷贝到挂载目录里就可以了。
UnrealPak的参数可以看我之前的一篇文章:UE4工具链配置与开发技巧#UnrealPak的参数
为了使HotPatcher更易用和更强大,我会在这里记录一些不错的更新建议。
XX_TARGET_PLATFORM_P.pak
改成XX_TARGET_PLATFORM_0_P
或者中间的数字可以指定。Chunk
的bMonolithic
模式增加多线程支持Commandlet
的支持本文会持续更新HotPatcher的文档。
扫描二维码,分享此文章
本文标题:UE4资源热更打包工具HotPatcher
文章作者:ZhaLiPeng
发布时间:2020年01月15日 09时41分
更新时间:2020年06月04日 23时49分
本文字数:本文一共有6.2k字
原始链接:https://imzlp.me/posts/17590/
专栏链接:https://zhuanlan.zhihu.com/p/103743690/
许可协议: CC BY-NC-SA 4.0
捐赠BTC:1CbUgUDkMdy6YRmjPJyq1hzfcpf2n36avm
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!
# 虚幻引擎 # UE4 # 热更新 # UnrealEngine # 补丁包
选择语言▼
© 2014 - 2020 | 424.6k
Github Pages & Hexo Deploy