概括:
做为一个商业项目,能快速响应玩家需求,及时修复项目BUG,是其成功的基本要素。
和所有的项目一样,从需求提出到最终呈现结果给用户,都要经历许多岗位、部门、公司间的沟通和协着。如何在技术实行,最快流程、最小感知的更新流程,就是一个重要课题。
问题提出:
unreal 有一套文件系统,让开发者能按照版本差异,封装出差异 pak 文件,再让线上运行中的应用根据触发条件下载对应的 pak ,实现热更新功能。
那么在工作过程中,我有没有办法来模拟线上运行环境加载和挂载(mount) pak 的过程?目前在 unreal 中是直接在 editor 中没有启动 FPakPlatformFile ,也就意味着无法在 editor 模式下进行 mount 操作了。
Unreal 的文件模式
如上图,unreal 的 platform file 是责任链模式,在实现文件的读写时,每个链点按各自的实现来调用,如果没有实现,便传递给 LowerLevel 。
从源代码可以看出,在 Editor 模式下是FPakPlatformFile 是不被加载到整个责任链中来的。代码如下(IPlatformFilePak.cpp 中):
bool FPakPlatformFile::ShouldBeUsed(IPlatformFile* Inner, const TCHAR* CmdLine) const
{
bool Result = false;
#if (!WITH_EDITOR || IS_MONOLITHIC)
if (!FParse::Param(CmdLine, TEXT("NoPak")))
{
TArray PakFolders;
GetPakFolders(CmdLine, PakFolders);
Result = CheckIfPakFilesExist(Inner, PakFolders);
}
#endif
return Result;
}
但是,通过调试,我们会发现,其实在启动阶段,engin 已经创建了 FPakPlatformFile :
/**
* Look for any file overrides on the command line (i.e. network connection file handler)
*/
bool LaunchCheckForFileOverride(const TCHAR* CmdLine, bool& OutFileOverrideFound)
{
OutFileOverrideFound = false;
// Get the physical platform file.
IPlatformFile* CurrentPlatformFile = &FPlatformFileManager::Get().GetPlatformFile();
// Try to create pak file wrapper
{
IPlatformFile* PlatformFile = ConditionallyCreateFileWrapper(TEXT("PakFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
PlatformFile = ConditionallyCreateFileWrapper(TEXT("CachedReadFile"), CurrentPlatformFile, CmdLine);
if (PlatformFile)
{
CurrentPlatformFile = PlatformFile;
FPlatformFileManager::Get().SetPlatformFile(*CurrentPlatformFile);
}
}
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;
}
// Don't delete the platform file. It will be automatically deleted by its module.
WrapperFile = nullptr;
}
}
else
{
// Make sure it won't be used.
WrapperFile = nullptr;
}
return WrapperFile;
}
IPlatformFile* FPlatformFileManager::GetPlatformFile(const TCHAR* Name)
{
IPlatformFile* PlatformFile = NULL;
// Check Core platform files (Profile, Log) by name.
if (FCString::Strcmp(FLoggedPlatformFile::GetTypeName(), Name) == 0)
{
static TUniquePtr AutoDestroySingleton(new FLoggedPlatformFile());
PlatformFile = AutoDestroySingleton.Get();
}
#if !UE_BUILD_SHIPPING
else if (FCString::Strcmp(FPlatformFileOpenLog::GetTypeName(), Name) == 0)
{
static TUniquePtr AutoDestroySingleton(new FPlatformFileOpenLog());
PlatformFile = AutoDestroySingleton.Get();
}
#endif
else if (FCString::Strcmp(FCachedReadPlatformFile::GetTypeName(), Name) == 0)
{
static TUniquePtr AutoDestroySingleton(new FCachedReadPlatformFile());
PlatformFile = AutoDestroySingleton.Get();
}
else if (FModuleManager::Get().ModuleExists(Name))
{
class IPlatformFileModule* PlatformFileModule = FModuleManager::LoadModulePtr(Name);
if (PlatformFileModule != NULL)
{
PlatformFile = PlatformFileModule->GetPlatformFile();
}
}
return PlatformFile;
}
也就是说,Engine 一开始就激活了 PakFileModule ,只是没有加入到 PlatformManager 中去。
因此我们可以用如下代码实现在责任链中加入 FPakPlatformFile :
FPakPlatformFile* GameHotPatchServer::CheckAndInitPakPlatform()
{
FPakPlatformFile* HandlePakPlatform = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
if (!HandlePakPlatform)
{
UE_LOG(LogTemp, Warning, TEXT("CheckAndInitPakPlatform FPakPlatformFile == NULL"));
#if WITH_EDITOR
IPlatformFileModule* PlatformFileModule = FModuleManager::LoadModulePtr(FPakPlatformFile::GetTypeName());
if (PlatformFileModule != NULL)
{
HandlePakPlatform = (FPakPlatformFile*)(PlatformFileModule->GetPlatformFile());
IPlatformFile& TopmostPlatformFile = FPlatformFileManager::Get().GetPlatformFile();
const TCHAR* Name = TEXT("");
for (IPlatformFile* ChainElement = &TopmostPlatformFile; ChainElement; ChainElement = ChainElement->GetLowerLevel())
{
UE_LOG(LogTemp, Warning, TEXT("CheckAndInitPakPlatform 寻找合适插入位置 Name = %s"), ChainElement->GetName());
if (ChainElement->GetLowerLevel() == NULL)
{
HandlePakPlatform->Initialize(ChainElement, TEXT(""));
HandlePakPlatform->InitializeAfterSetActive();
Name = ChainElement->GetName();
break;
}
}
for (IPlatformFile* ChainElement = &TopmostPlatformFile; ChainElement; ChainElement = ChainElement->GetLowerLevel())
{
UE_LOG(LogTemp, Warning, TEXT("CheckAndInitPakPlatform 寻找被插队的节点 Name = %s"), ChainElement->GetName());
if (ChainElement->GetLowerLevel() != NULL && FCString::Stricmp(ChainElement->GetLowerLevel()->GetName(), Name) == 0 && FCString::Stricmp(FPakPlatformFile::GetTypeName(), Name) != 0 )
{
UE_LOG(LogTemp, Warning, TEXT("CheckAndInitPakPlatform 寻找到了需要插入的节点 Name = %s OldGetLowerLevel = %s NewLowerLevel = %s"), ChainElement->GetName(), ChainElement->GetName(), HandlePakPlatform->GetName());
ChainElement->SetLowerLevel(HandlePakPlatform);
break;
}
}
if (TopmostPlatformFile.GetLowerLevel() == NULL)
{
UE_LOG(LogTemp, Warning, TEXT("CheckAndInitPakPlatform 没寻找到被插队的节点 HandlePakPlatform 被放在最上层!"));
FPlatformFileManager::Get().SetPlatformFile(*HandlePakPlatform);
}
}
#endif
}
else
{
UE_LOG(LogTemp, Warning, TEXT("CheckAndInitPakPlatform FPakPlatformFile is OK"));
}
return HandlePakPlatform;
}
上面代码实现了如下功能:
- 拿到Engine 一开始创建的 PakPlatformFile
- 按顺序插入到合适的位置
有人要问了,如果我不拿已经创建好的,而自己新建一个 PakPlatformFile 然后加入到责任链中会怎样?
运行时没什么问题,在Unreal Editor 关闭时会崩溃,因为 PakFileModule 销毁时会调用 PlatformFileManager 中的 RemovePlatformFile (已经被调用过一次)导致 check error。
总结
在 Unreal Editor 模式下,可以通过将 PakPlatformFile 加入责任链的方式来模拟热更新的方式,从而减少了调试成本。