UE热更新:Questions & Answers

HotPatcher项目开源这一年多以来,经过了不少的更新和优化,也被越来越多的开发者选择作为自己项目的热更新方案,期间有不少人陆陆续续询问UE4热更新相关遇到的问题,很多问题比较常见,重复询问的频率也比较多,所以我准备把一些常见的问题进行整理,方便初步上手UE4热更新方案的人能够尽快地排查问题。

本篇文章会持续更新UE4热更新和HotPatcher相关的Q&A内容,有疑问的地方也可以直接在本篇文章中评论,我会定期统一回答和整理,也可以加入我的UE4热更新群讨论遇到的问题(QQ群958363331)。

HotPatcher相关问题

  1. 是否可以用在商业项目中?

本软件的开源协议:允许在商业项目中免费使用功能,但不允许任何第三方基于该插件进行任何形式的二次收费,包括但不限于录制收费课程、对插件及代码的二次分发等。

  1. 是否可以热更C++?
    不能,只能用来更新uasset和Non-Asset(lua/db/json等等)。

  2. 支持移动端热更吗?
    支持,本身HotPatcher是没有平台限制的,可以打包和管理UE支持的任意平台。

注意:使用HotPatcher打包时,需要避免一个目录既包含uasset又包含non-asset的情况,不然会导致未被cook的uasset打包。

热更新系列文章

我写的UE4热更新的系列文章,可以作为工程实践的参考:

pak的自动挂载目录

以下三个路径中的Pak会在引擎启动时自动挂载:

  • Engine/Content/Paks
  • GAME_DIR/Content/Paks
  • GAME_DIR/Saved/Paks
Runtime\PakFile\Private\IPlatformFilePak.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FPakPlatformFile::GetPakFolders(const TCHAR* CmdLine, TArray<FString>& OutPakFolders)
{
#if !UE_BUILD_SHIPPING
// Command line folders
FString PakDirs;
if (FParse::Value(CmdLine, TEXT("-pakdir="), PakDirs))
{
TArray<FString> CmdLineFolders;
PakDirs.ParseIntoArray(CmdLineFolders, TEXT("*"), true);
OutPakFolders.Append(CmdLineFolders);
}
#endif

// @todo plugin urgent: Needs to handle plugin Pak directories, too
// Hardcoded locations
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::ProjectContentDir()));
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::ProjectSavedDir()));
OutPakFolders.Add(FString::Printf(TEXT("%sPaks/"), *FPaths::EngineContentDir()));
}

这三个路径下的pak的默认优先级不同(除非通过_1_P.pak这种形式命名):

Runtime\PakFile\Private\IPlatformFilePak.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int32 FPakPlatformFile::GetPakOrderFromPakFilePath(const FString& PakFilePath)
{
if (PakFilePath.StartsWith(FString::Printf(TEXT("%sPaks/%s-"), *FPaths::ProjectContentDir(), FApp::GetProjectName())))
{
return 4;
}
else if (PakFilePath.StartsWith(FPaths::ProjectContentDir()))
{
return 3;
}
else if (PakFilePath.StartsWith(FPaths::EngineContentDir()))
{
return 2;
}
else if (PakFilePath.StartsWith(FPaths::ProjectSavedDir()))
{
return 1;
}

return 0;
}

Mount Point的作用

在Mount Pak的时候,有一个参数可以指定MountPoint:

1
2
3
4
5
6
7
/**
* Mounts a pak file at the specified path.
*
* @param InPakFilename Pak filename.
* @param InPath Path to mount the pak at.
*/
bool Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath = NULL, bool bLoadIndex = true);

那么它是干什么的呢?
首先从Mount函数开始:

1
2
3
4
if (InPath != NULL)
{
Pak->SetMountPoint(InPath);
}

