如何在 Editor 模式下調試 mount pak ? 概括: 問題提出: Unreal 的文件模式 總結

概括:

做爲一個商業項目,能快速響應玩家需求,及時修復項目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 加入責任鏈的方式來模擬熱更新的方式,從而減少了調試成本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章