先来看一下UE4文件系统的类组成情况:
我们一个个类来看。
这里面类组成大致可以分为三大类:
- 物理文件类:UE4封装的在各个平台下直接对文件进行操作的类,主要是IPhysicalPlatformFile及其子类;
- 包装文件类:UE4封装的对特定情况的文件进行操作的类,如对pak包的处理,包装文件类不会直接对文件进行操作,而是通过其持有的底层对象来进行文件操作,包装文件类持有的底层对象也可以是另一个包装文件类,这样一层层的持有对象,各个类就链接成了一条链,当链头接收到要处理的文件若自己能处理则直接处理,若自己无法处理则抛给自己持有的底层对象,底层对象重复这个操作直到有一个对象可以处理则停止,这条链的链尾就是一个物理文件对象,这就是UE4文件系统的责任链模式。包装文件类主要包括继承自IPlatformFile的除IPhysicalPlatformFile类及其子类以外的其他类;
- 平台文件管理器:FPlatformFileManager用于管理注册的平台文件类及其组成的文件操作链。
参考博客:
芭蕉不解的博客、FlyingTree、代码质疑人生、Khcys_、dhb
一、IPlatformFile
IPlatformfile是文件类的基类,是一个UE4预定义的C++接口,是一个顶层类不继承任何类,所以IPlatformFile类及其子类均不支持反射和垃圾回收,也就是说这些类的指针管理需要我们自己手动管理,析构时需要手动delete对象,以防内存泄漏。
作为基类IPlatformFile的主要作用是用于文件操作链中的多态和提供一些通用方法与属性,如:初始化,判断文件是否存在,读取、写入、移动、删除、拷贝文件等等,其中需要注意的是virtual bool Initialize(IPlatformFile* Inner, const TCHAR* CmdLine) = 0; 在IPlatformfile类中是一个纯虚函数,在子类使用其作初始化时,对于包装文件类来说,第一个参数是他所指向的下一个文件类对象。对于物理文件类,第一个参数只能是空的; 第二个参数是命令行,部分文件类会从这里去解析一些参数。
二、IPhysicalPlatformFile
IPhysicalPlatformFile是物理文件类的基类,对SetLowerLevel函数进行了屏蔽,使其子类都无法访问SetLowerLevel函数,因为对于文件操作链来说,链尾一定是一个物理文件类,所以不需要再设置底层对象。
源码相当简单:
/**
* Common base for physical platform File I/O Interface
**/
class CORE_API IPhysicalPlatformFile : public IPlatformFile
{
public:
//~ Begin IPlatformFile Interface
virtual bool ShouldBeUsed(IPlatformFile* Inner, const TCHAR* CmdLine) const override
{
return true;
}
virtual bool Initialize(IPlatformFile* Inner, const TCHAR* CmdLine) override;
virtual IPlatformFile* GetLowerLevel() override
{
return nullptr;
}
virtual void SetLowerLevel(IPlatformFile* NewLowerLevel) override
{
check(false); // can't override wrapped platform file for physical platform file
}
virtual const TCHAR* GetName() const override
{
return IPlatformFile::GetPhysicalTypeName();
}
//~ End IPlatformFile Interface
};
在源码中IPhysicalPlatformFile::GetLowerLevel直接返回了一个nullptr。
查看FWindowsPlatformFile在UE4.26中的源码发现FWindowsPlatformFile.h是一个空的头文件,而整个FWindowsPlatformFile类都被定义在FWindowsPlatformFile.cpp中,这就谢绝了我们以常规的方式使用FWindowsPlatformFile这些类了,FAndriodPlatformFile类更是找都找不到,UE4这么做的意图已经很明显了,UE4是不想让开发者直接使用这些底层类的,这也是为什么UE4要提供一个FPlatformFileManager类的原因。
三、FPlatformFileManager
PLatformFileManager类就是用UE4用于文件系统的跨平台用的,在引擎的主循环的FEngineLoop::PreInit函数中会调用FEngineLoop::PreInitPreStartupScreen,然后通过LaunchCheckForFileOverride函数检测是否需要设置当前文件系统,不需要设置则将引擎的文件系统设置成默认物理文件系统,如果需要设置则根据配置设置对应的文件系统,并将这个文件类的LowerLevel设置成对应的物理文件系统。
FPlatformFileManager提供GetPlatformFile函数返回一个IPlatformFile指针,通过多态来调用接口的具体实现,可以忽略掉底层不同平台类的文件操作实现,从而达到跨平台的效果。
IPlatformFile的文件操作依然是比较底层的操作,如IPlatformFile::OpenRead和IPlatformFile::OpenWrite函数返回的都是一个IFileHandle文件操作句柄,通过这个句柄才能对文件做跟底层的读写,如IFileHandle::Read和IFileHandle::Write函数都是直接读写的字节码。
IPlatformFile主要是提供对文件的整体操作,如移动、删除、拷贝、读取与设置文件属性,寻找文件,判断文件是否存在,获取文件大小,同时也提供对文件夹层面的操作,如创建、删除、复制、遍历文件夹等等。
而对于常规文件的读写UE4提供了一个更高层的类FileHelper来操作,对目录路径的操作UE4提供了FPaths,对配置文件的操作UE4提供了GConfig,读取Json文件UE4提供了内置的Json读写工具链。这些我们后面再看,先把IPlatformFile的子类过完。
四、FPakPlatformFile
在IPlatFormFile派生的所有的包装文件类当中FPakPlatformFile是最重要也是最常用的,FPakPlatformFile是UE4专门用于Pak文件读取的包装文件类。
1.什么是Pak文件
pak文件,又称pak包,是UE4用于更新资源(包括热更新)的一种文件格式,UE4将多个文件合并到一个pak文件中,通过pak文件来更新资源,pak文件不仅能够装载UE4的资源文件,如:uasset、umap等,也能够装载非资源文件,如:xml、json、txt等,除了文件,pak文件还可以包含一些额外的信息,如:pak文件的加密情况,pak的版本等等。
要想对pak文件进行操作,我们首先要先获取pak文件。
pak文件的生成分为两步,烘培与打包。
2.Cook资源(烘焙资源)
为什么要烘焙资源
由于程序运行的平台多种多样,而不同平台有着各自的资源格式,所以在创建Pak文件之前必须先烘焙对应平台的资源才行,UE4提供UE4Editor-Cmd.exe工具来提供资源烘焙,UE4Editor-Cmd.exe可以直接在cmd命令。
<引擎路径>\Engine\Binaries\Win64\UE4Editor-Cmd.exe <项目路径>\RobotEngine.uproject -run=Cook -TargetPlatform=<平台类型> -fileopenlog -unversioned -abslog=<日志输出路径> -stdout -CrashForUAT -unattended -NoLogTimes -UTF8Output
烘培完的资源会存储在<项目文件夹>/Saved/Cooked/<对应的平台名称>/<项目名称>/Content/<对应的目录>
烘焙所支持的平台类型
UE4的资源烘焙自持目前大部分主流平台:
当然,如果我们只需要烘焙Windows平台的资源,UE4直接提供了烘焙按钮
2.Pak文件的打包
UE4提供了一个创建Pak文件的工具—UnrealPak.exe供我们使用,我们可以直接从cmd命令行运行UnrealPak.exe来对指定文件创建Pak包。
<引擎路径>\Engine\Binaries\Win64\UnrealPak.exe <pak文件路径> -Create=<项目路径>\RobotEngine\Saved\Cooked\Android_ASTC\RobotEngine\Content\Comps\<cook资源文件所在的文件夹> -compress
这里有几点需要注意,pak文件路径是我们要存放创建出来的pak文件的路径,如:D:\PAK\mypak.pak,cook资源文件所在文件夹即cook后的uasset文件所在目录,切记不是文件路径,因为目录下可以包含多个资源文件,其中Andriod_ASTC是对应平台类型的文件夹,需要实际根据cook的平台选择对应的文件夹。
-compress 表示文件打包pak时进行压缩。
UnrealPak可以封装一个指定文件到Pak文件中,也可以封装一个指定文件夹下的所有文件到Pak文件中,还可以封装一个文件下中指定的多个文件夹和文件的组合到Pak文件中。
比如:
指定单个目录打包Pak
D:\UE_4.26_SourceCode\UnrealEngine-4.26\Engine\Binaries\Win64\UnrealPak.exe D:\UE4\Paks\PakActor.pak -create=D:\UE4\Unkown\Unkown\Config -compress
指定多个目录和文件的组合打包Pak
D:\UE_4.26_SourceCode\UnrealEngine-4.26\Engine\Binaries\Win64\UnrealPak.exe D:\UE4\Paks\PakActor.pak -create=D:\UE4\Unkown\Unkown\Config\A.txt -compress
txt文件中目录和文件的组合格式如下:
D:\Goulandis\UE4\Paks\Paks_0104143216
D:\Goulandis\UE4\Paks\Paks_0104145559
D:\Goulandis\UE4\Paks\Json.json
路径与路径之间必须用换行符隔开,如果存在不可用路径则文件打包失败。
指定单个文件打包Pak
由于-create=只能接文件和目录,若是目录,则打包目录下的所有文件进Pak包,如果接的是文件,则会读取文件内容,将文件中的内容当作要打包的路径,所以直接指定文件路径的指令形式是无法打包单个文件的,因此我们还是需要使用文本来指定单个文件的路径的形式来打包单个文件。
-create可以读取常用的文本格式,如txt,ini,json等,只要里面内容符合格式要求即可读取。
小知识
当我们在打包引用程序的时候勾选Edit\Project Setting\Packaging\Use Pak File
则打包出来的资源将全部封装进一个独立的Pak包里面,pak包保存在<程序目录>/<项目名称>/Content/Paks/ 目录下,如果不勾选那么资源路径情况和编辑时一样,并且打包出来的资源也依旧是.uasset资源。由于pak文件可以加密而uasset不能加密,所以将资源封装进pak文件中有利于程序的安全性。
3.Pak文件的挂载
首先我们需要明白一个机制,就是UE挂载Pak文件,仅仅是引擎内部注册一个Pak挂载点对应的文件夹,这样当引擎去寻找资源的时候就知道有这么一个地址可以去搜索资源,而在实际的物理文件中这个文件夹是不存在的,由此可以知晓,Pak文件挂载之后并不能直接使用Pak文件中资源,因为资源仍然是一个虚拟文件夹下的物理文件,还未加载到内存中。
然后创建一个类作为挂载代码的载体,这个载体可以AActor也可以是UObject,甚至可以是自定义的C++类。这里我使用一个UObject类来作为载体。
先上源码,后面再做解析
//.h
#pragma once
#include "IPlatformFilePak.h"
#include "GenericPlatform/GenericPlatformFile.h"
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "PakExpand.generated.h"
UCLASS(Blueprintable,BlueprintType)
class UNKOWN_API UPakExpand : public UObject
{
GENERATED_BODY()
private:
FPakPlatformFile* HandlePakPlatform;
IPlatformFile* HandleOriginPlatform;
FPlatformFileManager* PlatformFileManager;
private:
~UPakExpand();
public:
UPakExpand();
UFUNCTION(BlueprintCallable,Category="PakExpand")
bool Mount(const FString PakFilePath);
};
//.cpp
UPakExpand::~UPakExpand()
{
PlatformFileManager = nullptr;
HandleOriginPlatform = nullptr;
HandlePakPlatform = nullptr;
}
UPakExpand::UPakExpand()
{
//获取平台文件链接管理器
PlatformFileManager = &FPlatformFileManager::Get();
//获取平台I/O接口,用于操作平台文件
HandleOriginPlatform = &PlatformFileManager->GetPlatformFile();
}
bool UPakExpand::Mount(const FString PakFilePath)
{
//判断文件时候是pak文件
if (!PakFilePath.EndsWith(".pak"))
{
return false;
}
if (!HandlePakPlatform)
{
//创建Pak平台文件系统
HandlePakPlatform = new FPakPlatformFile();
}
//使用平台接口初始化Pak文件平台包装器
HandlePakPlatform->Initialize(HandleOriginPlatform, TEXT(""));
//将PakPlatformFile设置到最顶层,查找文件时优先查找PakPlatformFile内的文件
PlatformFileManager->SetPlatformFile(*HandlePakPlatform);
//判断文件是否存在
if (!HandleOriginPlatform->FileExists(*PakFilePath))
{
PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);
return false;
}
//创建pak对象
TSharedPtr<FPakFile> PakFile = MakeShareable<FPakFile>(new FPakFile(HandlePakPlatform, *PakFilePath, false));
if (!PakFile)
{
PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);
return false;
}
//获取pak文件的无后缀文件名
FString PakName = GetPakFileName(PakFilePath);
TArray<FString> ExistPakFiles;
//查询已挂载的所有pak包名称
HandlePakPlatform->GetMountedPakFilenames(ExistPakFiles);
//判断当前pak包是否已挂载
if (ExistPakFiles.Find(PakFilePath) >= 0)
{
PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);
return false;
}
int32 Pos = PakFile->GetMountPoint().Find("Content/");
FString MountPoint = PakFile->GetMountPoint().RightChop(Pos);
MountPoint = FPaths::ProjectDir() + MountPoint;
PakFile->SetMountPoint(*MountPoint);
//使用pak包记录的挂载点挂载pak包
if (!HandlePakPlatform->Mount(*PakFilePath, 0, *PakFile->GetMountPoint()))
{
PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);
return false;
}
PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);
return true;
}
这里有一些类和函数需要说明一下:
- IPlatformFile:这是UE4对文件操作的最基础的类,定义了对文件进行操作的相关方法,UE4自己单独封装一个文件操作类而不是直接用C++的文件操作,是UE4为了提供平台的可移植性才建立这么一个更高层的I/O接口;
- FPakPlatformFile:这个类是UE4对pak文件操作的一个封装类,派生与IPlatformFile类;
- FPlatformFileManager:这个类是UE4用于获取不同平台的物理文件的封装,Get()方法是获取类自身的实例,GetPlatformFile()方法是获取文件操作链的链头,如果没有找到则返回nullptr;
- Initialize():初始化接口,第一个参数为FPakPlatformFile类指向的下一个文件类对象,我们这里是需要指向当前平台,第二参数是一个命令行,部分文件类会从这里解析一些参数,我们这里没有命令,所以用空字符串;
- SetPlatformFile():可以理解为想UE4文件系统设置Initialize()初始化的配置;
- GetMountedPakFilenames():获取已经挂载过的pak文件,参数为一个FString数组;
- GetMountPoint():获取pak文件的挂载点,这个挂载点是直接存储在pak文件中的,pak文件的结构具体将弘竣的开发笔记;
- RightChop():返回字符串指定位置右侧的字符串;
- SetMountPoint():设置挂载点,pak文件的挂载必须得有挂载点,否则UE4就无法找到pak文件,挂载点就是pak文件在UE4中上级目录;
- Mount():挂载pak文件;
这里有一点需要注意的是,PlatformFileManager->SetPlatformFile(*HandlePakPlatform);设置文件系统不可以在构造函数中执行否则打包会报错。
到这里一个pak文件的挂载就完成了。
里面的一些坑
在网上搜了相当多的UE4pak挂载的文章,里面大多数使用共享指针来指向FPakPlatformFile,我在UE4.26中在未勾选Edit\Project Setting\Packaging\Use Pak File的情况下,不会有任何问题,但是在勾选了的情况下会在退出游戏时报:
具体原因暂不清楚,所以我上面的源码一律没有使用共享指针,而是选择了C++指针,自己去管理指针。
4.Pak文件的加载
资源加载
想要加载Pak文件中的资源进入内存我们就只能使用C++的方式来加载,我以加载一个AActor为例,先上源码。
AActor* UPakExpand::SpawnActorFromPak(FString ClassRef, FTransform Transform, bool& Result)
{
if (!HandlePakPlatform)
{
Result = false;
return nullptr;
}
PlatformFileManager->SetPlatformFile(*HandlePakPlatform);
UClass* uclass = StaticLoadClass(AActor::StaticClass(), NULL, *ClassRef);
if (!uclass)
{
Result = false;
return nullptr;
}
AActor* Actor = GetWorld()->SpawnActor(uclass,&Transform);
if (!Actor)
{
Result = false;
return nullptr;
}
if (!HandleOriginPlatform)
{
Result = false;
return nullptr;
}
PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);
Result = true;
return Actor;
}
- PlatformFileManager->SetPlatformFile(*HandlePakPlatform);将文件系统切换到Pak文件系统,这样才能读取到Pak包中的资源,因为引擎默认使用的是物理文件系统,而Pak包挂载的不是物理文件,所以使用物理文件系统是无法读取到Pak文件内的资源的。
- PlatformFileManager->SetPlatformFile(*HandleOriginPlatform);将文件系统切换回原来的文件系统,实际上切不切回去是没有影响的,因为SetPlatformFile是在当前的文件操作链的链头再加一个操作节点,然后以这个新的操作节点作为文件操作链的链头,当加载资源时会从操作链的链头开始搜索资源,当然,当我们不需要再加载Pak包中的资源时,切回去可以使操作链少一个节点,资源的寻找速度会更快一点,所以这里我还是切回去。
里面的一些坑
这里有一点是需要注意的,前面有说过挂载只是在引擎内部注册了一个虚拟文件夹,这个文件夹在实际的物理目录中是不存在的,所以如果我们在编辑状态下直接使用要打成Pak文件的这些资源,在挂载了Pak之后引擎依然是找不到这些资源的,原因我猜是编辑状态下使用资源保存的路径应该是封装整个程序资源的Pak文件的路径即这个Pak文件:
这里面有一个比较隐晦的状况,就是如果我们打包的时候不排除需要Pak的文件夹,也就是说打包的时候把需要Pak的文件一并打进Unkown-WindowsNoEditor.pak这个Pak文件中,在实际运行的效果是,资源是可以被加载的,这是因为引擎加载的资源是Unkown-WindowsNoEditor.pak中的资源,而非我们自己挂载的Pak包中的资源,我们可通过修改Pak包中的资源内容,重新运行来证明这个问题。而如果我们把需要打Pak的文件夹不打包进Unkown-WindowsNoEditor.pak中,那么资源就识别不出来了。如果资源只是在Pak包自己内部使用则没有这个问题。
我们通过一个例子也许会更好理解一些。
第一步:
在Content文件夹下创建一个需要Pak的文件夹命名为DLCs,在里面创建两个资源,给Pak包外部使用的PakActor和给Pak包内部使用的PakWidget。
两个资源的内部情况如下:
PakActor:
在PakActor中去创建PakWidget,注意这里是直接在编辑状态下去加载PakWidget,并且PakWidget和PakActor在同一个文件夹下,即将来打Pak包后,两个资源在同一个Pak包中。
PrintDebugLog是自己封装的一个打印日志的函数。
PakWidget:
第二步:
在另一个文件夹下去创建一个GameInstance,使用GameInstance去挂载并加载资源,这里我创建一个名为UnkGameInstance的GameInstance,并放在Content/GamePlay文件夹下,蓝图内容如下:
这里我们先使用在编辑状态下直接使用UE4原生APISpawnActorFromClass 来加载PakActor,
好,现在我们不排除Content/DLCs文件夹,在Project Setting/Packaging/Packaging/Directories to never cook项不添加文件夹,勾选Use Pak File。
然后打一个Windows程序出来,然后把Content/DLCs文件夹打一个Pak包出来,我这里命名为PakActor.pak,并将Pak包放入程序目录/Content/DLCs/目录下。
可以看到在勾选了UsePakFile时引擎自动生成了一个Paks文件夹下的<项目名>-<平台类型>.pak 的文件,我这里是Unkown-WindowsNoEditor.pak文件,而DLCs文件夹是没有的,所以我们要自己创建一个,并把打包出来的Pak文件放进去,需要注意的是Pak文件的命名需要和加载时的名称要一致。
然后我们跑起来看一下。
PakActor和PakWidget资源是加载出来了的,此时我们无法分辨加载的到底是Unkown-WindowsNoEditor.pak中的资源还是PakActor.pak中的资源,因为在两个pak包中都存在一份/Game/DLCs/PakActor.uasset和/Game/DLCs/PakWidget.uasset资源。
为了识别资源是哪个Pak包中的,有两种方式验证,先看第一种
现在我们修改一下PakActor蓝图的打印的内容:
把this is PakActor修改成this is PakActor2,然后再打一个Pak包出来,同样命名为PakActor.pak并放到Content/DLCs/目录下替换掉之前的pak包,然后我们再跑一次看看,打印内容有没有变化。
可以看到,打印的内容依旧是this is PakActor,这说明加载到PakActor不是PakActor.pak中的资源而是Unkown-WindowsNoEditor.pak中的资源,因为我们没有重新打包程序,所以Unkown-WindowsNoEditor.pak中的PakActor.uasset打印依然是this is PakActor,而PakActor.pak中PakActor.uasset打印已经修改成了this is PakActor2。
为了严谨起见,我们再用第二种方式验证一遍。
现在我们在打包时不把Content/DLCs/文件夹打进Unkown-WindowsNoEditor.pak中,在Project Setting/Packaging/Packaging/Directories to never cook项中把Content/DLCs/添加进去。
然后我们再打包来跑一次看看。
可以看到没有任何打印出来,并且PakWidget也没有加载出来,我们再看一下日志,日志存储在<程序目录>/<项目名称>/Saved/Logs/ 目录下。
日志打印了PakActor is not valid这说明PakActor未识别到,由于此时Unkown-WindowsNoEditor.pak中不包含PakActor.uasset资源,只有PacActor.pak中包含,所以可以验证在编辑状态下使用Pak中的资源引擎保存的路径和实际Pak挂载的路径是不一致的(尽管匹配到物理路径上两者是一样的,个人猜想编辑状态下使用的是物理路径,而挂载使用的是虚拟路径,纯个人猜想)。
好,现在我们换一种方式来加载Pak包中的资源,使用前面我们封装好的UPakExpand::SpawnActorFromPak函数,通过C++的方式来加载,我们重新修改一下UnkGameInstance的蓝图内容。
重新打一个包出来,这一次我们依旧不把Content/DLCs/文件夹打进Unkown-WindowsNoEditor.pak,重新打一份程序,再跑一次看看。
可以看到,我们对PakActor的修改也生效了,嗯,这就是这个坑的解释。
5.Pak文件的加密
UE提供了UnrealPak工具提供了对Pak文件的加密功能,通过Project Settings/Crypto/Encryption/Generate New Encryption Key可以自动生成用于加密Pak文件的密钥,并且这些配置信息会保存在<项目目录>/Config/DefaultCrypto.ini 文件中。
使用预定义文件加密Pak文件
在我们对项目进行了打包操作后,UE会根据Project Settings/Crypto/Encryption/中配置生成一个<项目目录>/Saved/Cooked/<平台名称>/<项目名称>/Metadata/Crypto.json文件,这个文件使用Json的格式保存了配置中的信息,以便UnrealPak使用。具体加密指令如下:
D:\UE_4.26_SourceCode\UnrealEngine-4.26\Engine\Binaries\Win64\UnrealPak.exe D:\UE4\Paks\PakActor.pak -create=D:\UE4\Unkown\Unkown\Saved\Cooked\WindowsNoEditor\Unkown\Content\DLCs -encrypt -encryptindex -compress -cryptokeys=D:\UE4\Unkown\Unkown\Saved\Cooked\WindowsNoEditor\Unkown\Metadata\Crypto.json
- -encrypt : 加密Pak文件;
- -encryptindex : 加密索引,解包时不提供Key无法解包,如果不使用这个参数,解包时就不需要Key也可以解包;
- -compress : 对Pak文件数据进行压缩;
- -cryptokeys : 指向加密需要的带密钥的Json文件;
更多指令可以参考代码质疑人生的这篇博客。
按理说在Project Settings/Crypto/Encryption/中应该可以对-encrypt和-encryptindex参数进行配置,且在Crypto.json中也都有字段存储,但是似乎在使用UnrealPak打包时依旧需要手动指定,否则加密无效。
使用自定义文件加密Pak
-cryptokeys除了可以读取UE打包时自动生成的json文件中的密钥来加密Pak,也可以使用自定义的json文件来加密Pak,只需要json中有一个EncryptionKey 对象,对象中有一个Key 字段即可,如:
{
"EncryptionKey":
{
"Key":"xlTq7RXTF5yhLRkoJd8m9tQMdjxsalROP6DycVf+UNc="
}
}
Json可以有其他的自定义字段。
6.Pak文件的解密
使用UnrealPak解密
使用UnrealPak解密基本和加密是反着来的,当然加密和解密必须使用同一个Key,否者解密会失败。
D:\UE_4.26_SourceCode\UnrealEngine-4.26\Engine\Binaries\Win64\UnrealPak.exe D:\UE4\Paks\PakActor.pak -Extract D:\UE4\Paks\PakContent -cryptokeys=D:\UE4\Unkown\Unkown\Saved\Cooked\WindowsNoEditor\Unkown\Metadata\Crypto.json
这个指令会把Pak中的文件全部解包成源文件。
挂载时解密
在代码中解密Pak包需要用到UE内置的FPakEncryptionKeyDelegate委托,且UE在FCoreDelegates类中已经预制了FPakEncryptionKeyDelegate的对象,可以通过FCoreDelegates::GetPakEncryptionKeyDelegate()函数获取。在UE通过Mount挂载Pak包是会判断Pak有没有加密,如果加密了则使用委托调用绑定的解密函数来获取密钥进而对Pak文件进行密钥验证。
//Mount重载,解密挂载
bool UPakExpand::Mount(const FString PakFilePath, const FString CryptoJsonPath)
{
if (CryptoJsonPath.IsEmpty())
{
return Mount(PakFilePath);
}
else
{
SetEncryptJsonPath(CryptoJsonPath);
//UE4预制委托,当挂载Pak包的时候自动调用
FCoreDelegates::GetPakEncryptionKeyDelegate().BindUObject(this, &UPakExpand::UnEncrypt);
return Mount(PakFilePath);
}
}
//设置存储密钥的文件路径,之所以要专门设置密钥文件的路径,是因为GetPakEncryptionKeyDelegate()返回的委托是一个返回值为空,参数为uint8*的委托,无法直接往UnEncrypt函数传入文件路径
void UPakExpand::SetEncryptJsonPath(const FString CryptoJsonPath)
{
if (FPaths::FileExists(CryptoJsonPath))
{
EncryptJsonPath = CryptoJsonPath;
}
}
//读取json文件
FString UPakExpand::ReadEncryptKeyStrFromJson()
{
if (!FPaths::FileExists(EncryptJsonPath))
{
return FString(TEXT(""));
}
FString JsonStr;
FFileHelper::LoadFileToString(JsonStr, *EncryptJsonPath);
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());
TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonStr);
FString KeyStr;
if (FJsonSerializer::Deserialize(JsonReader, JsonObject))
{
TSharedPtr<FJsonObject> EncryptionKey = JsonObject->GetObjectField(TEXT("EncryptionKey"));
KeyStr = EncryptionKey->GetStringField(TEXT("Key"));
return KeyStr;
}
return FString(TEXT(""));
}
//密钥转码将ascii码下表现为字符串的密钥转换成二进制码
void UPakExpand::UnEncrypt(uint8* Key)
{
FString KeyStr = ReadEncryptKeyStrFromJson();
TArray<uint8> KeyBase64Ary;
FBase64::Decode(KeyStr, KeyBase64Ary);
FMemory::Memcpy(Key, KeyBase64Ary.GetData(), FAES::FAESKey::KeySize);
}
Pak文件的解密实际上也不难,我们对第三小节Pak挂载的Mount函数进行函数重载,再挂载Pak文件之前先对Pak文件进行解密,这里之所以使用文件的形式来加载密钥而不是直接将密钥定义再代码里是为了更灵活的使不同的Pak包可以使用不同的密钥来加密解密。
7.Pak加载的优先级
当同一份资源同时存在于两个pak文件中时,就涉及到资源加载的优先级问题了。
目录优先级
我们可以直接从源码入手
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;
}
源码的注释是这样的:
/*
* Hardcode default load ordering of game main pak -> game content -> engine content -> saved dir
* would be better to make this config but not even the config system is initialized here so we can't do that
*/
static int32 GetPakOrderFromPakFilePath(const FString& PakFilePath);
UE会在游戏启动时自动挂载某些目录下的Pak文件,分别是<项目目录>/Content/Paks,<引擎目录>/Content/Paks,<项目目录>/Saved/Paks
这里的项目目录和引擎目录均是打包后游戏根目录下的项目目录和引擎目录。
三者满足优先级:<项目目录>/Content/Paks > <引擎目录>/Content/Paks > <项目目录>/Saved/Paks ,其中最高优先级为游戏主Pak包,即在<项目目录>/Content/Paks/目录下以项目名开头,由引擎打包出来的Pak文件。
但是我在实际测试中发现实际加载顺序不是这样的,具体原因未知。
我分别在三个目录下放入一个内容相同而名字不同的三个Pak文件,通过日志看看挂载顺序:
Pak位置情况:
Pak加载日志,exe运行后会在/游戏根目录/Saved/Logs/目录下生成运行日志,其中项目名称.log 为最新的日志:
可以看到,位于<项目目录>/Saved/Paks/PakActor_3.pak最先加载,而游戏主Pak包/<项目目录>/Content/Paks/Unkown-WindowsNoEditor.pak第二加载,/<项目目录>/Content/Paks/PakActor_1.pak第三加载,/<引擎目录>/Content/Paks/PakActor_2.pak最后加载。
加载顺序似乎有出入,目前未找到原因,不过一般不影响正常游戏流程。
二级优先级
在满足目录优先级的情况下,UE还增加了Pak文件名以_P.pak 结尾的二级优先级,如:Pak_P.pak的优先级就要高于Pak.pak,而Pak_1_P.pak的优先级又高于Pak_P.pak,Pak_2_P.pak高于Pak_1_P.pak,以此类推。来实践验证一下。
前面有说过,在PakActor蓝图中创建了一个黑色的PakWidget界面,现在把PakWidget界面设置成白色,然后再打一个包,重命名为“PakActor_1_P.pak”,一并放入Content/DLCs目录下,这里还需要对挂载做一点改动,即把DLCs目录下的Pak文件都挂载起来,所以我们往UPakExpaned类中新增一个函数。
TArray<FString> UPakExpand::GetAllPakFromDir(const FString Dir, bool& Result)
{
FString PakDir(FPaths::ProjectContentDir() + Dir + TEXT("/"));
IFileManager& FileManager = IFileManager::Get();
TArray<FString> ResultList;
if (FileManager.DirectoryExists(*PakDir))
{
TArray<FString> PakList;
FileManager.FindFiles(PakList, *PakDir,TEXT("*.pak"));
for (int i = 0; i < PakList.Num(); i++)
{
FString PakFilePath(PakDir + PakList[i]);
ResultList.Add(PakFilePath);
}
Result = true;
}
if (ResultList.Num() <= 0)
{
Result = false;
}
return ResultList;
}
然后再UnkGameInstance中对挂载步骤进行改进:
Mount节点的CryptoJsonPath参数可以忽略,这是用来解密加密Pak包的。
Pak包位置:
好,运行一下试试
可以看到加载的PakActor已经是PakActor_1_P.pak中的资源了。
8.以DLC的形式进行资源更新
DLC的形式与pak包的形式不同的只在于pak包的生成方式,pak包是直接使用命令行烘培和打包的,而DLC则是使用的ProjectLauncher,最终资源都是以pak包的形式下载到本地,只是pak的形式需要我们手动写C++代码挂载,而DLC的形式将pak文件放到指定文件夹内可自动挂载并加载。
用DLC的形式需要配置两个ProjectLauncher,一个为打包本地的ProjectLauncher,一个为打包DLC的ProjectLauncher,具体配置方法见wmc的一篇博文。
五、FCachedReadPlatformFile
FCachedReadPlatformFile实现了文件的预读写逻辑,FCachedReadPlatformFile对IPlatformFile的修改主要是在OpenRead和OpenWrite的返回值中。
virtual IFileHandle* OpenRead(const TCHAR* Filename, bool bAllowWrite) override
{
IFileHandle* InnerHandle=LowerLevel->OpenRead(Filename, bAllowWrite);
if (!InnerHandle)
{
return nullptr;
}
return new FCachedFileHandle(InnerHandle, true, false);
}
virtual IFileHandle* OpenWrite(const TCHAR* Filename, bool bAppend = false, bool bAllowRead = false) override
{
IFileHandle* InnerHandle=LowerLevel->OpenWrite(Filename, bAppend, bAllowRead);
if (!InnerHandle)
{
return nullptr;
}
return new FCachedFileHandle(InnerHandle, bAllowRead, true);
}
二者的返回值不再是IFileHanle而是其子类FCachedFileHandle,预读逻辑就在FCachedFileHandle中,每次预读64k的数据到一个缓存块,这个类应该是给一些为做预读取优化的平台使用的,而像Windows和PS4这些平台本身就对文件读取做了预读取的平台,UE是默认启用FCachedReadPlatformFile的,即在文件的责任链中是没有FCachedReadPlatformFile节点的,当然我们也可以通过可通过NoCachedReadFile 参数和CachedReadFile 参数强行关闭或启用。
个人认为,FCachedReadPlatformFile的使用就是将FCachedReadPlatformFile设置进责任链中,在读取文件时上层抛到FCachedReadPlatformFile时就进行预读取处理。实践验证由于我是Windows平台还想到什么好的验证方法,也就没验证了。
六、FLoggedPlatformFile
FLoggedPlatformFile包装类会把每一次对文件的操作都通过UE_LOG使用LogPlatformFile打印到日志中去,使用方法也是将FLoggedPlatformFile对象设置进责任链中,如:
void UPakExpand::ReadFile(const FString FilePath)
{
TSharedPtr<FLoggedPlatformFile> LoggedPlatformFile = MakeShareable(new FLoggedPlatformFile());
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
LoggedPlatformFile->Initialize(PlatformFile, TEXT(""));
FPlatformFileManager::Get().SetPlatformFile(*LoggedPlatformFile);
FString FileData;
FFileHelper::LoadFileToString(FileData, *FilePath);
FileData = TEXT("this is write data");
FFileHelper::SaveStringToFile(FileData, *FilePath);
FPlatformFileManager::Get().SetPlatformFile(*PlatformFile);
}
然后在UnkGameInstance中调用,运行,打开日志搜索LogPlatformFile:
可以看到对文件的读写关闭等操作多被打印进了日志中。
七、FPlatformFileOpenLog
FPlatformFileOpenLog包装类的功能、用法和FLoggedPlatformFile类似,FPlatformFileOpenLog类只会记录文件的打开历史,并且在编辑器模式下运行使用单独的EditorOpenOrder.log文件保存,在非Shipping版本的游戏包中使用GameOpenOrder.log文件保存,在Shipping版本的有游戏包中不能使用。在编辑模式下的EditorOpenOrder.log文件保存在<项目目录>\Build\Windows\FileOpenOrder,在Game模式下的GameOpenOrder.log文件保存在<游戏目录>\Build\WindowsNoEditor\FileOpenOrder下。
现在我们把上面的ReadFile函数修改一下。
void UPlatformFileExpand::ReadFile(const FString FilePath)
{
TSharedPtr<FPlatformFileOpenLog> PlatformFileOpenLog = MakeShareable(new FPlatformFileOpenLog());
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
PlatformFileOpenLog->Initialize(PlatformFile, TEXT(""));
FPlatformFileManager::Get().SetPlatformFile(*PlatformFileOpenLog);
FString FileData;
FFileHelper::LoadFileToString(FileData, *FilePath);
DEBUGLOG(FEnumSet::DebugLogType::Log, (TEXT("Read file data:") + FileData));
FileData = TEXT("this is write data");
FFileHelper::SaveStringToFile(FileData, *FilePath);
FPlatformFileManager::Get().SetPlatformFile(*PlatformFile);
}
然后打开
不过后面的数字代表什么意思,暂时还没弄明白,无论重启多少次exe,或在一次启动中打开多次同一文件,后面的数字始终是1。
八、FNetworkPlatformFile、FCookedIterativeNetworkFile和FStreamingNetworkPlatformFile
1.FNetworkPlatformFile
这三个包装文件类都是用于实现网络文件的读取功能,当UE要打开某个文件,而文件却不存在时,则先查看是否连接了文件服务器,连接了则从文件服务器下载文件到本地然后再打开。
源码中的描述是:用于将低级文件系统重定向到服务器的包装器。
FCookedIterativeNetworkFile和FStreamingNetworkPlatformFile的初始化函数和前面的包装文件类有所不同:
virtual bool InitializeInternal(IPlatformFile* Inner, const TCHAR* HostIP) override;
不仅要指定下层文件类对象,还要指定服务器端口地址,HostIP可以使用tcp:// 开头指定tcp通信协议,使用http:// 指定http通信协议,不指定则默认使用tcp,同时可以使用+ 号分隔来指定多个Host地址,初始化后UE会将本地文件与服务器文件做出映射,以便在本地没有找到文件时能快速的找到服务器中对应的文件,同时在Tick函数中会通过PerformHeartbeat函数监测服务器文件的更新,使本地文件与服务器文件保持一致。
三者具有优先级,优先级为:FStreamingNetworkPlatformFile> FCookedIterativeNetworkFile> FNetworkPlatformFile
且在Shipping版本中被禁止使用。
不是所有的目录下的文件都会在没读取到时去文件服务器下载,在源码中有一个LocalDirectories数组记录着只会进行本地读取的目录:
// Save and Intermediate directories are always local
LocalDirectories.Add(FPaths::EngineDir() / TEXT("Binaries"));
LocalDirectories.Add(FPaths::EngineIntermediateDir());
LocalDirectories.Add(FPaths::ProjectDir() / TEXT("Binaries"));
LocalDirectories.Add(FPaths::ProjectIntermediateDir());
LocalDirectories.Add(FPaths::ProjectSavedDir() / TEXT("Backup"));
LocalDirectories.Add(FPaths::ProjectSavedDir() / TEXT("Config"));
LocalDirectories.Add(FPaths::ProjectSavedDir() / TEXT("Logs"));
LocalDirectories.Add(FPaths::ProjectSavedDir() / TEXT("Sandboxes"));
分别是引擎目录下的Binaries,Intermediate目录,项目目录下的Binaries,Intermediate,Saved/Backup,Saved/Config,Saved/Logs,Saved/SandBoxes目录。
2.FCookedIterativeNetworkFile
FCookedIterativeNetworkFile在FNetworkPlatformFile的基础上增加了,本地Pak文件绕过网络访问的机制,如在某次更新中有些Pak文件未做修改,可以不更新,尽管服务器中这些Pak文件的版本号更高了,但是由于内容未做更改所以可以使用FCookedIterativeNetworkFile直接绕过更新。
3.FStreamingNetworkPlatformFile
FStreamingNetworkPlatformFile在FNetworkPlatformFile的基础上实现了对服务器的流式访问,和FNetworkPlatformFile不能对服务器文件进行修改不同,FStreamingNetworkPlatformFile可以直接修改服务器上的文件,FStreamingNetworkPlatformFile对文件的访问操作均是直接操作的服务器文件。
这里引用dhb大佬博客的一段原话:
打开文件的OpenRead()、OpenWrite()函数使用SendOpenMessage()通知服务器打开文件,服务器会返回一个文件的句柄ID,后续对该文件的操作以这个句柄ID作为标识。在接口配套定义的FStreamingNetworkFileHandle文件句柄中,Read()、Write()等操作都被转换到对服务器发送消息,分别在SendReadMessage()与SendWriteMessage()中。这样,对文件的读取实时从服务器获得(每次网络传输以64KB的块为单位进行,进行缓存),而对文件的写入则实时发送到服务器。
九、FSandboxPlatformFile
1.什么是沙盒
FSandboxPlatformFile是UE实现的一个简单的沙盒机制,首先我们需要了解什么是沙盒:
沙盒机制是内存空间访问的一种安全机制,一个应用程序被分配到专属于自己的存储空间,程序只能在自己的空间中访问内存,不可以越过边界访问外部内存,这个区域就叫沙盒。
2.FSandboxPlatformFile的使用
FSandboxPLatformFile的使用需要在.Build.cs中添加SandBoxFile 模块,且FSandboxPlatformFile的构造函数不是公有的,需要使用FSandboxPLatformFile::Create函数来创建TUniquePtr指针来使用。
关于FSandboxPlatformFile的使用网上的资料几乎为零,在官方文档也只找到了类的描述,没办法还得自己上手撸源码。
首先先上一段自己写的代码:
void UPlatformFileExpand::ReadFile(const FString FilePath)
{
TUniquePtr<FSandboxPlatformFile> SandboxPlatformFile = FSandboxPlatformFile::Create(true);
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
SandboxPlatformFile->Initialize(&PlatformFileTmp, TEXT("-Sandbox=Unique"));
SandboxPlatformFile->SetSandboxEnabled(true);
FPlatformFileManager::Get().SetPlatformFile(*SandboxPlatformFile);
FString FileData;
FFileHelper::LoadFileToString(FileData, *FilePath);
FPlatformFileManager::Get().SetPlatformFile(PlatformFile);
}
-
Initialize:FSandboxPlatformFile的初始化需要添加命令行参数,-sandbox= ,看源码可以看出UE提供多种沙盒类型: bool FSandboxPlatformFile::Initialize(IPlatformFile* Inner, const TCHAR* CmdLine)
{
FString CommandLineDirectory;
FParse::Value( CmdLine, TEXT("-Sandbox="), CommandLineDirectory);
//...
if (LowerLevel != NULL && !CommandLineDirectory.IsEmpty())
{
//...
if( CommandLineDirectory == TEXT("User") )
{
// Special case - platform defined user directory will be used
SandboxDirectory = FPlatformProcess::UserDir();
SandboxDirectory += TEXT("My Games/");
SandboxDirectory += TEXT( "UE4/" );
bSandboxIsAbsolute = true;
}
else if( CommandLineDirectory == TEXT("Unique") )
{
const FString Path = FPaths::GetRelativePathToRoot() / TEXT("");
SandboxDirectory = FPaths::ConvertToSandboxPath( Path, *FGuid::NewGuid().ToString() );
}
else if (CommandLineDirectory.StartsWith(TEXT("..")))
{
// for relative-specified directories, just use it directly, and don't put into FPaths::ProjectSavedDir()
SandboxDirectory = CommandLineDirectory;
}
else if( FPaths::IsDrive( CommandLineDirectory.Mid( 0, CommandLineDirectory.Find(TEXT("/"), ESearchCase::CaseSensitive) ) ) == false )
{
const FString Path = FPaths::GetRelativePathToRoot() / TEXT("");
SandboxDirectory = FPaths::ConvertToSandboxPath( Path, *CommandLineDirectory );
}
else
{
SandboxDirectory = CommandLineDirectory;
bSandboxIsAbsolute = true;
}
//...
}
-Sandbox=可以指定User ,Unique ,.. ,自定义目录 等参数。
User :在用户用户目录下创建沙盒目录,如:C:/Users/admin/Documents/My Games/UE4/Unkown/GitToken.txtUnique :在项目目录/Saved目录下使用唯一的编码创建沙盒目录,如:D:/UE4/Unkown/Unkown/Saved/Sandboxes/260659334D930AEACA179EB8977A20F8/Unkown/GitToken.txt.. :在引擎目录/Binaries目录下创建沙盒目,如:D:/UnrealEngine-4.26/Engine/Binaries/Unkown/GitToken.txt自定义参数 :使用在自定义目录下作为沙盒目录,如:D:/UESandbox/Unkown/GitToken.txt 需要注意的是,这里所说的沙盒目录在实际物理文件夹下是不存在的,这里的目录只是作为一个安全边界来使用。 -
FPlatformFileManager::Get().SetPlatformFile(PlatformFile):这里之所以要把责任链的链头还原回去是因为如果我不设置回去会导致崩溃,这里就有一个疑问了,既然必须要用的时候把沙盒添加到链头,用完了有必须还原回去,那么在还原回去之后,文件依然可以使用非沙盒模式去读取,所以这里使用沙盒又有什么意义呢?
3.FSandboxPlatformFile的原理
FSandboxPlatformFile的核心就是ConvertToSandboxPath()、 ConvertFromSandboxPath(),在访问文件时FSandboxPlatformFile会直接将访问文件的路径替换为沙盒路径来使用,使程序处于沙盒模式下无法通过非沙盒路径读写文件。
十、FPlatformFileReadStats和FProfiledPlatformFile
FPlatformFileReadStats用于记录文件读取的速度,FProfiledPlatformFile用于记录文件操作得速度,不过这两个类在4.26中似乎已经废弃了,在源码中搜索不到这个类,官方文档也是个404页面。
十一、FFileHelper
FFileHelper是UE最顶的文本文件处理类了,提供一系列接口用于处理文本文件,最神奇的是FFileHelper是一个结构体。
-
BufferToString:从缓冲中读取内容到字符串,适用于比较大的文本,无法一次性把文本都读取到字符串时,使用缓冲分批读取; -
LoadFileToArray:以二进制的形式一次性加载文本全部内容到一个unit8数组; -
LoadFileToString:以字符串的形式一次性加载文本的全部内容到一个字符串中; -
LoadFileToStringArray:以字符串的形式按行加载文本到FString数组中,数组中每一个元素存储文本中一行的文本; -
LoadFileToStringArrayWithPredicate:以字符串的形式同时使用谓词对文本进行筛选后再加载到FString数组中,数组中每一个元素存储文本中一行的文本; 按照C++的用法,函数指针既可以指向一个Lambda表达式也是可以指向一个实际的函数的,指向实际的函数时只能使用静态函数; //使用Lambda表达式
FFileHelper::LoadFileToStringArrayWithPredicate(Result, *FilePath, [](const FString& Item) {return Item.Contains(TEXT("Sandbox")); });
//使用实际函数
TFunction<bool(const FString&)> Fun = UPlatformFileExpand::Filter;
FFileHelper::LoadFileToStringArrayWithPredicate(Result, *FilePath, Fun);
static bool Filter(const FString& Item)
{
return Item.Contains(TEXT("Sandbox"));
}
-
SaveArrayToFile:有两个中类型的重载,一个传入TArrayView<const uint8> 类型的参数,一个是传入TArray64<uint8> 类型的参数,二者都是TArray的变种,使用方法和TArray基本一致,只是内部构造可能有些不同; -
SaveStringToFile:把字符串保存到文本文件,可以在字符转中使用\n 来对写入的内容进行换行; -
SaveStringArrayToFile:将String数组元素按行写入到文本文件,一个元素对应一行; -
CreateBitmap:创建一个bmp图片,其中FColor*参数需要使用一个C++数组; FColor color[20];
for (int i = 0; i < 20; i++)
{
color[i] = FColor::Red;
}
FFileHelper::CreateBitmap(*FilePath, 20, 20, color);
-
GenerateNextBitmapFilename、GenerateDateTimeBasedBitmapFilename:这两个函数从源码来看就是创建一个指定后缀的唯一的文件来存储位图文件,但是并没有创建实际的文件,没搞懂使用来干嘛的; -
LoadANSITextFileToStrings:专门用来加载ANSI编码的文件; -
IsFilenameValidForSaving:判断文件是否可用。
十二、FPaths
FPaths是UE专门用于处理文件路径的封装类,一般文件访问都绕不开FPaths,FPath记录着各种常用的项目、引擎等的目录。
-
提供路径的接口: DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("LaunchDir:") + FPaths::LaunchDir()));
//> LaunchDir:D:\Goulandis\UE4\Unkown\Unkown\Intermediate\ProjectFiles/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineDir:") + FPaths::EngineDir()));
//> EngineDir:../../../Engine/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineUserDir:") + FPaths::EngineUserDir()));
//> EngineUserDir:../../../Engine/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineVersionAgnosticUserDir:") + FPaths::EngineVersionAgnosticUserDir()));
//> EngineVersionAgnosticUserDir:../../../Engine/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineContentDir:") + FPaths::EngineContentDir()));
//> EngineContentDir:../../../Engine/Content/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineConfigDir:") + FPaths::EngineConfigDir()));
//> EngineConfigDir:../../../Engine/Config/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineEditorSettingsDir:") + FPaths::EngineEditorSettingsDir()));
//> EngineEditorSettingsDir:../../../Engine/Saved/Config/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineIntermediateDir:") + FPaths::EngineIntermediateDir()));
//> EngineIntermediateDir:../../../Engine/Intermediate/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineSavedDir:") + FPaths::EngineSavedDir()));
//> EngineSavedDir:../../../Engine/Saved/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EnginePluginsDir:") + FPaths::EnginePluginsDir()));
//> EnginePluginsDir:../../../Engine/Plugins/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineDefaultLayoutDir:") + FPaths::EngineDefaultLayoutDir()));
//> EngineDefaultLayoutDir:../../../Engine/Config/Layouts/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineProjectLayoutDir:") + FPaths::EngineProjectLayoutDir()));
//> EngineProjectLayoutDir:../../../../../Goulandis/UE4/Unkown/Unkown/Config/Layouts/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineUserLayoutDir:") + FPaths::EngineUserLayoutDir()));
//> EngineUserLayoutDir:../../../Engine/Saved/Config/Layouts/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EnterpriseDir:") + FPaths::EnterpriseDir()));
//> EnterpriseDir:D:/UE_4.26_SourceCode/UnrealEngine-4.26/Enterprise/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EnterprisePluginsDir:") + FPaths::EnterprisePluginsDir()));
//> EnterprisePluginsDir:D:/UE_4.26_SourceCode/UnrealEngine-4.26/Enterprise/Plugins/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EnterpriseFeaturePackDir:") + FPaths::EnterpriseFeaturePackDir()));
//> EnterpriseFeaturePackDir:D:/UE_4.26_SourceCode/UnrealEngine-4.26/Enterprise/FeaturePacks/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EnginePlatformExtensionsDir:") + FPaths::EnginePlatformExtensionsDir()));
//> EnginePlatformExtensionsDir:../../../Engine/Platforms/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectPlatformExtensionsDir:") + FPaths::ProjectPlatformExtensionsDir()));
//> ProjectPlatformExtensionsDir:../../../../../Goulandis/UE4/Unkown/Unkown/Platforms/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("RootDir:") + FPaths::RootDir()));
//> RootDir:D:/UE_4.26_SourceCode/UnrealEngine-4.26/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectDir:") + FPaths::ProjectDir()));
//> ProjectDir:../../../../../Goulandis/UE4/Unkown/Unkown/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectUserDir:") + FPaths::ProjectUserDir()));
//> ProjectUserDir:../../../../../Goulandis/UE4/Unkown/Unkown/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectContentDir:") + FPaths::ProjectContentDir()));
//> ProjectContentDir:../../../../../Goulandis/UE4/Unkown/Unkown/Content/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectConfigDir:") + FPaths::ProjectConfigDir()));
//> ProjectConfigDir:../../../../../Goulandis/UE4/Unkown/Unkown/Config/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectSavedDir:") + FPaths::ProjectSavedDir()));
//> ProjectSavedDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectIntermediateDir:") + FPaths::ProjectIntermediateDir()));
//> ProjectIntermediateDir:../../../../../Goulandis/UE4/Unkown/Unkown/Intermediate/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ShaderWorkingDir:") + FPaths::ShaderWorkingDir()));
//> ShaderWorkingDir:C:/Users/admin/AppData/Local/Temp/UnrealShaderWorkingDir/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectPluginsDir:") + FPaths::ProjectPluginsDir()));
//> ProjectPluginsDir:../../../../../Goulandis/UE4/Unkown/Unkown/Plugins/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectModsDir:") + FPaths::ProjectModsDir()));
//> ProjectModsDir:../../../../../Goulandis/UE4/Unkown/Unkown/Mods/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectPersistentDownloadDir:") + FPaths::ProjectPersistentDownloadDir()));
//> ProjectPersistentDownloadDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/PersistentDownloadDir
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("SourceConfigDir:") + FPaths::SourceConfigDir()));
//> SourceConfigDir:../../../../../Goulandis/UE4/Unkown/Unkown/Config/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GeneratedConfigDir:") + FPaths::GeneratedConfigDir()));
//> GeneratedConfigDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Config/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("SandboxesDir:") + FPaths::SandboxesDir()));
//> SandboxesDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Sandboxes
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProfilingDir:") + FPaths::ProfilingDir()));
//> ProfilingDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Profiling/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ScreenShotDir:") + FPaths::ScreenShotDir()));
//> ScreenShotDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Screenshots/Windows/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("BugItDir:") + FPaths::BugItDir()));
//> BugItDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/BugIt/Windows/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("VideoCaptureDir:") + FPaths::VideoCaptureDir()));
//> VideoCaptureDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/VideoCaptures/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("ProjectLogDir:") + FPaths::ProjectLogDir()));
//> ProjectLogDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Logs/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("AutomationDir:") + FPaths::AutomationDir()));
//> AutomationDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Automation/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("AutomationTransientDir:") + FPaths::AutomationTransientDir()));
//> AutomationTransientDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Automation/Tmp/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("AutomationReportsDir:") + FPaths::AutomationReportsDir()));
//> AutomationReportsDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Automation/Reports/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("AutomationLogDir:") + FPaths::AutomationLogDir()));
//> AutomationLogDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Automation/Logs/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("CloudDir:") + FPaths::CloudDir()));
//> CloudDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Cloud/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GameDevelopersDir:") + FPaths::GameDevelopersDir()));
//> GameDevelopersDir:../../../../../Goulandis/UE4/Unkown/Unkown/Content/Developers/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GameUserDeveloperFolderName:") + FPaths::GameUserDeveloperFolderName()));
//> GameUserDeveloperFolderName:admin
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GameUserDeveloperDir:") + FPaths::GameUserDeveloperDir()));
//> GameUserDeveloperDir:../../../../../Goulandis/UE4/Unkown/Unkown/Content/Developers/admin/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("DiffDir:") + FPaths::DiffDir()));
//> DiffDir:../../../../../Goulandis/UE4/Unkown/Unkown/Saved/Diff/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GameAgnosticSavedDir:") + FPaths::GameAgnosticSavedDir()));
//> GameAgnosticSavedDir:../../../Engine/Saved/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("EngineSourceDir:") + FPaths::EngineSourceDir()));
//> EngineSourceDir:../../../Engine/Source/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GameSourceDir:") + FPaths::GameSourceDir()));
//> GameSourceDir:../../../../../Goulandis/UE4/Unkown/Unkown/Source/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("FeaturePackDir:") + FPaths::FeaturePackDir()));
//> FeaturePackDir:D:/UE_4.26_SourceCode/UnrealEngine-4.26/FeaturePacks/
DEBUGLOG(FEnumSet::DebugLogType::Warning, (TEXT("GetProjectFilePath:") + FPaths::GetProjectFilePath()));
//> GetProjectFilePath:../../../../../Goulandis/UE4/Unkown/Unkown/Unkown.uproject
-
GetExtension:获取文件的后缀,不带. ; -
GetCleanFilename:获取带后缀的文件名,不包含路径; -
GetBaseFilename:获取不带后缀的文件名,不包含路径; -
GetPath:只获取路径; -
GetPathLeaf:获取路径中的叶子节点,即路径中最后一个节点,如:D:\UE4\A.txt,则获取到A.txt,D:\UE4\,则获取到UE4; -
ChangeExtension:当文件有后缀时,改变文件的后缀,否则直接退出; -
SetExtension:无论文件有没有后缀都为为文件设置一个新的后缀; -
FileExists:判断文件是否存在; -
DirectoryExists:判断文件夹是否存在; -
IsDrive:判断路径是否是根目录,路径中只有C: 这种形式才被认定为根目录,: 之后不能跟任何东西; -
IsRelative:判断路径是否是相对路径,路径不能以\ 或/ 开头,否则会被认定为False; -
NormalizeFilename:格式化路径,会将路径中的所有\ 替换为/ ; -
IsSamePath:判断两个路径是否一样; -
IsUnderDirectory:判断路径是否在某个指定目录下; -
NormalizeDirectoryName:格式化目录,和NormalizeFilename类似; -
RemoveDuplicateSlashes:删除路径中重复/ ; -
CreateStandardFilename:把UE的相对路径转换成绝对路径,如上面的所示,由于FPaths获取的路径几乎都是../../ 这种形式,有的时候我们可能需要绝对路径,就可以使用这个函数来转换。如: DEBUGLOG(FEnumSet::DebugLogType::Warning, FPaths::CreateStandardFilename(FPaths::GetProjectFilePath()));
//> GetProjectFilePath: D:/Goulandis/UE4/Unkown/Unkown/Unkown.uproject
-
MakeStandardFilename:效果和CreateStandardFilename一致,只是CreateStandardFilename使用返回值返回路径,MakeStandardFilename直接传入引用,从参数返回; -
MakePlatformFilename:这个函数好像没什么卵用,从源码上看就是跟格式化路劲好像没啥区别; -
MakePathRelativeTo:传入两个路径,其中参数一为FString&,参数二为const TCHAR*,如果二者是同一路径下的,那么将参数一的路径转化为相对于参数二的相对路径,如:参数一为:D:/UE/Content/A.txt,参数二为:D:/UE/B.txt,那么经过转换,参数将变成:Content/A.txt;如果二者路径不一致,则不做转换; -
ConvertRelativePathToFull:这个函数似乎也是和CreateStandardFilename和MakeStandardFilename的作用是一样的,估计是历史遗留原因吧,很多重复功能的函数; -
ConvertToSandboxPath:将路径转换为沙盒路径; -
ConvertFromSandboxPath:将沙盒路径转为平台路径; -
CreateTempFilename:创建临时文件,需要指定路径,可选文件名前缀和文件名后缀,函数会根据设置的路径、前缀、后缀生成一个随机名称的临时文件,如果没有指定后缀,默认使用.tmp,如果没有指定前缀,默认使用无前缀的随机数作为文件名; DEBUGLOG(FEnumSet::DebugLogType::Warning, FPaths::CreateTempFilename(TEXT("D:/Unkwon/Content/",TEXT("Temp_"))));
//> D:/Unkwon/Content/Temp_684BF44044F4C6356EFEC8BA14E6B32B.tmp
-
GetInvalidFileSystemChars:获取操作系统规定的所有在路径中使用后是无效字符的字符串; DEBUGLOG(FEnumSet::DebugLogType::Warning, FPaths::GetInvalidFileSystemChars());
//> /?:&\*"<>|%#@^
-
MakeValidFileName:去除路径中的无效字符后,返回一个可用的路径; -
ValidatePath:判断路径的组成字符中是否包含了操作系统规定的无效字符; -
Split:将一个文件路径拆分成路径、不带后缀的文件名和后缀三部分; -
Combine:这是一个可变参数的函数,将传入的参数组合成一个路径格式; 小知识: 这里涉及到了UE的可变参数,实际上就是C++11中的可变参数模板,如: template<typename... STR>
inline void UPlatformFileExpand::FuncTest(STR... Strs)
{
TCHAR* Str[] = { Strs... };
for (TCHAR* Item : Str)
{
UE_LOG(LogTemp, Warning, TEXT("%s"), Item);
}
}
STR是一个可变参数包,可以接收任意类型的任意数量的参数,只是传进来的参数需要自行进行处理,否则可能会因为参数类型不符而导致编译失败。 其中参数可以通过数组初始化的形式来使用数组接收参数包中的参数,当前提是需要提前对参数类型做处理,否者初始化就会失败,这种方式应该是UE自己实现的,C++中读取参数使用的是va_arg,va_start,va_end,va_list等来获取可变参数。 -
TearDown:释放所有FPaths所暂用的内存,由于FPaths类在程序开始由引擎自动初始化,其中保存了大量的路径字符串,如果不需要再用到FPaths了也可以直接把这部分内存释放出来。
十三、GConfig
GConfig是一个定义在CoreGlobals.cpp中一个全局FConfigCacheIni*变量,可以在UEC++中的任何地方使用,FConfigCacheIni是UE专门用来处理缓存在内存中的配置文件的类,其中涉及到FConfigFile、FConfigSection、FConfigValue类。
UE使用FConfigValue来封装配置文件中= 号后面的内容,FConfigValue是一个结构体;使用一个TPair<FName,FConfigValue> 来封装一行内容,其中TPair的Key就是一行中的Key,Value就是一行中的Value;使用FConfigSection来封装一个块的内容,FConfigSection是一个TMultiMap<FName,FConfigValue> 的Map;使用FConfigFile来封装一个配置文件,FConfigFile是一个TMap<FString,FConfigSection> 的Map。
FConfigCacheIni
-
DisableFileOperations和EnableFileOperations:这两个是相对的函数,具体是干什么的至今没搞懂; -
AreFileOperationsDisabled:判断是DisableFileOperations状态还是EnableFileOperations状态; -
IsReadyForUse:判断ini文件是否已经加载好了,GConfig在加载ini文件时,会把ini文件内容加载到一个FConfigFile类对象中,FConfigFile类才是直接对ini文件进行操作的类; -
Parse1ToNSectionOfStrings:用于读取读取ini文件的二级键值,并用一个TMap<FString,TArray<FStting>>& 作为参数返回;如: [TEXT]
AA=T1
BB=Y1
BB=Y2
AA=T2
BB=Y3
AA=W1
BB=Y4
BB=Y5
BB=Y6
其中AA=T1为一级键值,在T1下游两个二级键值BB=Y1,BB=Y2,以此类推,一级键值AA=T2下有一个二级键值BB=Y3,一级键值AA=W1下有三个二级键值BB=Y4,BB=Y5,BB=Y6。 Parse1ToNSectionOfStrings参数,其中Section为要读取的块的名称,KeyOne为一级键值名称,KeyN为二级键值名称,只能支持到二级键值。 -
Parse1ToNSectionOfNames:和Parse1ToNSectionOfStrings的效果一样只是使用FName替换了FString; -
FindConfigFile:在已加载的配置文件中通过文件路径寻找FConfigFile类型的配置文件对象,只有已加载到内存中的配置文件才能被找到; -
Find:Find的功能和FindConfigFile一样,只是新增了一个bool参数,指定在没有找到对象时是否创建一个; -
FindConfigFileWithBaseName:使用不包含路径的文件名寻找加载到内存中FConfigFile对象; -
Flush:清除指定的已加载到内存中的FConfigFile对象; -
LoadFile:加载一个配置文件到一个FConfigFile对象并存储在内存中; -
SetFile:为一个指定的配置文件设定一个新的FConfigFile对象,UE加载所有的配置文件使用了一个Map来在内存中存储,Map使用配置文件的全路径来作为Key,使用对应FConfigFile对象来作为Value; -
UnloadFile:从内存中移除指定的FConfigFile对象; -
Detach:这个函数着实是没弄明白是干什么的; -
DoesSectionExist:判断指定块是否存在; -
GetConfigFilenames:获取所有已加载到内存中的配置文件的名称,使用一个TArray<FString> 数组存储; -
GetSectionNames:获取所有的已加载到内存中的指定的配置文件的块的键值,使用一个TArray<FString> 数组存储; -
Exit:退出FConfigCacheIni,清除所有加载到内存的FConfigFile对象; -
GetMaxMemoryUsage:获取FConfigCachIni的做大内存使用量; -
ForEachEntry:遍历指定块的所有行,通过传入的FKeyValueSink委托绑定操作函数; -
Factory:创建一个FConfigChechIni实例; -
InitializeConfigSystem:创建GConfig实例,加载标准的全局ini文件; -
GetDestIniFilename:这个函数会根据平台的类型给定标准的ini文件在不同平台中的名称; -
LoadGlobalIniFile:加载ini文件,并把生成的FConfigFile配置到GConfig以便使用; -
LoadLocalIniFile:加载ini文件,但不把生成的FConfigFile配置到GConfig; -
LoadExternalIniFile:从指定的配置文件文件夹中加载配置文件,可以使用直接使用不带后缀的文件名加载; -
SaveCurrentStateForBootstrap:将当前的FConfigCacheIni中的所有配置项存储到指定的配置文件中; -
ini文件操作: bool GetString( const TCHAR* Section, const TCHAR* Key, FString& Value, const FString& Filename );
bool GetText( const TCHAR* Section, const TCHAR* Key, FText& Value, const FString& Filename );
bool GetSection( const TCHAR* Section, TArray<FString>& Result, const FString& Filename );
void SetString( const TCHAR* Section, const TCHAR* Key, const TCHAR* Value, const FString& Filename );
void SetText( const TCHAR* Section, const TCHAR* Key, const FText& Value, const FString& Filename );
bool RemoveKey( const TCHAR* Section, const TCHAR* Key, const FString& Filename );
bool EmptySection( const TCHAR* Section, const FString& Filename );
bool EmptySectionsMatchingString( const TCHAR* SectionString, const FString& Filename );
FString GetStr(const TCHAR* Section, const TCHAR* Key, const FString& Filename );
bool GetInt(const TCHAR* Section,const TCHAR* Key,int32& Value,const FString& Filename);
bool GetFloat(const TCHAR* Section,const TCHAR* Key,float& Value,const FString& Filename);
bool GetDouble(const TCHAR* Section,const TCHAR* Key,double& Value,const FString& Filename);
bool GetBool(const TCHAR* Section,const TCHAR* Key,bool& Value,const FString& Filename);
int32 GetArray(const TCHAR* Section,const TCHAR* Key,TArray<FString>& out_Arr,const FString& Filename);
int32 GetSingleLineArray(const TCHAR* Section,const TCHAR* Key,TArray<FString>& out_Arr,const FString& Filename);
bool GetColor(const TCHAR* Section,const TCHAR* Key,FColor& Value,const FString& Filename);
bool GetVector2D(const TCHAR* Section,const TCHAR* Key,FVector2D& Value,const FString& Filename);
bool GetVector(const TCHAR* Section,const TCHAR* Key,FVector& Value,const FString& Filename);
bool GetVector4(const TCHAR* Section,const TCHAR* Key,FVector4& Value,const FString& Filename);
bool GetRotator(const TCHAR* Section,const TCHAR* Key,FRotator& Value,const FString& Filename);
void SetInt(const TCHAR* Section,const TCHAR* Key,int32 Value,const FString& Filename);
void SetFloat(const TCHAR* Section,const TCHAR* Key,float Value,const FString& Filename);
void SetDouble(const TCHAR* Section,const TCHAR* Key,double Value,const FString& Filename);
void SetBool(const TCHAR* Section,const TCHAR* Key,bool Value,const FString& Filename);
void SetArray(const TCHAR* Section,const TCHAR* Key,const TArray<FString>& Value,const FString& Filename);
void SetSingleLineArray(const TCHAR* Section,const TCHAR* Key,const TArray<FString>& In_Arr,const FString& Filename);
void SetColor(const TCHAR* Section,const TCHAR* Key,FColor Value,const FString& Filename);
void SetVector2D(const TCHAR* Section,const TCHAR* Key,FVector2D Value,const FString& Filename);
void SetVector(const TCHAR* Section,const TCHAR* Key,FVector Value,const FString& Filename);
void SetVector4(const TCHAR* Section,const TCHAR* Key,const FVector4& Value,const FString& Filename);
void SetRotator(const TCHAR* Section,const TCHAR* Key,FRotator Value,const FString& Filename);
十四、FJsonObject
UE提供FJsonObject来对json文件进行操作,同时提供一整套的json操作工具链,如:FJsonValue,FJsonReader,FJsonSerializer,FJsonWriter,FJsonTypes等。
详细使用请查看这里。
|