引擎启动时 Pak 的加载

当在 UE 的 Project SettingProject-Packaging-UsePakFile启用时,会打包出来 pak 文件,以 Windows 平台为例,打包出来的 pak 路径为:

1
WindowsNoEditor/PROJECT_NAME/Content/Paks

该目录下的 pak 文件在游戏启动时会自动加载,在引擎的 FEngineLoop::PreInit 中调用 LaunchCheckForFileOverride(LaunchEngineLoop.cpp) 又调用 ConditionallyCreateFileWrapper 来加载 PakFilePlatformFile,但是在 ConditionallyCreateFileWrapper 的代码中做了一层 WrapperFile->ShouldBeUsed 判断:

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
// Runtime/Launch/Private/LaunchEngineLoop.cpp
static IPlatformFile* ConditionallyCreateFileWrapper(const TCHAR* Name, IPlatformFile* CurrentPlatformFile, const TCHAR* CommandLine, bool* OutFailedToInitialize = nullptr, bool* bOutShouldBeUsed = nullptr )
{
if (OutFailedToInitialize)
{
*OutFailedToInitialize = false;
}
if (bOutShouldBeUsed)
{
*bOutShouldBeUsed = false;
}
IPlatformFile* WrapperFile = FPlatformFileManager::Get().GetPlatformFile(Name);
if (WrapperFile != nullptr && WrapperFile->ShouldBeUsed(CurrentPlatformFile, CommandLine))
{
if (bOutShouldBeUsed)
{
*bOutShouldBeUsed = true;
}
if (WrapperFile->Initialize(CurrentPlatformFile, CommandLine) == false)
{
if (OutFailedToInitialize)
{
*OutFailedToInitialize = true;
}
// Don't delete the platform file. It will be automatically deleted by its module.
WrapperFile = nullptr;
}
}
else
{
// Make sure it won't be used.
WrapperFile = nullptr;
}
return WrapperFile;
}

