概括:
做爲一個商業項目,能快速響應玩家需求,及時修復項目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<FString> 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<IPlatformFile> AutoDestroySingleton(new FLoggedPlatformFile());
PlatformFile = AutoDestroySingleton.Get();
}
#if !UE_BUILD_SHIPPING
else if (FCString::Strcmp(FPlatformFileOpenLog::GetTypeName(), Name) == 0)
{
static TUniquePtr<IPlatformFile> AutoDestroySingleton(new FPlatformFileOpenLog());
PlatformFile = AutoDestroySingleton.Get();
}
#endif
else if (FCString::Strcmp(FCachedReadPlatformFile::GetTypeName(), Name) == 0)
{
static TUniquePtr<IPlatformFile> AutoDestroySingleton(new FCachedReadPlatformFile());
PlatformFile = AutoDestroySingleton.Get();
}
else if (FModuleManager::Get().ModuleExists(Name))
{
class IPlatformFileModule* PlatformFileModule = FModuleManager::LoadModulePtr<IPlatformFileModule>(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<IPlatformFileModule>(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 加入責任鏈的方式來模擬熱更新的方式,從而減少了調試成本。