yufan163 发表于 2024-7-15 17:57

Unreal Engine序列化个人笔记

近期工作涉及到自定义资产的创建以及在Runtime模式下的保留与加载,在学习过程中对于担任自UObject的自定义资产实现序列化的相关基础常识进行归纳整理。如有错误,欢迎斧正。
1. 序列化基本概念

序列化(serialization)在计算机科学的数据措置中,是指将数据布局或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在不异或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式从头获取字节的成果时,可以操作它来发生与原始对象不异语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据布局的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。序列化在计算机科学中凡是有以下定义:

[*]对同步控制而言,暗示强制在同一时间内进行单一访谒。
[*]在数据储存与发送的部门是指将一个对象存储至一个存储介质,例如文件或是存储器缓冲等,或者透过网络发送数据时进行编码的过程,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这法式被应用在分歧应用法式之间发送对象,以及处事器将对象存储到文件或数据库。相反的过程又称为反序列化。
上述序列化定义来源于维基百科,简单来说序列化的最终目的是为了对象可以跨平台存储和进行网络传输,其方式就是字节流IO。C++没有提供任何类型的高阶序列化构造,但是撑持将内置数据类型和一般的数据布局(struct)输出为二进制文件。
2. UObject的序列化

UE中的变量分为UPROPERTY()修饰的变量和普通C++变量两种,普通的C++变量在Runtime的时候进行读写,UPROPERTY宏修饰的变量可以在Editor中进行各类操作。UE中的序列化有2种:TaggedPropertySerializer(TPS)和UnversionedPropertySerializer(UPS),仅研究TPS,UPS见文章(不懂日语)。
TPS方式下,没有被 UPROPERTY标识表记标帜的成员变量不参与序列化。TPS首先找到UClass中的持有FProperty属性的变量,这个FProperty属性保留着这个变量的名字,类型,类中的位置,meta修饰符数据 等等的数据谍报。按照变量的FProperty属性,TPS会为其创建一个FPropertyTag的数据。
/** Object.h

* Handles reading, writing, and reference collecting using FArchive.
* This implementation handles all FProperty serialization, but can be overridden for native variables.
*/
virtual void Serialize(FArchive& Ar);
virtual void Serialize(FStructuredArchive::FRecord Record);UObject声明了两个Serialize()方式,此中Serialize(FArchive& Ar)用宏定义,
/** Obj.cpp
IMPLEMENT_FARCHIVE_SERIALIZER(UObject)

/** ObjectMacros.h
#define IMPLEMENT_FARCHIVE_SERIALIZER( TClass ) void TClass::Serialize(FArchive& Ar) { TClass::Serialize(FStructuredArchiveFromArchive(Ar).GetSlot().EnterRecord()); }等价于
void UObject::Serialize(FArchive& Ar)
{ UObject::Serialize(FStructuredArchiveFromArchive(Ar).GetSlot().EnterRecord());
}由此可见UObject的第一个Serialize(FArchive& Ar)方式实际上接收一个FArchive类型的输入,FStructuredArchiveFromArchive就是将传入的FArchive包了一层并做了一些措置,最终返回了FStructuredArchive的成员Record,并以此为参数传入UObject的第二个Serialize()方式。
2.1 FArchive

UObject的序列化和反序列化都对应函数Serialize。通过传递进来的FArchive的类型分歧而进行分歧的操作。FArchive作为基类重写了 “<<” 操作符实现序列化。FArchive类的派生类FLinkerSave、FLinkerLoad负责UObject的序列化与反序列化。
2.2 UObject基础

通过NewObject<>()方式实例化一个UObject时需要传一个参数Outer来指定这个UObject属于哪一个UPackage,如果不传则创建一个临时的Package,每一个UObject都存在于一个UPackage中,UObject的保留与加载也是基于UPackage。


内容浏览器中每一个图标对应一个资产uasset,在内存中即为UPackage,UPackage颠末序列化保留在当地之后生成uasset文件。凡是一个资产包含多个UObject。以UE5小白酬报例,右键选择Asset Actions -> Export,导出为Unreal Object text可查看一个Package内部的Object信息。


2.2.1 UPackage布局




[*]File Summary 文件头信息。
[*]Name Table 包中对象的名字表。
[*]Import Table 存放被该包中对象引用的其它包中的对象信息(路径名和类型)。
[*]Export Table 该包中的对象信息(路径名和类型)。
[*]Export Objects 所有Export Table中对象的实际数据。
2.2.2 UObject的保留

创建一个测试用的UObject派生类,如下


首先是实例化,创建一个Actor蓝图作为测试入口,在Actor内实例化Object并保留。
//** MyActor.h
UCLASS()
class UOBJECTSER_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    // Sets default values for this actor&#39;s properties
    AMyActor();

    UFUNCTION(BlueprintCallable)
    void CreatePackageAndSave();

private:
    bool SaveObjInternal(const FString& AssetPath, const FString& PackageFileName, const FString &ObjectName);
};


//MyActor.cpp

bool AMyActor::SaveObjInternal(const FString& AssetPath, const FString& PackageFileName, const FString& ObjectName)
{
      // 创建一个空的Package
      UPackage* Package = CreatePackage(*AssetPath);
      Package->FullyLoad();

      // 创建对象时,指定他对应的Package就是刚才创建的空资源Package
      UMyObject* InMyObj = NewObject<UMyObject>(Package, FName(*ObjectName), EObjectFlags::RF_Public | EObjectFlags::RF_Standalone);

      InMyObj->MyName = ”ObjName”;
      InMyObj->MyAge = 18;

      UE_LOG(LogTemp, Display, TEXT(”This is my Obj”));

      // 保留这个对象到一个指定路径的uasset文件
      bool bSaved = UPackage::SavePackage(Package, InMyObj, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName, GError, nullptr, true, true, SAVE_NoError);
      return bSaved;
}

void AMyActor::CreatePackageAndSave()
{
    // Package名
    FString AssetPath = TEXT(”/Game/MyPackage”);
    // 资源路径,这里是: MyGame/Content/MyPackage.uasset
    FString PackageFileName = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());

    // Object在Package里面的名字
    // 一般package里面的主要对象的名字跟Package名的最后字段是一致的。
    FString ObjectName = TEXT(”MainObj”);

    // 如果资源包MyGame/Content/MyPackage.uasset已存在,则不需要反复保留
    if (FPaths::FileExists(PackageFileName))
    {
    }
    else
    {
      bool bSaved = SaveObjInternal(AssetPath, PackageFileName, ObjectName);
      if (bSaved)
      {
            UE_LOG(LogTemp, Display, TEXT(”Save Package Success! %s ”), *PackageFileName);
      }
    }
}

可以看出PackageFileName是uasset在当地的带uasset后缀的路径,当前这个Package的Name为传入的AssetPath,Package中包含刚实例化的UObject对象,调用Upackage::SavePackage()方式时对UObject对象执行序列化。进一步断点调试会进入FSavePackageResultStruct::Save2()函数,在内部创建一个FSaveContext的辅助类来存放保留Package所需的信息,将SaveContext传给InnerSave函数实现保留。
//SavePackage2.cpp
//省略部门代码
ESavePackageResult InnerSave(FSaveContext& SaveContext)
{
        //创建一个FUObjectSerializeContext类型的指针
       //FUObjectSerializeContext用来保留当前UObject的序列化状态
        TRefCountPtr<FUObjectSerializeContext> SerializeContext(FUObjectThreadContext::Get().GetSerializeContext());
       //赋给SaveContext的SerializeContext成员
        SaveContext.SetSerializeContext(SerializeContext);
   
        //....
        //捕捉Package
        SaveContext.Result = HarvestPackage(SaveContext);

        //....
        return SaveContext.Result;
}HarvestPackage(SaveContext)函数捕捉当前的UPackage按照Package的布局来执行分歧的数据措置,此中FPackageHarvester::ProcessExport负责措置UPackage中的Export信息并进行序列化。
//PackageHarvester.cpp
//省略部门代码
void FPackageHarvester::ProcessExport(const FExportWithContext& InProcessContext)
{
    //获取要保留的UObject
    UObject* Export = InProcessContext.Export;
   
    // Harvest its class
    //获取UObject的Class
        UClass* Class = Export->GetClass();
        *this << Class;
   
    // Harvest the export outer
    //获取UObject的Outer
    if (UObject* Outer = Export->GetOuter())
    {
      //...
      *this << Outer;
      //...
    }
   
    // 如果是模板类型,获取其模板
        UObject* Template = Export->GetArchetype();
        if (Template
               && (Template != Class->GetDefaultObject() || SaveContext.IsCooking())
                )
        {
                *this << Template;
        }
   
    //...
    //查抄该UObject是否是CDO,如果是的话序列化成CDO
    if (Export->HasAnyFlags(RF_ClassDefaultObject))
        {
                Class->SerializeDefaultObject(Export, *this);
        }
   
    {
      //调用UObject的Serialize()函数进行序列化
      Export->Serialize(*this);
    }
   
    //....
   
}2.2.3 UObject的加载

在蓝图中调用加载UObject代码与测试成果如下。可见实例化的UMyObject类型的对象MainObj被成功加载并输出log。


加载UObject会顺着Outer最终加载该UObject所属的UPackage,调用LoadPackage()加载Package后会最终跳转到UPackage* LoadPackageInternal()函数。
//UObjectGlobals.cpp
//省略部门代码

UPackage* LoadPackageInternal(UPackage* InOuter, const FPackagePath& PackagePath, uint32 LoadFlags, FLinkerLoad* ImportLinker, FArchive* InReaderOverride,
    const FLinkerInstancingContext* InstancingContext, const FPackagePath* DiffPackagePath)
{
    //....
    //新创建一个空的Upackage
    UPackage* Result = nullptr;

    //....

    // Set up a load context
    TRefCountPtr<FUObjectSerializeContext> LoadContext = FUObjectThreadContext::Get().GetSerializeContext();

    UE_SCOPED_IO_ACTIVITY(*WriteToString<512>(TEXT(”Sync ”), PackagePath.GetDebugName()));

    // Try to load.
    BeginLoad(LoadContext, *PackagePath.GetDebugName());

    //....
    //创建一个FLinkerLoad执行序列化相关操作
    // Declare here so that the linker does not get destroyed before ResetLoaders is called
    FLinkerLoad* Linker = nullptr;

    //....
    /**GetPackageLinker获取一个Package的Linker并在不加载对象的情况下返回这个Linker。
    该函数调用之前必需要有BeginLoad(),之后必需有EndLoad()   **/
    FUObjectSerializeContext* InOutLoadContext = LoadContext;
            Linker = GetPackageLinker(InOuter, PackagePath, LoadFlags, nullptr, InReaderOverride, &InOutLoadContext, ImportLinker, InstancingContext);

    //....
    if (!Linker)
    {
      EndLoad(LoadContext, &LoadedPackages);
      BroadcastEndLoad(MoveTemp(LoadedPackages));
      return nullptr;
    }
}FLinkerLoad 是作为 uasset 和内存 UPackage 的中间桥梁。在加载内容生成 UPackage 的时候,UPackage 会按照名字找到 uasset 文件,由 FLinkerLoad 来负责加载。FLinkerLoad 主要内容如下:

