HotPatcher项目开源这一年多以来,经过了不少的更新和优化,也被越来越多的开发者选择作为自己项目的热更新方案,期间有不少人陆陆续续询问 UE4 热更新相关遇到的问题,很多问题比较常见,重复询问的频率也比较多,所以我准备把一些常见的问题进行整理,方便初步上手 UE4 热更新方案的人能够尽快地排查问题。
本篇文章会持续更新 UE4 热更新和 HotPatcher 相关的 Q&A 内容,有疑问的地方也可以直接在本篇文章中评论,我会定期统一回答和整理,也可以加入我的 UE4 热更新群讨论遇到的问题(QQ 群 958363331)。
HotPatcher 相关问题
是否可以用在商业项目中?
可以,使用的是 MIT 开源协议。是否可以热更 C++?
不能,只能用来更新 uasset 和 Non-Asset(lua/db/json 等等)。支持移动端热更吗?
支持,本身 HotPatcher 是没有平台限制的,可以打包和管理 UE 支持的任意平台。
注意:使用 HotPatcher 打包时,需要避免一个目录既包含 uasset 又包含 non-asset 的情况,不然会导致未被 cook 的 uasset 打包。
热更新系列文章
我写的 UE4 热更新的系列文章,可以作为工程实践的参考:
- UE4 热更新:需求分析与方案设计
- UE4 资源热更打包工具 HotPatcher
- UE4 热更新:基于 HotPatcher 的自动化流程
- 2020 Unreal Open Day
- UE4 热更新:拆分基础包
- UE4 热更新:资产管理与审计工具
- UE4 热更新:Create Shader Patch
- UE4 热更新:Questions & Answers
pak 的自动挂载目录
以下三个路径中的 Pak 会在引擎启动时自动挂载:
- Engine/Content/Paks
- GAME_DIR/Content/Paks
- GAME_DIR/Saved/Paks
1 | void FPakPlatformFile::GetPakFolders(const TCHAR* CmdLine, TArray<FString>& OutPakFolders) |
这三个路径下的 pak 的默认优先级不同(除非通过_1_P.pak 这种形式命名):
1 | int32 FPakPlatformFile::GetPakOrderFromPakFilePath(const FString& PakFilePath) |
Mount Point 的作用
在 Mount Pak 的时候,有一个参数可以指定 MountPoint:
1 | /** |
那么它是干什么的呢?
首先从 Mount 函数开始:
1 | if (InPath != NULL) |
如果在调用 Mount 时传递了 InPath
,则通过加载 Pak 的 FPakFile 实例调用SetMountPoint
,把 InPath 设置给它。
其实在 FPakFile 中,MountPath 是有默认值的(从 Pak 文件中读取),在 FPakFile 的构造函数中调用了 Initialize(Reader, bLoadIndex);
,Initialize 中又调用了LoadIndex
,在LoadIndex
中从 Pak 中读取 Pak 的 Mount Point 的逻辑:
1 | // Runtime/PakFile/Private/IPlatformFilePak.cpp |
简单的可以理解为:如果 Mount 时不传递 Mount Point 就会从 Pak 文件中读取,如果有传入就设置为传入的值(Pak 文件中的 MountPoint 是 Pak 中所有文件的公共路径)。
那么,给 Pak 设置 MountPoint 的作用是什么呢?
真实目的是,检测要加载的文件是否存在于当前 Pak 中!因为 Pak 的 Mount Point 的默认含义是当前 Pak 中所有文件的公共路径,所以只需要检测要读取的文件是否以这个路径开头,就可以首先排除掉基础路径不对的文件(基础路径都不对,意味着这个文件在 Pak 中也不存在)。
具体逻辑可以看这个函数的实现:
1 | // Runtime/PakFile/Public/IPlatformFilePak.h |
当我们从 Pak 中读取文件时,通过对游戏中所有 Mount 的 Pak 调用 Find
函数,而 FPakFile::Find
的函数就实现了上述我说的逻辑:
1 | // Runtime/PakFile/Private/IPlatformFilePak.cpp |
所以,MountPoint 的作用就是在从 Pak 中查找文件时,首先判断文件的路径是否与 Pak 中所有文件的 基础路径 相匹配(StartWith),如果不存在也就不会进入后续的流程了。
Pak 无法被挂载
在本体包中开启 signature 后,打包出来的 Pak 无法被挂载
同样是 pak 的 signature 的错误,是因为没有为 pak 生成对应的.sig 文件。
Log 中的内容如下:
1 | LogPakFile: Warning: Couldn't find pak signature file '../../../Pak/Content/Paks/1.0.3_WindowsNoEditor_P.pak' |
这是因为打出本体包时 Project Setting
-Crypto
中的 bEnablePakSigning
被设置成了 true,这样对打出来的包里的所有 pak
都会执行校验,目的就是为了确保 只有自己打包的 pak 才可以被加载。
相关的代码处理在:
1 | // Runtime/PakFile/Private/SignedArchiveReader.cpp |
所以,如果在用 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
文件了,把 pak
和sig
文件一同拷贝到挂载目录里就可以了。
UnrealPak 的参数可以看我之前的一篇文章:UE4 工具链配置与开发技巧 #UnrealPak 的参数
Pak master signature table check failed for pak
- 使用 HotPatcher 打包出来的 pak 在挂载时 Crash 并具有 Pak master signature table check failed for pak 提示
这是由于打出本体包的时候在项目设置中设置了 Signing
加密,需要在 HotPatcher 中的 UnrealPak 参数中添加相同的加密参数。
在 IPlatformFilePak.cpp
中的 RegisterPakFile
中,同样做了判断:
1 | // Runtime/PakFile/Private/ |
iOS 热更 metallib 问题
在 4.25 存在不会重新加载 shaderbytecode 的问题,而且引擎内部对加载 metallib 是单独处理的流程,无法服用 usahderbytecode 的流程,所以出 iOS 包尽量使用远程打包的方式,会生成ushaderbytecode
,在 4.25 里 LoadLibrary 没有问题,但是如果去加载 metallib 就有问题。
UE 热更 Shader 相关的内容可以看之前的文章:UE4 热更新:Create Shader Patch
UE4.25+ ShaderPatch Crash
这是因为在 4.25+ 引擎内部的 bug 导致的,UE4 热更新:Create Shader Patch#4.25+ ShaderPatch Crash](https://imzlp.com/posts/5867/)这篇文章中提供了修改方案。
热更一个不存在的插件中的资源
打包之后引擎是会从 upluginmanifest 中读取当前工程中具有有哪些插件的,加载插件中的资源先判断插件是否存在,从而实现一个粒度较粗的过滤效果。
所以,当需要把一个在基础包中不存在的插件打包至 pak 中,需要在打包资源的同时需要把项目的 upluginmanifest
文件同步打包,挂载点为:
1 | ../../../PROJECT_NAME/Plugins/PROJECT_NAME.upluginmanifest |
关于 upluginmanifest
的介绍,可以看我之前的笔记:UE4#upluginmanifest。
热更的资源没有效果 / 材质丢失
如果热更蓝图,逻辑没有变化,需要检查资源是否被 Cook,可以手动在 Content Browser 中通过 HotPatcher 中提供的功能对选中资源执行 Cook,也可以在打包 Patch 时勾选 bCookAsset
选项。
如果时热更了资源 / 材质,没有效果,需要检查是否把 Shaderbytecode 打包,如果新增材质没有打包 shaderbytecode 是会导致 Shader 获取失败使用默认材质的。
Log 中的错误:
如果不使用引擎启动时自动挂载 pak 的方式,而是运行时手动 Mount 包含新 shaderbytecode 的 pak,则需要在 mount 之后手动重新加载一遍 shaderbytecode,这样引擎才能够读取到最新的 shader,插件中提供了一个辅助函数:
1 |
|
在 mount 之后调用即可。
AssetRegistry 是否必须热更
看需求,如果 Runtime 的代码中有通过 AssetRegistry 模块获取资源的引用关系、检测资源是否存在,需要热更。但是 AssetRegistry 并不是引擎必要的,如果肯定不会在运行时用到,可以去掉它,会节省一点内存。
具体介绍可以看我之前的笔记:UE4# 控制 AssetRegistry 的序列化
Android 提示 not found uproject
UE 中有一个 BUG,在 4.25.1 引擎版本中可以复现,步骤如下:
- 安装 apk,第一次启动游戏
- 打开 UE 的沙盒数据目录
UE4Game/PROJECTNAME
,在这个目录下创建Content/Paks
目录 - 重新启动游戏
Log 中也有 Project file not found: ../../../FGame/FGame.uproject
提示。
在 Android 上自动挂载的 Pak 文件可以放到 Saved/Paks
下,有时间具体分析一下这个问题。
控制资源不打到基础包中
拆分基础包的实践可看我的这两篇文章:
分析某个平台的包中的资源
可以使用 UE 提供的 Asset Audit 工具,需要在每次打包时备份好 Cooked/PLATFORM/PROJECT_NAME/Metadata 目录中的 DevelopmentAssetRegistry.bin 文件。
也可以使用 UnrealPakViewer 来直接加载 Pak 文件。
具体可以看这篇文章的资产审计小节:UE4 热更新:资产管理与审计工具 #资产审计
UMG 子控件热更不生效
如果 Instanced 的形式引用的 UMG,子 UMG 的变动需要递归包含所有以子控件形式引用的 UMG 资源。我之前在笔记中记录过这个问题:UE4#UMG 的子控件引用热更问题。
解决方案:HotPatcher 中具有一个递归分析 UMG 父控件的的选项(bRecursiveWidgetTree),开启即可。
这个问题的具体分析在我 2020 UOD 的演讲中有详细介绍,感兴趣的可以去这里查看视频和 PPT:
Pak 是否可以跨引擎版本使用
不行,要保持打包和使用的引擎版本一致。
从 A 项目打包给 B 项目使用的 Pak
HotPatcher 中做了替换 pakcommand 的功能,可以通过以下参数指定:
注意:From 和 To 都必须要包含 ../../../
前缀,不然会把文件的绝对路径替换了。
plugin HotPatcher faild
打包之后又如下提示:
应该是纯蓝图项目打包导致的,在项目中新建一个 C++ 类,变成一个 C++ 工程重新打包即可。
打包原始 uasset 资源
目前插件并没有能直接选择打包原始 uasset 资源的功能,但是可以使用一个取巧的方法实现。
可以设置 ReplacePakCommandTexts
把 Cooked 的目录替换为项目的Content 目录
:
虽然 *pakcommand.txt
里依然会有 uexp
等文件的记录,但是在项目的 Content
下没有,也并不会打包到 pak 中去,会忽略不存在的文件,并有以下 log 输出:
1 | LogPakFile: Warning: Missing file "D:/Client/Content/Assets/Scene/Map/LookDev/DemoAssets/Mesh/FFXV/000.uexp" will not be added to PAK file. |
算是取巧的一种实现吧,但是可行。
导出跨机器的通用配置文件
Q:HotPatcher 中导出的配置,有些是依赖于本地绝对路径的文件,如 BaseVersion、Non-Asset 文件、SavePath 等,不同的机器这些绝对路径并不能保证一致,能否基于相对路径进行配置?
A:可以。HotPatcher 所有能够指定路径的配置项,都支持标记符替换,可以使用以下标记符来替代绝对路径。
1 | [ENGINEDIR] |
在打包时会自动替换为当前机器的绝对路径,基于相对路径则是完全通用的配置文件。
依赖分析耗时
当项目资源非常多,插件提供的依赖分析功能的耗时十分可观,其主要目的是分析被依赖的资源,防止依赖了但是没有被打包的情况。
如果依赖了引擎、插件中的资源,不进行依赖分析是管理不到的(或者手动指定),而且如果想要剔除没有引用的资源不分析也做不到。不需要的话也是可以关掉的,设置 bAnalysisFilterDependencies 就可以了。
这样只会对配置中所指定的目录下的所有资源、单独指定的资源,与基础版本中的进行 Diff 分析,能够减少依赖分析的耗时(如果资源量非常大,并且能保证所有的资源依赖都在 /Game)下的可以不开启依赖分析。
OTHER UPDATE
使用 Github Gist 管理的动态更新内容,在国内网络可能会无法查看。