当在 UE 的 Project Setting
里Project
-Packaging
-UsePakFile
启用时,会打包出来 pak
文件,以 Windows
平台为例,打包出来的 pak
路径为:
1 WindowsNoEditor/PROJECT_NAME/Content/Paks
该目录下的 pak
文件在游戏启动时会自动加载,在引擎的 FEngineLoop::PreInit
中调用 LaunchCheckForFileOverride
(LaunchEngineLoop.cpp
) 又调用 ConditionallyCreateFileWrapper
来加载 PakFile
的PlatformFile
,但是在 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 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 ; } WrapperFile = nullptr ; } } else { 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 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 bool FPakPlatformFile::Initialize (IPlatformFile* Inner, const TCHAR* CmdLine) { LLM_SCOPE (ELLMTag::FileSystem); SCOPED_BOOT_TIMING ("FPakPlatformFile::Initialize" ); check (Inner != NULL ); LowerLevel = Inner; #if EXCLUDE_NONPAK_UE_EXTENSIONS 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 bSigned = GetPakSigningKey ().IsValid () && !FParse::Param (FCommandLine::Get (), TEXT ("fileopenlog" ));; TArray<FString> PakFolders; GetPakFolders (FCommandLine::Get (), PakFolders); MountAllPakFiles (PakFolders); #if !UE_BUILD_SHIPPING GPakExec = MakeUnique<FPakExec>(*this ); #endif 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) { 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 void FPakPlatformFile::GetPakFolders (const TCHAR* CmdLine, TArray<FString>& OutPakFolders) {#if !UE_BUILD_SHIPPING FString PakDirs; if (FParse::Value (CmdLine, TEXT ("-pakdir=" ), PakDirs)) { TArray<FString> CmdLineFolders; PakDirs.ParseIntoArray (CmdLineFolders, TEXT ("*" ), true ); OutPakFolders.Append (CmdLineFolders); } #endif 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 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.pak
的PakOrder
加了 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 bool FPakPlatformFile::Mount (const TCHAR* InPakFilename, uint32 PakOrder, const TCHAR* InPath ) { 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" ))) { 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 ) { ChunkVersionNumber = (uint32)ChunkVersionSigned + 1 ; } } } } PakOrder += 100 * ChunkVersionNumber; } { 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 之后还需要再更新可能会造成用户流失,这是个要考虑的问题。