[*]FArchive* Loader;      //Loader 负责读取具体文件
[*]TArray ImportMap;   //将 uasset 的 ImportTable 加载到 ImportMap 中,FObjectImport 是需要依赖(导入)的 UObject
[*]TArray ExportMap;   //FObjectExport 是这个 UPackage 所拥有的 UObject(这些 UObject 都能提供给其他 UPackage 作为 Import)
GetPackageLinker()函数首先会FindExistingLinkerForPackage去查找该Package是否已经有存在的Linker,并将成果赋值给局部变量Result,若已存在Linker则返回该Result,否则继续向下执行。


接下来会创建两个UPackage指针TargetPackage和CreatedPackage用来在内存中创建Package并按照PackageName与保留在硬盘上的资源名做比对,最终将创建好的Package传给TargetPackage。接着按照TargetPackage与LoadContext(调用GetPackageLinker()传入)创建新的Linker并将其返回。


执行完GetPackageLinker()可以发现Linker的ExportMap包含2个元素,一个是PackageMetaData,一个就是本身创建的UObject。


之后会执行Linker->LoadAllObjects(bool bForcePreload)来加载Package内的Object,参数暗示是否此刻就执行Preload()进行序列化还是在后续的Endload()方式中调用。UObjectGlobals.cpp定义了一个lambda表达式,通过调用该表达式在内部调用Endload()方式。具体的资源加载可参考文章。
//UObjectGlobals.cpp
auto EndLoadAndCopyLocalizationGatherFlag = [&]
{
   EndLoad(Linker->GetSerializeContext(), &LoadedPackages);
   // Set package-requires-localization flags from archive after loading. This reinforces flagging of packages that haven&#39;t yet been resaved.
   Result->ThisRequiresLocalizationGather(Linker->RequiresLocalizationGather());
};