如果在调用Mount时传递了InPath,则通过加载Pak的FPakFile实例调用SetMountPoint,把InPath设置给它。
其实在FPakFile中,MountPath是有默认值的(从Pak文件中读取),在FPakFile的构造函数中调用了Initialize(Reader, bLoadIndex);,Initialize中又调用了LoadIndex,在LoadIndex中从Pak中读取Pak的Mount Point的逻辑:

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
// Runtime/PakFile/Private/IPlatformFilePak.cpp
void FPakFile::LoadIndex(FArchive* Reader)
{
if (CachedTotalSize < (Info.IndexOffset + Info.IndexSize))
{
UE_LOG(LogPakFile, Fatal, TEXT("Corrupted index offset in pak file."));
}
else
{
if (Info.Version >= FPakInfo::PakFile_Version_FrozenIndex && Info.bIndexIsFrozen)
{
SCOPED_BOOT_TIMING("PakFile_LoadFrozen");

// read frozen data
Reader->Seek(Info.IndexOffset);
int32 FrozenSize = Info.IndexSize;

// read in the index, etc data in one lump
void* DataMemory = FMemory::Malloc(FrozenSize);
Reader->Serialize(DataMemory, FrozenSize);
Data = TUniquePtr<FPakFileData>((FPakFileData*)DataMemory);

// cache the number of entries
NumEntries = Data->Files.Num();
// @todo loadtime: it is nice to serialize the mountpoint right into the Data so that IndexSize is right here
// but it takes this to copy it out, because it's too painful for the string manipulation when dealing with
// MemoryImageString everywhere MountPoint is used
MountPoint = Data->MountPoint;
}
// ...
}
// ...
}

简单的可以理解为:如果Mount时不传递Mount Point就会从Pak文件中读取,如果有传入就设置为传入的值(Pak文件中的MountPoint是Pak中所有文件的公共路径)。

那么,给Pak设置MountPoint的作用是什么呢?
真实目的是,检测要加载的文件是否存在于当前Pak中!因为Pak的Mount Point的默认含义是当前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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Runtime/PakFile/Public/IPlatformFilePak.h
/**
* Finds a file in the specified pak files.
*
* @param Paks Pak files to find the file in.
* @param Filename File to find in pak files.
* @param OutPakFile Optional pointer to a pak file where the filename was found.
* @return Pointer to pak entry if the file was found, NULL otherwise.
*/
static bool FindFileInPakFiles(TArray<FPakListEntry>& Paks,const TCHAR* Filename,FPakFile** OutPakFile,FPakEntry* OutEntry = nullptr)
{
FString StandardFilename(Filename);
FPaths::MakeStandardFilename(StandardFilename);

int32 DeletedReadOrder = -1;

for (int32 PakIndex = 0; PakIndex < Paks.Num(); PakIndex++)
{
int32 PakReadOrder = Paks[PakIndex].ReadOrder;
if (DeletedReadOrder != -1 && DeletedReadOrder > PakReadOrder)
{
//found a delete record in a higher priority patch level, but now we're at a lower priority set - don't search further back or we'll find the original, old file.
UE_LOG( LogPakFile, Verbose, TEXT("Delete Record: Accepted a delete record for %s"), Filename );
return false;
}

FPakFile::EFindResult FindResult = Paks[PakIndex].PakFile->Find(*StandardFilename, OutEntry);
if (FindResult == FPakFile::EFindResult::Found )
{
if (OutPakFile != NULL)
{
*OutPakFile = Paks[PakIndex].PakFile;
}
UE_CLOG( DeletedReadOrder != -1, LogPakFile, Verbose, TEXT("Delete Record: Ignored delete record for %s - found it in %s instead (asset was moved between chunks)"), Filename, *Paks[PakIndex].PakFile->GetFilename() );
return true;
}
else if (FindResult == FPakFile::EFindResult::FoundDeleted )
{
DeletedReadOrder = PakReadOrder;
UE_LOG( LogPakFile, Verbose, TEXT("Delete Record: Found a delete record for %s in %s"), Filename, *Paks[PakIndex].PakFile->GetFilename() );
}
}

UE_CLOG( DeletedReadOrder != -1, LogPakFile, Warning, TEXT("Delete Record: No lower priority pak files looking for %s. (maybe not downloaded?)"), Filename );
return false;
}

