Unreal: Dynamic load map from Pak file


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 內含的關卡。

流程如下:

  1. 確認 CoreDelegate::MountPak 可用,若不可用,初始化之並注冊
  2. mount pak file
  3. 設置正確的 mount point
  4. 打開 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 回答

https://answers.unrealengine.com/questions/963414/view.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM