如何在 Editor 模式下调试 mount pak ?

概括:

做为一个商业项目,能快速响应玩家需求,及时修复项目BUG,是其成功的基本要素。

和所有的项目一样,从需求提出到最终呈现结果给用户,都要经历许多岗位、部门、公司间的沟通和协着。如何在技术实行,最快流程、最小感知的更新流程,就是一个重要课题。

问题提出:

unreal 有一套文件系统,让开发者能按照版本差异,封装出差异 pak 文件,再让线上运行中的应用根据触发条件下载对应的 pak ,实现热更新功能。
那么在工作过程中,我有没有办法来模拟线上运行环境加载和挂载(mount) pak 的过程?目前在 unreal 中是直接在 editor 中没有启动 FPakPlatformFile ,也就意味着无法在 editor 模式下进行 mount 操作了。

Unreal 的文件模式

Unreal 责任链初始化流程.png

如上图,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 加入责任链的方式来模拟热更新的方式,从而减少了调试成本。

你可能感兴趣的:(如何在 Editor 模式下调试 mount pak ?)