当我们从Pak中读取文件时,通过对游戏中所有Mount的Pak调用Find函数,而FPakFile::Find的函数就实现了上述我说的逻辑:

1
2
3
4
5
6
7
8
9
10
11
// Runtime/PakFile/Private/IPlatformFilePak.cpp
FPakFile::EFindResult FPakFile::Find(const FString& Filename, FPakEntry* OutEntry) const
{
QUICK_SCOPE_CYCLE_COUNTER(PakFileFind);
if (Filename.StartsWith(MountPoint))
{
FString Path(FPaths::GetPath(Filename));
// ...
}
// ...
}

所以,MountPoint的作用就是在从Pak中查找文件时,首先判断文件的路径是否与Pak中所有文件的基础路径相匹配(StartWith),如果不存在也就不会进入后续的流程了。

Pak无法被挂载

在本体包中开启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的参数可以看我之前的一篇文章:UE4工具链配置与开发技巧#UnrealPak的参数

Pak master signature table check failed for pak

  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;
}

iOS热更metallib问题

在4.25存在不会重新加载shaderbytecode的问题,而且引擎内部对加载metallib是单独处理的流程,无法复用usahderbytecode的流程,所以出iOS包尽量使用远程打包的方式,会生成ushaderbytecode,在4.25里LoadLibrary没有问题,但是如果去加载metallib就有问题。

UE热更Shader相关的内容可以看之前的文章:UE热更新:Create Shader Patch

UE4.25+ ShaderPatch Crash

这是因为在4.25+引擎内部的bug导致的,UE热更新: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中的错误:

如果在打包时使用了bSharedShaderLibrary,在运行时手动Mount包含新shaderbytecode的pak,则需要在mount之后手动重新加载一遍shaderbytecode,这样引擎在加载材质时才能够读取到最新的Shader:

1
2
3
#include "ShaderCodeLibrary.h"

FShaderCodeLibrary::OpenLibrary("NewShaderLib", FPaths::ProjectContentDir());

在mount之后,根据ShaderLib的实际路径和命名调用即可。

如果没有开bSharedShaderLibrary,则无需处理,引擎会默认使用资源内的Inline Shader Code。

AssetRegistry是否必须热更

看需求,如果Runtime的代码中有通过AssetRegistry模块获取资源的引用关系、检测资源是否存在,需要热更。但是AssetRegistry并不是引擎必要的,如果肯定不会在运行时用到,可以去掉它,会节省一点内存。

具体介绍可以看我之前的笔记:UE4#控制AssetRegistry的序列化

Android提示not found uproject

UE中有一个BUG,在4.25.1引擎版本中可以复现,步骤如下:

  1. 安装apk,第一次启动游戏
  2. 打开UE的沙盒数据目录UE4Game/PROJECTNAME,在这个目录下创建Content/Paks目录
  3. 重新启动游戏

Log中也有Project file not found: ../../../FGame/FGame.uproject提示。

在Android上自动挂载的Pak文件可以放到Saved/Paks下,有时间具体分析一下这个问题。

控制资源不打到基础包中

拆分基础包的实践可看我的这两篇文章:

分析某个平台的包中的资源

可以使用UE提供的Asset Audit工具,需要在每次打包时备份好Cooked/PLATFORM/PROJECT_NAME/Metadata目录中的DevelopmentAssetRegistry.bin文件。

也可以使用UnrealPakViewer来直接加载Pak文件。

具体可以看这篇文章的资产审计小节:UE热更新:资产管理与审计工具#资产审计

UMG子控件热更不生效

如果Instanced的形式引用的UMG,子UMG的变动需要递归包含所有以子控件形式引用的UMG资源。我之前在笔记中记录过这个问题:UE4#UMG的子控件引用热更问题

解决方案:HotPatcher中具有一个递归分析UMG父控件的的选项(bRecursiveWidgetTree),开启即可。

这个问题的具体分析在我2020 UOD的演讲中有详细介绍,感兴趣的可以去这里查看视频和PPT:

注意,Instanced的UMG只对控件的序列化有影响,对于子控件中的逻辑变动无关的。

如下这种情况,只打包子控件的UMG是生效的:

  1. UMG_Child中有一个Button,并绑定了Button事件,输出了TEST01
  2. UMG_Main中嵌入UMG_Child
  3. 打出版本
  4. 修改UMG_Child中Button事件的输出,改为TEST02
  5. 只打包UMG_Child
  6. 创建出UMG_Main,点击它里的UMG_Child的Button,会正确输出TEST02

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
2
3
4
5
6
[ENGINEDIR]
[ENGINE_CONTENT_DIR]
[PROJECTDIR]
[PROJECT_CONTENT_DIR]
[PROJECT_SAVED_DIR]
[PROJECT_CONFIG_DIR]

在打包时会自动替换为当前机器的绝对路径,基于相对路径则是完全通用的配置文件。

依赖分析耗时

当项目资源非常多,插件提供的依赖分析功能的耗时十分可观,其主要目的是分析被依赖的资源,防止依赖了但是没有被打包的情况。
如果依赖了引擎、插件中的资源,不进行依赖分析是管理不到的(或者手动指定),而且如果想要剔除没有引用的资源不分析也做不到。不需要的话也是可以关掉的,设置bAnalysisFilterDependencies就可以了。
这样只会对配置中所指定的目录下的所有资源、单独指定的资源,与基础版本中的进行Diff分析,能够减少依赖分析的耗时(如果资源量非常大,并且能保证所有的资源依赖都在/Game)下的可以不开启依赖分析。

Memory mapped file has the wrong alignment

注意:最新的插件中已经做了适配支持,无需再手动指定。

在IOS包中出现以下Crash:

1
IsAligned(MappedRegion->GetMappedPtr(), FPlatformProperties::GetMemoryMappingAlignment()), TEXT("Memory mapped file has the wrong alignment!")

这是因为打包IOS时没有设置alignformemorymapping的值导致的,默认是0,但是IOS要设置为16384

Engine/Source/Runtime/Core/Public/IOS/IOSPlatformProperties.h
1
2
3
4
static FORCEINLINE int64 GetMemoryMappingAlignment()
{
return 16384;
}

打包Pak时传递参数即可:-alignformemorymapping=16384

在UE默认打包时也添加了该参数:

Source\Programs\AutomationTool\Scripts\CopyBuildToStagingDirectory.Automation.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void CreatePaks(ProjectParams Params, DeploymentContext SC, List<CreatePakParams> PakParamsList, EncryptionAndSigning.CryptoSettings CryptoSettings, FileReference CryptoKeysCacheFilename)
{
// ...
string BulkOption = "";
ConfigHierarchy PlatformEngineConfig = null;
if (Params.EngineConfigs.TryGetValue(SC.StageTargetPlatform.PlatformType, out PlatformEngineConfig))
{
bool bMasterEnable = false;
PlatformEngineConfig.GetBool("MemoryMappedFiles", "MasterEnable", out bMasterEnable);
if (bMasterEnable)
{
int Value = 0;
PlatformEngineConfig.GetInt32("MemoryMappedFiles", "Alignment", out Value);
if (Value > 0)
{
BulkOption = String.Format(" -AlignForMemoryMapping={0}", Value);
}
}
}
//...
}

