[UE4](译)(Rama)保存系统,二进制压缩文件读写信息

来源:互联网 发布:炎亚纶汪东城天涯 知乎 编辑:程序博客网 时间:2024/06/06 07:44

本文内容主要来自于Rama大神,为了方便自己查阅和理解而写这篇。
原文链接 - Rama - Save System, Read & Write Any Data to Compressed Binary Files

开始

作者:Rama(talk)
亲爱的社区成员你们好,
这篇教程来指导大家如何写出自己的保存系统。

  • 写出你想要的任何游戏相关的数据
  • 随时从硬盘中读出这些数据
  • 为了节省空间,最小化压缩(ZLIB压缩)这些文件

我已经写出这些例子,从压缩的二进制文件中,加载了这些文件的信息到UE编辑器里。
我已经开发出一种方便的方法去做序列化与反序列化的工作,是通过重载特定的UE4 C++操作符实现的,非常感谢UE4框架对这个功能的支持。

序言

在教程中有很多概念,请在确保当前的内容理解后,再继续下一个。
只是复制粘贴代码,是不能正常工作的,你需要理解基础,因为我正在呈现的就是基础。而且你必须实现这些基础内容,到你需要的项目中。
毕竟,这是关于保存系统为了任何项目自定义的数据类型。

转换层级

在你想要保存你的自定义变量数据到你的保存系统时,
这里包含2个主要步骤:
第一步:变量格式 -> 二进制数组(使用UE4中FArchive类进行序列化)
第二步:二进制数组 -> 硬盘
读取数据时,则相反进行这2步。

二进制数组TArray< uint8 >

二进制数组在UE4 C++中表现的很友好,是以uint8为元素的动态数组。
所以在这篇教程中,任何时候当你看到TArray< uint8 >,那就意味着,那是一个二进制数组

第1步:变量格式 -> 二进制数组

1个 int32 占用 4字节,和float一样
1个 int64 占用 8字节,
1个 FName 占用 8字节,
1个 FString 占用 16字节,
1个 FVector 占用 3 x float 个字节,即12字节。
等等。
所以一个int32类型的变量,实际上是一个字节数组,不是一个整体。
现在举个例子,让我们看看,你的保存系统需要多少空间去存储,

  • 3 个 FVector
  • 40 个 int32
  • 20 个 FName

计算一下得知需要很多字节!

3x3x4=36(byte)
40x4=160(byte)
20x8=160(byte)
356 bytes

这意味着在你保存数据到硬盘前,你需要一个356字节的二进制数组。

第2步:二维数组 -> 硬盘

UE4 C++通过FileManager.h的方法,把TArray< uint8 >写到硬盘上。

第3步(可选):压缩二进制

UE4在Archive.h中提供了方法,在发送到FileManager之前,压缩二进制数组。
提供C++代码。
下面提供给你的方法是,我选择一些特定的自定义数据,去读写二进制文件的例子。

核心头文件

Archive.h 和 ArchiveBase.h

查看Archive.h 和 ArchiveBase.h,里面有所有关于自定义数据类型转化二进制的方法和信息。

FileManager.h

你需要的全部方法。

  • 创建路径
  • 删除路径
  • 创建文件
  • 删除文件
  • 获得指定路径下的文件列表
  • 或者指定路径下的文件夹列表
  • 获得文件的年龄?

以及更多功能,都在FileManager.h里。
你可以在任何地方使用此对象的方法。

if(GFileManager) GFileManager->TheFunction()

BufferArchive

关于FBufferArchive类型,既继承TArray< uint8 >,又继承MemoryWriter。
Archive.h

