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

posted @ 2021-05-21 10:16  皮斯卡略夫  阅读(3776)  评论(2编辑  收藏  举报