它读取的是Config/PLATFORM/*Engine.iniMemoryMappedFilesSection下的Alignment值,如IOS:

Config/IOS/IOSEngine.ini
1
2
3
[MemoryMappedFiles]
MasterEnable=true
Alignment=16384

正好对应了代码里的值,并且只有IOS的平台指定了该项的值:MemoryMappedFiles

导入Release漏掉部分插件资源

请检查插件路径名字是否与插件名完全一致。如:

1
Plugins\HDiffPatchUE4\HDiffPatch.uplugin

这种情况是不允许的,路径必须要与插件名一致。

Editor中资源打包和真机加载流程

随着游戏内资源越来越多,包体越来越大,但移动端的包大小是有限制的,不能无限度地把所有的资源都打到基础包中,Android上限是2G(可以开启Allow Large Obb files以支持4G),IOS提交到AppStore的上限也是4G,通常会保持IOS/Android基础包内的资源统一,其余的资源用热更或动态下载的形式更新到设备上。

所以,实际上在开发阶段打进基础包内的资源,只是工程中的一部分。经常无法满足策划、美术、测试同学的验证需要。

  1. 新的地图、资源想要在真机查看
  2. 包内资源有问题,提交后等待整包构建时间太久
  3. 基础包的资源量超标,可能无法添加新的资源

基于这种需求,在开发阶段,可以使用基础包+补丁的形式实现。

资源的打包

在我们当前的工程中,在编辑器中,可以直接在资源、目录上右键选择Cook And Pak Actions,选择对应的平台,以及选择AnalysisDependencies(分析依赖)。

不同平台的包需要选择的不一样(根据自己项目实际情况选择):

  • 安卓:Android_ASTC
  • IOS:IOS
  • PCVersion:WindowsNoEditor

执行之后右下角会有执行提示:

需要等待执行完毕,这个过程不会卡住编辑器,可以做其他的编辑操作。

执行的耗时取决于引用的资源量,以及所选择的资源之前有没有执行过打包(DDC)。

执行完毕之后,会在项目的Saved/HotPatcher/Paks/{EXECUTE_TIME}/Android_ASTC下面创建出一个对应的 .pak 文件,这个 pak 文件就是打包资源的补丁包,文件名中包含了平台名,不同平台的 pak 文件不能混用

放入真机中

PCVersion

PCVersion需要将这个Pak文件放到以下目录中:

1
WindowsNoEditor\PROJECT_NAME\Content\Paks

若不存在Paks目录,可手动创建。

Android

在Android中,需要将这个 pak 文件放到下面目录中:

1
UE4Game/PROJECT_NAME/PROJECT_NAME/Saved/Paks

如果修改了数据目录到沙盒路径,则是这个路径:

1
Android/data/com.xxxx.yyyy/files/UE4Game/PROJECT_NAME/PROJECT_NAME/Saved/Paks

如果没有Paks目录,可以手动创建一个。

IOS

IOS则要复杂一些,需要用爱思助手iMaZing等IOS管理工具传递文件。
放入 文稿 - PROJECT_NAME - Saved - Paks下,若不存在Paks目录,可手动创建。

检查挂载情况

当将pak文件放入上述目录之后,就可以启动游戏了。

如果为了确认pak文件是否生效,可以从Log中查看:

1
2
LogPakFile: Display: Found Pak file ../../../PROJECT_NAME/Saved/Paks/2022.10.18-13.02.53_IOS_001_P.pak attempting to mount.
LogPakFile: Display: Mounting pak file ../../../PROJECT_NAME/Saved/Paks/2022.10.18-13.02.53_IOS_001_P.pak.

有这样的Log,就代表资源包被引擎读取成功,可以直接使用,就像它被打进了基础包中一样

注意事项

手动打包Pak的优先级高于基础包内的资源,意味着一个资源如果补丁包基础包同时存在,就会替换基础包内的,就像热更新逻辑。

  1. 不同平台的pak不能混用!
  2. 测试完毕之后要把设备上的pak文件删除,不然后面新打的包可能会出现资源表现错误。

UE5+的问题排查

注意:从v82.0开始,已兼容UE5.2、5.3。

在UE5中如果发现打包的Pak出现异常:

  1. Pak无法挂载:排查是否项目开启了IoStore,在项目设置中关闭后,重新打基础包。
本篇文章的内容会持续更新。

微信扫描二维码,关注我的公众号。

本文标题:UE热更新:Questions & Answers
文章作者:查利鹏
发布时间:2021/03/12 10:45
本文字数:7.3k 字
原始链接:https://imzlp.com/posts/16895/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!