如果为 false 不会对该 PlatformFile 调用 Initialize,而FPakPlatformFile 的一些成员就是在这里被设置的。FPakPlatformFile::ShoubleBeUsed的定义如下(UE_4.22.3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Runtime/PakFile/Private/IPlatformFilePak.cpp
bool FPakPlatformFile::ShouldBeUsed(IPlatformFile* Inner, const TCHAR* CmdLine) const
{
bool Result = false;
#if !WITH_EDITOR
if (!FParse::Param(CmdLine, TEXT("NoPak")))
{
TArray<FString> PakFolders;
GetPakFolders(CmdLine, PakFolders);
Result = CheckIfPakFilesExist(Inner, PakFolders);
}
#endif
return Result;
}

编辑器模式下直接就是 false,即编辑器模式下不可以使用 pak 的 mount 操作,因为 mount 中需要用到 LowerLevel 成员,而该成员在 FPakPlatformFile::Initialize 中被设置,所以不可以调用 mount,否则必 Crash。

Mount 所有 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Runtime\PakFile\Private\IPlatformFilePak.cpp
bool FPakPlatformFile::Initialize(IPlatformFile* Inner, const TCHAR* CmdLine)
{
LLM_SCOPE(ELLMTag::FileSystem);
SCOPED_BOOT_TIMING("FPakPlatformFile::Initialize");
// Inner is required.
check(Inner != NULL);
LowerLevel = Inner;

#if EXCLUDE_NONPAK_UE_EXTENSIONS
// Extensions for file types that should only ever be in a pak file. Used to stop unnecessary access to the lower level platform file
ExcludedNonPakExtensions.Add(TEXT("uasset"));
ExcludedNonPakExtensions.Add(TEXT("umap"));
ExcludedNonPakExtensions.Add(TEXT("ubulk"));
ExcludedNonPakExtensions.Add(TEXT("uexp"));
#endif

#if DISABLE_NONUFS_INI_WHEN_COOKED
IniFileExtension = TEXT(".ini");
GameUserSettingsIniFilename = TEXT("GameUserSettings.ini");
#endif

// signed if we have keys, and are not running with fileopenlog (currently results in a deadlock).
bSigned = GetPakSigningKey().IsValid() && !FParse::Param(FCommandLine::Get(), TEXT("fileopenlog"));;

// Find and mount pak files from the specified directories.
TArray<FString> PakFolders;
GetPakFolders(FCommandLine::Get(), PakFolders);
MountAllPakFiles(PakFolders);

#if !UE_BUILD_SHIPPING
GPakExec = MakeUnique<FPakExec>(*this);
#endif // !UE_BUILD_SHIPPING

FCoreDelegates::OnMountAllPakFiles.BindRaw(this, &FPakPlatformFile::MountAllPakFiles);
FCoreDelegates::OnMountPak.BindRaw(this, &FPakPlatformFile::HandleMountPakDelegate);
FCoreDelegates::OnUnmountPak.BindRaw(this, &FPakPlatformFile::HandleUnmountPakDelegate);

#if !(IS_PROGRAM || WITH_EDITOR)
FCoreDelegates::OnFEngineLoopInitComplete.AddLambda([this] {
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("Checking Pak Config"));
bool bUnloadPakEntryFilenamesIfPossible = false;
GConfig->GetBool(TEXT("Pak"), TEXT("UnloadPakEntryFilenamesIfPossible"), bUnloadPakEntryFilenamesIfPossible, GEngineIni);

if (bUnloadPakEntryFilenamesIfPossible)
{
// With [Pak] UnloadPakEntryFilenamesIfPossible enabled, [Pak] DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames
// can contain pak entry directory wildcards of which the entire recursive directory structure of filenames underneath a
// matching wildcard will be kept.
//
// Example:
// [Pak]
// DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames="*/Config/Tags/"
// +DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames="*/Content/Localization/*"
TArray<FString> DirectoryRootsToKeep;
GConfig->GetArray(TEXT("Pak"), TEXT("DirectoryRootsToKeepInMemoryWhenUnloadingPakEntryFilenames"), DirectoryRootsToKeep, GEngineIni);

FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
PakPlatformFile->UnloadPakEntryFilenames(&DirectoryRootsToKeep);
}

bool bShrinkPakEntriesMemoryUsage = false;
GConfig->GetBool(TEXT("Pak"), TEXT("ShrinkPakEntriesMemoryUsage"), bShrinkPakEntriesMemoryUsage, GEngineIni);
if (bShrinkPakEntriesMemoryUsage)
{
FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
PakPlatformFile->ShrinkPakEntriesMemoryUsage();
}
});
#endif

return !!LowerLevel;
}

UE 自动加载的 pak 路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Runtime\PakFile\Private\IPlatformFilePak.cpp
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()));
}

在非 Shipping 打包的时候可以通过才命令行加启动参数 -pakdir 来添加额外的 pak 路径。

引擎默认添加的路径为:

1
2
3
4
5
# relative to Project Path
Content/Paks/
Saved/Paks/
# relative to Engine Path
Content/Paks

之后又调用了 FPakPlatformFile::MountAllPakFiles 来把挂载所有 pak(默认对 pak 的名字进行了个降序排序,但是这里的排序没用),在该函数中 mount 的时候会给不同的路径加载的 pak 设置不同的Order,其函数在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Runtime\PakFile\Private\IPlatformFilePak.cpp
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;
}

概括来说:

1
2
3
4
5
6
7
# relative to project path
4 Content/Paks/PROJECT_NAME-*.pak
3 Content/Paks/
1 Saved/Paks

# relative to engine path
2 Content/Paks/

可以看到 Saved/Paks 下的 pak 文件加载的优先级是最低的。

