大象无形UE笔记十一:UObject (二)

2

主题

4

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 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 "CoreMinimal.h"
#include "../PlayerObject.h"

DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All);

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FObjectRefTest, "MyTest.PublicTest.ObjectRefTest", 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("/Game/Link");
        // 资源路径,这里是: MyGame/Content/Link.uasset
        FString PackageFileName = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
        // 对象名,就是 我们PlayerObject在资源包里面的名字,这里是Link,
        // 一般资源包里面的主要对象的名字,跟资源包名的最后字段是一致的。
        // 比如 资源包名是 /Game/Link, 那么对象名就是Link。对象全路径名就是 /Game/Link.Link
        FString ObjectName = TEXT("Link");

        // 资源包名
        FString AssetPath2 = TEXT("/Game/Zelda");
        // 资源路径,这里是: MyGame/Content/Link.uasset
        FString PackageFileName2 = FPackageName::LongPackageNameToFilename(AssetPath2, FPackageName::GetAssetPackageExtension());
        // 对象名,就是 我们PlayerObject在资源包里面的名字,这里是Zelda
        FString ObjectName2 = TEXT("Zelda");


        // 如果资源包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("Load Package Success! %s "), *PackageFileName);
        }
        // 否则MyGame/Content/Link.uasset不存在,让我们来保存一下资源包
        else
        {
                bool bSaved = SaveTwoPlayerAsset(AssetPath, PackageFileName, ObjectName, AssetPath2, PackageFileName2, ObjectName2);

                if (bSaved)
                {
                        UE_LOG(TestLog, Display, TEXT("Save Package Success! %s "), *PackageFileName);
                }
        }

        UE_LOG(TestLog, Display, TEXT("Done ......"));
        return true;
}
记得把之前创建的Link.uasset文件删除掉。
然后我们打开“工具”--> “会话前端” -->“自动化”,并勾选“MyTest/PublicTest”的"ObejctRefTest",“开始测试”:


然后我们就可以发现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的垃圾收集相关,敬请期待!
回复

举报 使用道具

您需要登录后才可以回帖 登录 | 立即注册
快速回复 返回顶部 返回列表