//省略部门代码
void EndLoad(FUObjectSerializeContext* LoadContext, TArray<UPackage*>* OutLoadedPackages){
    //....
    for (int32 i = 0; i < ObjLoaded.Num(); i++)
    {
      // Preload.
      UObject* Obj = ObjLoaded;
      if (Obj->HasAnyFlags(RF_NeedLoad))
      {
            FLinkerLoad* Linker = Obj->GetLinker();
            check(Linker);

            UPackage* Package = Linker->LinkerRoot;
            check(Package);

            //....
            //Preload()内会调用Object->Serialize(*this)来执行序列化
            Linker->Preload(Obj);
            //....
      }
    //....
}对一个Object调用序列化函数后大致过程如下(见文章):

[*] 通过GetClass函数获取当前的类信息,通过GetOuter函数获取Outer。这个Outer实际上指定了当前UObject会被当作为哪一个对象的子对象进行序列化。
[*] 判断当前等待序列化的对象的类UClass的信息是否被载入,没有的话:
[*] 预载入当前类的信息;
[*] 预载入当前类的默认对象CDO的信息;
[*] 载入名字
[*] 载入Outer
[*] 载入当前对象的类信息,保留于ObjClass对象中。
[*] 载入对象的所有脚本成员变量信息。这一步必需在类信息加载后,否则无法按照类信息获得有哪些脚本成员变量需要加载。
[*] 对应函数为SerializeScriptProperties,序列化在类中定义的对象属性。
[*] 调用FArchive.MarkScriptSerializationStart,标识表记标帜脚本序列化数据开始;
[*] 调用SerializeTaggedProperties,序列化对象属性,而且插手tag;
[*] 调用FArchive.MarkScriptSerializationEnd,标识表记标帜脚本序列化数据结束。
//Obj.cpp
void UObject::Serialize(FStructuredArchive::FRecord Record)
{
    //....
    //获取当前Object的信息
    UClass *ObjClass = GetClass();
    UObject* LoadOuter = GetOuter();
    FName LoadName = GetFName();
    // Make sure this object&#39;s class&#39;s data is loaded.
    if(ObjClass->HasAnyFlags(RF_NeedLoad) )
    {
      //预载入当前类的信息
      UnderlyingArchive.Preload(ObjClass);

      if ( !HasAnyFlags(RF_ClassDefaultObject) && ObjClass->GetDefaultsCount() > 0 )
      {
            //预载入当前类的默认对象CDO的信息
            UnderlyingArchive.Preload(ObjClass->GetDefaultObject());
      }

      //....
      // Serialize object properties which are defined in the class.
      // Handle derived UClass objects (exact UClass objects are native only and shouldn&#39;t be touched)
      if (ObjClass != UClass::StaticClass())
      {
            //载入对象的所有脚本成员变量信息
            SerializeScriptProperties(Record.EnterField(TEXT(”Properties”)));
      }
      //....
      // 设置GUID
      FLazyObjectPtr::PossiblySerializeObjectGuid(this, Record)
    }
}TPS方式的序列化会执行UStruct::SerializeVersionedTaggedProperties()函数,通过PropertiesStream来获取加载每一个被UProperty Tag标识表记标帜的变量。每一个Property保留着下一个被标识表记标帜的变量用来流送。


创建一个FPropertyTag的布局体来存每一次加载的TaggedProperty,并调用SerializeTaggedProperty()实现序列化。
参考文章

UE4中的Serialization
大象无形UE笔记十一:UObject (二)
Ue4_序列化浅析
浅谈UE4序列化系列(1) 结合用例浅谈 UE4序列化
UE4资源加载(转载)
页: [1]
查看完整版本: Unreal Engine序列化个人笔记