|
发表于 2022-12-9 21:03:15
|
显示全部楼层
概述
接上回大象无形UE笔记十:UObject (一),本节主要探讨UObject的序列化,对应原书中的10.1.2小节。让我们开始吧!
序列化时指将一个对象变为更容易保存的形式,写入持久存储中(比如硬盘)。反序列化则反之,从持久存储中读取数据,然后还原原来的对象。如下图:

UObject的序列化和反序列化都对应同一个函数Serialize。正向写入和反向读取需要按同样的方式进行。
需要注意的是,序列化/反序列化的过程是一个“步骤”,而不是一个完整的对象初始化过程。反序列化的主要步骤是:先实例化对象,然后再反序列化对象的属性。
我们先来研究一下反序列化对象。因为理解对象如何读取出来后,写入就显得更简单了。
反序列化
主要步骤
首先我们实例化一个对象出来,步骤大致如下,我们以加载Content/link.uasset的名为“link”的UPlayerObject对象来举例(参照UObject(一)):

让我们详细看看最后一个步骤,即载入所有脚本成员变量信息,这里的代码主要在 UObject::Serialize(...)函数中,它主要有如下几个步骤:
1,调用FArchive::MarkScriptSerializationStart函数,标记脚本数据序列化开始。
2,调用ObjClass对象的SerializeTaggedProperties函数,进而调用类的每个Tag的Tag.SerializeTaggedProperty()函数,载入脚本定义的成员变量,比如我们的UPlayerObject里面自定义的CurPlayerName 和 CurAge两个成员变量:

3,调用MarkScriptSerializationEnd标记脚本数据序列化结束。
切豆腐理论
对于硬盘上保存的数据来说,其本身不具备“意义”,其含义取决于我们如何解释这一段数据。我们每次反序列化一个C++基本对象,例如一个float浮点数,我们是从豆腐最开头切下一块和浮点数长度一样大的豆腐,然后把这块豆腐解释为一个浮点数。

那读者会问,你怎么知道豆腐这一块区域就是浮点数?答案是,我们按照什么顺序把豆腐排起来的,并且按同样的顺序切,那就能保证每次切出来都是正确的。比如我放了三块豆腐:豆腐1(float),豆腐2(bool),豆腐3(double)。把它们按顺序靠近摆放,组合成一块大豆腐,然后把它放冰箱里冻起来。下次吃的时候,由于我知道豆腐1,豆腐2,豆腐3的长度。所以我在1号长度切一刀,1和2号长度总和处切一刀,妥了!三块豆腐就分开了。

递归序列化/反序列化
一个类的成员会有以下几种类型:C++基本类型,自定义类型的对象,自定义类型的指针:

对于C++基础类型对象,直接调用切豆腐理论序列化,对于自定义对象则调用该类型的序列化函数自行去切豆腐,然后把切好的豆腐还回来即可。对于自定义类型的指针,这里暂时不讲解,由我们后面的UPakcage包的对象引用那部分再讲。

理解这两个概念之后,我们就能开始分析虚幻引擎的反序列化过程。这其实是一个两步的步骤:

同时,虚幻引擎,对序列化后的大小进行了优化。对于每个类都有一个类的默认对象即CDO,我们的序列化只保存跟CDO的差异部分即可。比如如果PlayerObject的CurPlayerName == ""的话, 则不需要占用序列化后的存储空间。
接下来的问题就是,自定义对象的指针如何序列化,这里就涉及到UObject对象的互相引用,以及加载UPackage包,如何加载引用问题了,我们来探讨一下UPackage包的相关知识吧。
UPackage资源包
概述
虚幻引擎中,在Content目录中保存资源的*.uasset文件,都是UPackage资源包。无论是纹理资源,材质资源,静态模型,骨骼模型,骨架,动画序列,动画蒙太奇,蓝图,场景*.umap等等,其实都是UPackage包。也就是UPackage就是虚幻引擎存储资源的统一方式。
包结构
UPackage包体的数据结构大致如下:

如上图,我们重点关注的是 Import table,Exports table,以及Export-objects 这几个项目,我们先把 上一节节保存的link.uasset加载起来,然后看看link.uasset的包体结构。我们在上一节保存了一个Content/link.uasset,并且用LoadPackage把它加载出来了,我们可以在Package->FullyLoad();这句的后面打一个断点,并在命中断点时用VisualStudio的变量查看功能来观察这个包体数据:

经过我们的调试分析,我们得出:link.uasset的UPackage数据如下:

可以看到我们包体保存的UObject对象主要是Link,它是UPlayerObject的对象,另外还有一个PackageMetaData应该是一些额外数据,我们暂时忽略。在导入表(import table)中它引用的主要都是C++对象,即Script/CoreUObject里面的对象,包括UPlayerObject类以及MyGame模块等。
UPackage包的引用关系
我们可以在内容浏览器中,右键点击一个资源,并点击“引用查看器”,可以查看该资源引擎哪些其他资源,我们右击link.uasset试试:

可以看到link.uasset现在没有引用任何其他资源,也没有被其他资源所引用。现在让我们来给它加一些引用了。我们在MyGame/Private/Tests/ 目录来创建一个ObjectRefTest.cpp,关闭VisualStudio,并右击MyGame.uproject,重新生成MyGame.sln项目。
然后我们修改PlayerObject.h,加上一个UPROPERTY成员变量:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PlayerObject")
TObjectPtr<UPlayerObject> Lover;

