PAK 文件
Pak 文件是 Unreal 打包素材的文件。一個 Unreal 程序可以使用或者不使用 Pak 來管理素材。
Unreal 的 Pak 文件內包括了物體,材質,blueprint,map等等。Level 以 map 的形式保存。
Dynamic load map from pak
目標:在程序運行時加載自定義 Pak 文件,並加載其中的完整 map 內容
How
Pak 文件的加載類似文件系統加載一個外接硬盤,需要先 mount 這個硬盤到某一個 mount point 然后就可以像普通文件一樣讀寫這個外置硬盤的內容。
Delegate 是 UE 提供給開發者的安全便捷調用一些函數的方法,加載 Pak 也是通過 Delegate 來實現的:FCoreDelegates::MountPak
。
UE 推薦在調用 Delegate 之前確認函數是否已綁定,如果啟動的游戲程序從來沒有加載過任何 Pak 文件,則該 Delegate 並未被綁定,所以需要先啟動 Pak 加載器。
然后再調用加載函數才能成功加載。
加載完成后需要設置正確的 Pak 掛載點,這樣才能夠完美打開 Pak 內含的關卡。
流程如下:
- 確認 CoreDelegate::MountPak 可用,若不可用,初始化之並注冊
- mount pak file
- 設置正確的 mount point
- 打開 map
CoreDelegates 的預先注冊
Delegate 的綁定判斷:FCoreDelegates::MountPak.IsBound()
若為 false,則無法掛載 pak
MountPak 在 UE 啟動時,根據啟動參數視情況而啟動。若原始程序需要帶有 Pak 需要加載,則上述判斷返回true,若原始程序啟動時無加載 Pak 事項,則上述函數返回 false。
為了解決第二種情況的問題,令程序無論何時均可以加載 Pak,則需要手動注冊 Delegate.
該 Delegate 實際調用到的函數為:FPakPlatformFile::HandleMountPakDelegate
該函數在 Initialize
函數內注冊。故需要手動初始化 FPakPlatformFile
.
分析 UE Game 的啟動流程,在如下流程中:
FEngineLoop::PreInitPreStartupScreen()
FEngineLoop::LaunchCheckForFileOverride()
ConditionallyCreateFileWrapper(TEXT("PakFile"), CurrentPlatformFile, CmdLine)
會創建 PakPlatformFile 並進行 initialize 並將得到的 PakPlatformFile 加入到系統的 Platform File 中。
同時需要調用 InitializeNewAsyncIO
否則無法正常解析 Pak 文件。參考該部分代碼,具體實現如下:
IPlatformFile* CurrentPlatformFile = &FPlatformFileManager::Get().GetPlatformFile();
IPlatformFile* WrapperFile = FPlatformFileManager::Get().GetPlatformFile(TEXT("PakFile"));
const TCHAR* CommandLine = FCommandLine::Get();
WrapperFile->Initialize(CurrentPlatformFile, CommandLine);
FPlatformFileManager::Get().SetPlatformFile(*WrapperFile);
#if WITH_COREUOBJECT
FPlatformFileManager::Get().InitializeNewAsyncIO();
#endif
則后續的 IsBound()
判斷為 True,可以調用到 Mount 函數。
Mount
Mount Pak 的函數如下:
IPakFile* FPakPlatformFile::HandleMountPakDelegate(const FString& PakFilePath, int32 PakOrder)
PakOrder
, 這個參數代表加載的 Pak 文件的優先級,優先級越高,引擎搜索資源文件時越先搜索。UE4 內部有一個根據文件路徑分配優先級的函數:
// 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;
}
可以看出在不同的文件夾下的文件優先級不同,其中在 Paks 路徑下的文件優先級最高。新加載的 Pak 文件若需要較高的優先級,則 order 提供 4 即可。
如果該參數提供INDEX_NONE
則會根據上面的函數計算該文件的優先級。
RegisterMountPoint
RegisterMountPoint 的函數聲明如下
// This will insert a mount point at the head of the search chain
// (so it can overlap an existing mount point and win).
static void RegisterMountPoint
(
const FString & RootPath,
const FString & ContentPath
)
參數解釋:
RootPath: Logical Root Path.
ContentPath: Content Path on disk.
Logical Path 是程序運行時會去找的 path 並非實際路徑,根據反復嘗試,只有在這個參數寫成 "/Game/" 的時候才能正確加載到所有資源,若隨意命名可能只能加載到 map 資源,連 map_builtdata
都找不到。會有如下報錯:
LogStreaming: Error: Couldn't find file for package /Game/StarterContent/Maps/Map2_BuiltData requested by async loading code. NameToLoad: /Game/StarterContent/Maps/Map2_BuiltData
根據 UE 的定義,"/Game/" 是 GameRootPath(見 PackageName.cpp) 是程序加載文件時搜索的路徑之一。 所以這樣寫可以成功,應該也有方法自定義加載的路徑,但是我暫時不知道。
Content Path 是指需要 mount 到前面這個點的真正內容的父路徑,這個路徑取決於加載的 Pak 的當前掛載點和 Pak 內部的文件路徑。
通過 PakGetMountPoint() 函數得到當前掛載點。在一個示例中,為 "../../../"
且該例子 Pak 內部文件結構如下
MyProject1/Content/StarterContent/Blueprints/...
MyProject1/Content/StarterContent/Maps/...
MyProject1/Content/StarterContent/Materials/...
MyProject1/Content/StarterContent/Shapes/...
...
故最終路徑為 "../../../MyProject1/Content/"
Code
示例調用代碼如下:
IPakFile *pakFile = FCoreDelegates::MountPak.Execute(FString(pakFilePath.c_str()), 4);
if (pakFile)
{
const auto& mountPoint = pakFile->PakGetMountPoint();
FString pakContentPath = mountPoint + FString(contentPath.c_str());
FPackageName::RegisterMountPoint("/Game/", pakContentPath);
}
打開關卡:UGameplayStatics::OpenLevel
.
問題解決
Missing shader resource
問題:成功加載 map 但是模型材質和 shader 丟失,無法正確顯示對應內容。log 如下:
[UE4] [2021.05.17-03.57.53:734][ 0]LogShaders: Error: Missing shader resource for hash '589973CAE03D7F0ECFEC6B825B774136FF9FCB9D' for shader platform 16 in the shader library
LogMaterial: Error: Tried to access an uncooked shader map ID in a cooked application
[UE4] [2021.05.17-08.02.25:186][ 0]LogMaterial: Can't compile BasicShapeMaterial with cooked content, will use default material instead
這個問題原因是在要加載 Pak 的工程設置里面啟用了 Share Material Shader Code,啟用這個選項會 “Save shader only once” 這樣的優化選項導致了外部的 shader 無法被找到。
在工程中關閉此開關即可。
Ref
https://answers.unrealengine.com/questions/363767/how-to-load-a-map-from-a-dynamic-level.html 參見 TestyRabbit May 03 '18 at 4:07 PM 的評論
sample code: https://pastebin.com/ZWAPtynK
https://answers.unrealengine.com/questions/258386/loading-map-from-pak-at-runtime.html top 回答