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 回答