/** * Buffer archiver. */class FBufferArchive : public FMemoryWriter, public TArray<uint8>{

因为多重继承,在写二进制文件时,我推荐使用BufferArchive。
作为代码展示,因为GFileManager希望接受TArray< uint8 >,而不是MamoryArchive。
重复第1步,第2步,FBufferArchive真是超棒der类,手动滑稽。
感谢UE4的开发者!

FMemoryReader

若想读取二进制数组,需要通过FileManager恢复,你需要FMemoryReader。

Archive.h

/** * Archive for reading arbitrary data from the specified memory location */class FMemoryReader : public FMemoryArchive{public:

<<操作符

BufferArchive或二进制数组都需要恢复成游戏的变量数据,那么如何转化成二进制,又或者如何恢复成原有数据呢?
使用<<操作符。

变量 -> 二进制

这里演示了,为了保存到硬盘,如何从FVector转化到一个BufferArchive。
在PlayerController类中。

FBufferArchive ToBinary;ToBinary << GetPawn()->GetActorLocation(); //save player location to hard disk//save ToBinary to hard disk using File Manager, //see complete code samples below

二进制 -> 变量

这里演示了,在从硬盘得到的二进制数组后,如何从TArray< uint8 > 转化到 FVector。

//TheBinaryArray was already obtained from FileManager, //see code below for full examples//need to supply a variable to be filled with the dataFVector ToBeFilledWithData;FMemoryReader Ar = FMemoryReader(TheBinaryArray, true); //true, free data after doneAr.Seek(0); //make sure we are at the beginningAr << ToBeFilledWithData;

关键概念:UE4 C++ 自定义保存系统

这里有2行代码:

ToBinary << GetPawn()->GetActorLocation();Ar << ToBeFilledWithData;

最难理解的概念是,关于Archive中<<操作符的含义,它包含2中含义。

  • 二进制数据档案 转化到 变量。
  • 变量 转化到 二进制数据档案格式

操作的含义取决于操作对象!

所以,这就是为什么我推荐你命名你的BufferArchive类型的名称为ToBinary。
而且,和你的MemoryReader有些东西是不同的。
你需要辨别,到底是二进制转化变量,还是,变量转化二进制。
那基于你代码中操作符操作的内容的类型,<<操作符会一目了然的告诉你。

写一种双向的方法

这个系统的关键优点在于,您可以编写一个单一的功能,这两种方式都有效。 所以你可以编写一个从文件加载数据或保存到文件的函数。
为什么需要这功能?

因为,你使用一种顺序,把变量序列化成二进制格式,那么反过来,就必须以相同的顺序,反序列化。

你的电脑可不知道你是用哪一种方法,什么顺序序列化变量,UE4也不知道。
你有责任告诉电脑或者UE4,你序列化与反序列化,使用同一种顺序。
因此,读和写使用同一个方法,使用重载的<<操作符,这就是你能做的,确保读写数据一致性的最安全的方式。

SaveLoadData:双向保存系统方法

'''.h'''//FArchive is shared base class for FBufferArchive and FMemoryReadervoid SaveLoadData(FArchive& Ar, int32& SaveDataInt32, FVector& SaveDataVector, TArray<FRotator>& SaveDataRotatorArray);'''.cpp'''//I am using controller class for convenience, use any class you want//SaveLoadDatavoid YourControllerClass::SaveLoadData(FArchive& Ar,  int32& SaveDataInt32,  FVector& SaveDataVector,  TArray<FRotator>& SaveDataRotatorArray){    Ar << SaveDataInt32;    Ar << SaveDataVector;    Ar << SaveDataRotatorArray;}

Saving

声明一个BufferArchive并传入它,它是一个二进制数组,也是一个FArchive。

FBufferArchive ToBinary;SaveLoadData(ToBinary,NumGemsCollected,PlayerLocation,ArrayOfRotationsOfTheStars);//save the binary array / FBufferArchive to hard disk, see below

Loading

// TheBinaryArray already retrieved from file, see full code sampleFMemoryReader FromBinary = FMemoryReader(TheBinaryArray, true); //true, free data after doneFromBinary.Seek(0);SaveLoadData(FromBinary,NumGemsCollected,PlayerLocation,ArrayOfRotationsOfTheStars);

总结

使用此设置可避免由于读取与您将其写入磁盘的顺序不同的数据而导致的崩溃!
这种双向的<<操作符,节省了很多时间。
感谢Epic开发者!

我的二进制保存系统方法

下面展示了我用来读写二进制的文件。

Saving Binary Files

bool ControllerClass::SaveGameDataToFile(const FString& FullFilePath, FBufferArchive& ToBinary){    //note that the supplied FString must be the entire Filepath    //  if writing it out yourself in C++ make sure to use the \\    //  for example:    //  FString SavePath = "C:\\MyProject\\MySaveDir\\mysavefile.save";    //Step 1: Variable Data -> Binary    //following along from above examples    SaveLoadData(ToBinary,NumGemsCollected,PlayerLocation,ArrayOfRotationsOfTheStars);     //presumed to be global var data,     //could pass in the data too if you preferred    //No Data    if(ToBinary.Num() <= 0) return false;    //~    //Step 2: Binary to Hard Disk    if (FFileHelper::SaveArrayToFile(ToBinary, * FullFilePath))     {        // Free Binary Array            ToBinary.FlushCache();        ToBinary.Empty();        ClientMessage("Save Success!");        return true;    }    // Free Binary Array        ToBinary.FlushCache();    ToBinary.Empty();    ClientMessage("File Could Not Be Saved!");    return false;}

Loading Binary Files

//I am using the sample save data from above examples as the data being loadedbool ControllerClass::LoadGameDataFromFile(    const FString& FullFilePath,     int32& SaveDataInt32,    FVector& SaveDataVector,    TArray<FRotator>& SaveDataRotatorArray){    //Load the data array,    //  you do not need to pre-initialize this array,    //      UE4 C++ is awesome and fills it     //      with whatever contents of file are,     //      and however many bytes that is    TArray<uint8> TheBinaryArray;    if (!FFileHelper::LoadFileToArray(TheBinaryArray, *FullFilePath))    {        ClientMessage("FFILEHELPER:>> Invalid File");        return false;        //~~    }    //Testing    ClientMessage("Loaded File Size");    ClientMessage(FString::FromInt(TheBinaryArray.Num()));    //File Load Error    if(TheBinaryArray.Num() <= 0) return false;    //~    //        Read the Data Retrieved by GFileManager    //~    FMemoryReader FromBinary = FMemoryReader(TheBinaryArray, true); //true, free data after done    FromBinary.Seek(0);    SaveLoadData(FromBinary,NumGemsCollected,PlayerLocation,ArrayOfRotationsOfTheStars);    //~    //                              Clean up     //~    FromBinary.FlushCache();    // Empty & Close Buffer     TheBinaryArray.Empty();    FromBinary.Close();    return true;}

Saving Compressed

bool ControllerClass::SaveGameDataToFileCompressed(const FString& FullFilePath,     int32& SaveDataInt32,    FVector& SaveDataVector,    TArray<FRotator>& SaveDataRotatorArray){    FBufferArchive ToBinary;    SaveLoadData(ToBinary,NumGemsCollected,PlayerLocation,ArrayOfRotationsOfTheStars);     //Pre Compressed Size    ClientMessage("~ PreCompressed Size ~");    ClientMessage(FString::FromInt(ToBinary.Num()));    //    // Compress File     //tmp compressed data array    TArray<uint8> CompressedData;    FArchiveSaveCompressedProxy Compressor =         FArchiveSaveCompressedProxy(CompressedData, ECompressionFlags::COMPRESS_ZLIB);    //Send entire binary array/archive to compressor    Compressor << ToBinary;    //send archive serialized data to binary array    Compressor.Flush();    //    //Compressed Size    ClientMessage("~ Compressed Size ~");    ClientMessage(FString::FromInt(CompressedData.Num()));    if (!GFileManager) return false;    //vibes to file, return successful or not    if (FFileHelper::SaveArrayToFile(CompressedData, * FullFilePath))     {        // Free Binary Arrays         Compressor.FlushCache();        CompressedData.Empty();        ToBinary.FlushCache();        ToBinary.Empty();        // Close Buffer         ToBinary.Close();        ClientMessage("File Save Success!");        return true;        //    }    else    {        // Free Binary Arrays                 Compressor.FlushCache();        CompressedData.Empty();        ToBinary.FlushCache();        ToBinary.Empty();        // Close Buffer         ToBinary.Close();        ClientMessage("File Could Not Be Saved!");        return false;        //    }}

Loading Compressed

//I am using the sample save data from above examples as the data being loadedbool ControllerClass::LoadGameDataFromFileCompressed(    const FString& FullFilePath,     int32& SaveDataInt32,    FVector& SaveDataVector,    TArray<FRotator>& SaveDataRotatorArray){    //Load the Compressed data array    TArray<uint8> CompressedData;    if (!FFileHelper::LoadFileToArray(CompressedData, *FullFilePath))    {        Optimize("FFILEHELPER:>> Invalid File");        return false;        //~~    }    // Decompress File     FArchiveLoadCompressedProxy Decompressor =         FArchiveLoadCompressedProxy(CompressedData, ECompressionFlags::COMPRESS_ZLIB);    //Decompression Error?    if(Decompressor.GetError())    {        Optimize("FArchiveLoadCompressedProxy>> ERROR : File Was Not Compressed ");        return false;        //    }    //Decompress    FBufferArchive DecompressedBinaryArray;    Decompressor << DecompressedBinaryArray;    //~    //        Read the Data Retrieved by GFileManager    //~    FMemoryReader FromBinary = FMemoryReader(DecompressedBinaryArray, true); //true, free data after done    FromBinary.Seek(0);    SaveLoadData(FromBinary,NumGemsCollected,PlayerLocation,ArrayOfRotationsOfTheStars);    //~    //                              Clean up     //~    CompressedData.Empty();    Decompressor.FlushCache();    FromBinary.FlushCache();    // Empty & Close Buffer     DecompressedBinaryArray.Empty();    DecompressedBinaryArray.Close();    return true;}

重载<<操作符

创建你自己的<<操作符重载去简化过程!
假设你有自己的USTRUCT或你自己的类,你想写一个简单的写法。
简单的写法。

ToBinary << MyEntireSaveSystem;

或者

ToBinary << MySpecialUStruct;

接下来,告诉你如何重载<<。
请注意把内容写在.h文件中,而不是.cpp文件。
注意也没有MyClass::这种内容,重载的内容需要在全局等级。
此外,具有此定义的.h文件必须在任何想要使用它的类之前进行编译。
您可以使用UClass(取决于= UYourDefinitionsClass)来确保这一点。
或者简单地将.h内容放在你的公共目录中,并将它们包括在某个位置。

.h

//Make as many Unique Overloads as you want!FORCEINLINE FArchive& operator<<(FArchive &Ar, UMySaveGameClass* SaveGameData ){    if(!SaveGameData) return Ar;    //~    Ar << SaveGameData->NumGemsCollected;  //int32    Ar << SaveGameData->PlayerLocation;  //FVector    Ar << SaveGameData->ArrayOfRotationsOfTheStars; //TArray<FRotator>        return Ar;}

注意

  1. 操作返回引用类型
  2. 在全局级别不允许使用const(它是编译错误)
  3. 不允许内部的const,因为你不知道你是否因为<<操作符的性质而阅读或写作。

崩溃?

如果你遇到崩溃,说明你不使用相同的顺序读写的。
使用SaveLoadGame方法,以相同的方式操作,来避免崩溃。
另外,如果您在保存之前压缩数据,请确保使用我的压缩功能加载数据,而不是常规的反序列化操作,反之亦然。

享受!

祝你玩得开心,做一个自己的自定义保存游戏系统,并保存到压缩的二进制文件!

(づ ̄3 ̄)づ╭❤~。

Rama(talk)

0 0