Mount 的时候如果上述路径中有打出来 Patch 包,以 _Num_P.pak 结尾的文件,其中 Num 是数字,Patch 包的优先级高于普通的 pak,在 IPlatformFilePak.cpp 中默认给 _P.pakPakOrder加了 100,_P.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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// Runtime\PakFile\Private\IPlatformFilePak.cpp
bool FPakPlatformFile::Mount(const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath /*= NULL*/)
{
bool bSuccess = false;
TSharedPtr<IFileHandle> PakHandle = MakeShareable(LowerLevel->OpenRead(InPakFilename));
if (PakHandle.IsValid())
{
FPakFile* Pak = new FPakFile(LowerLevel, InPakFilename, bSigned);
if (Pak->IsValid())
{
if (InPath != NULL)
{
Pak->SetMountPoint(InPath);
}
FString PakFilename = InPakFilename;
if (PakFilename.EndsWith(TEXT("_P.pak")))
{
// Prioritize based on the chunk version number
// Default to version 1 for single patch system
uint32 ChunkVersionNumber = 1;
FString StrippedPakFilename = PakFilename.LeftChop(6);
int32 VersionEndIndex = PakFilename.Find("_", ESearchCase::CaseSensitive, ESearchDir::FromEnd);
if (VersionEndIndex != INDEX_NONE && VersionEndIndex > 0)
{
int32 VersionStartIndex = PakFilename.Find("_", ESearchCase::CaseSensitive, ESearchDir::FromEnd, VersionEndIndex - 1);
if (VersionStartIndex != INDEX_NONE)
{
VersionStartIndex++;
FString VersionString = PakFilename.Mid(VersionStartIndex, VersionEndIndex - VersionStartIndex);
if (VersionString.IsNumeric())
{
int32 ChunkVersionSigned = FCString::Atoi(*VersionString);
if (ChunkVersionSigned >= 1)
{
// Increment by one so that the first patch file still gets more priority than the base pak file
ChunkVersionNumber = (uint32)ChunkVersionSigned + 1;
}
}
}
}
PakOrder += 100 * ChunkVersionNumber;
}
{
// Add new pak file
FScopeLock ScopedLock(&PakListCritical);
FPakListEntry Entry;
Entry.ReadOrder = PakOrder;
Entry.PakFile = Pak;
PakFiles.Add(Entry);
PakFiles.StableSort();
}
bSuccess = true;
}
else
{
if (Pak->GetInfo().EncryptionKeyGuid.IsValid())
{
UE_LOG(LogPakFile, Log, TEXT("Deferring mount of pak \"%s\" until encryption key '%s' becomes available"), InPakFilename, *Pak->GetInfo().EncryptionKeyGuid.ToString());

check(!GetRegisteredEncryptionKeys().HasKey(Pak->GetInfo().EncryptionKeyGuid));
FPakListDeferredEntry& Entry = PendingEncryptedPakFiles[PendingEncryptedPakFiles.Add(FPakListDeferredEntry())];
Entry.Filename = InPakFilename;
Entry.Path = InPath;
Entry.ReadOrder = PakOrder;
Entry.EncryptionKeyGuid = Pak->GetInfo().EncryptionKeyGuid;
Entry.ChunkID = Pak->ChunkID;

delete Pak;
PakHandle.Reset();
return false;
}
else
{
UE_LOG(LogPakFile, Warning, TEXT("Failed to mount pak \"%s\", pak is invalid."), InPakFilename);
}
}
}
else
{
UE_LOG(LogPakFile, Warning, TEXT("Pak \"%s\" does not exist!"), InPakFilename);
}
return bSuccess;
}

另外,腾讯的和平精英的热更新的 pak 就是放在 Saved/Paks 里面。

而且,和平精英的 apk 的大小是 1.6G,我下载完之后又热更新了 1.2G 左右的内容,总共占了 3 个多 G…这样的 apk 里面 obb 的数据估计和外部的 pak 中的内容都是覆盖的,但是 apk 内容又没办法改,只能越更新越多了(但是可以在用户更新 APK 之后删除多余的 pak)。

注:腾讯的吃鸡用 sluaunreal,脚本的热更也是通过 pak 方式来加载的,这点在 sluaunreal 中有写:sluaunreal 增量打包问题

如果从省空间的角度考虑,最好的方式就是基础 apk+ 更新的方式修改游戏内容,但是玩家下载完 apk 之后还需要再更新可能会造成用户流失,这是个要考虑的问题。