然后我们把之前创建的Link.uasset文件删除掉,并打开在ObjectRefTest.cpp输入如下代码:
#include &#34;CoreMinimal.h&#34;
#include &#34;../PlayerObject.h&#34;
DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All);
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FObjectRefTest, &#34;MyTest.PublicTest.ObjectRefTest&#34;, EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
// 保存 两个UPlayerObject的对象到*uasset资产
bool SaveTwoPlayerAsset(const FString &AssetPath1, const FString &PackageFileName1, const FString &ObjectName1,
const FString& AssetPath2, const FString& PackageFileName2, const FString& ObjectName2)
{
// 创建两个空的资源Package
UPackage* Package1 = CreatePackage(nullptr, *AssetPath1);
UPackage* Package2 = CreatePackage(nullptr, *AssetPath2);
// 创建“林克”对象时,指定他对应的Package就是刚才创建的空资源Package1
UPlayerObject* Link = NewObject<UPlayerObject>(Package1, FName(*ObjectName1), EObjectFlags::RF_Public | EObjectFlags::RF_Standalone);
// 创建“塞尔达”对象时,指定她对应的Package就是刚才创建的空资源Package2
UPlayerObject* Zelda = NewObject<UPlayerObject>(Package2, FName(*ObjectName2), EObjectFlags::RF_Public | EObjectFlags::RF_Standalone);
// 设置“林克”的对象属性
Link->CurPlayerName = ObjectName1;
Link->CurAge = 117;
Link->Lover = Zelda; // 林克的恋人是塞尔达。哈哈
Package1->MarkPackageDirty();
// 设置“塞尔达”的对象属性
Zelda->CurPlayerName = ObjectName2;
Zelda->CurAge = 117;
Package2->MarkPackageDirty();
// 保存这个“林克”对象到一个指定路径的*.uasset资源文件
bool bSaved = UPackage::SavePackage(Package1, Link, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName1, GError, nullptr, true, true, SAVE_NoError);
// 保存这个“塞尔达”对象到一个指定路径的*.uasset资源文件
bSaved = UPackage::SavePackage(Package2, Zelda, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName2, GError, nullptr, true, true, SAVE_NoError);
return bSaved;
}
// 测试入口函数
bool FObjectRefTest::RunTest(const FString& Param)
{
// 资源包名
FString AssetPath = TEXT(&#34;/Game/Link&#34;);
// 资源路径,这里是: MyGame/Content/Link.uasset
FString PackageFileName = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
// 对象名,就是 我们PlayerObject在资源包里面的名字,这里是Link,
// 一般资源包里面的主要对象的名字,跟资源包名的最后字段是一致的。
// 比如 资源包名是 /Game/Link, 那么对象名就是Link。对象全路径名就是 /Game/Link.Link
FString ObjectName = TEXT(&#34;Link&#34;);
// 资源包名
FString AssetPath2 = TEXT(&#34;/Game/Zelda&#34;);
// 资源路径,这里是: MyGame/Content/Link.uasset
FString PackageFileName2 = FPackageName::LongPackageNameToFilename(AssetPath2, FPackageName::GetAssetPackageExtension());
// 对象名,就是 我们PlayerObject在资源包里面的名字,这里是Zelda
FString ObjectName2 = TEXT(&#34;Zelda&#34;);
// 如果资源包MyGame/Content/Link.uasset已存在,则不需要重复保存
if (FPaths::FileExists(PackageFileName))
{
UPackage* Package = LoadPackage(nullptr, *AssetPath, LOAD_None);
Package->FullyLoad();
const TArray<FObjectExport> &ExportMap = Package->LinkerLoad->ExportMap;
UPlayerObject* Link = nullptr;
for (const FObjectExport& CurObjExport : ExportMap)
{
if (CurObjExport.ObjectName.ToString() == ObjectName)
{
Link = (UPlayerObject *)(CurObjExport.Object);
}
}
UE_LOG(TestLog, Display, TEXT(&#34;Load Package Success! %s &#34;), *PackageFileName);
}
// 否则MyGame/Content/Link.uasset不存在,让我们来保存一下资源包
else
{
bool bSaved = SaveTwoPlayerAsset(AssetPath, PackageFileName, ObjectName, AssetPath2, PackageFileName2, ObjectName2);
if (bSaved)
{
UE_LOG(TestLog, Display, TEXT(&#34;Save Package Success! %s &#34;), *PackageFileName);
}
}
UE_LOG(TestLog, Display, TEXT(&#34;Done ......&#34;));
return true;
}
记得把之前创建的Link.uasset文件删除掉。
然后我们打开“工具”--> “会话前端” -->“自动化”,并勾选“MyTest/PublicTest”的&#34;ObejctRefTest&#34;,“开始测试”:

然后我们就可以发现Content里面多了两个文件,Link.uasset和Zelda.uasset:

我们再打开引用查看器看看Link.uasset的引用,可以发现,Link是依赖Zelda的:

我们也可以双击打开Link.uasset看到他的三个属性:

然后,我们在ObjectRefTest.cpp中的Package->FullyLoad();的下一行打一个断点,并观察一下Package的结构:

经过我们的调试分析,我们得出:当前link.uasset的UPackage数据如下:

如上,可以看出,Link的UPackage的导入表(Import table)多了 /Game/Zelda的UPackage包,以及Zelda的依赖。另外,我们再调试一下加载过程:

如上可以看出在加载Link.uasset时,会调用另一个VerityImport的函数,这里的importIndex传的是2,当它发现导入表(import table)的 第二个元素 /Game/Zelda 还没有加载进来,于是它会触发LoadPackageInternal去把/Game/Zelda的uasset也加载进来。这个就是UE加载包引用的方式。
好的,我们的UObject的序列化,差不多就讲到这里,原书上对加载UPackage的相互引用时,还有一个“班级早恋”的例子,我这里就不重复了,大家有兴趣可以自己翻一下书。
下一节我们会分享UObject的垃圾收集相关,敬请期待! |
|