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

2

主题

3

帖子

6

积分

新手上路

Rank: 1

积分
6
发表于 2022-12-2 14:56:35 | 显示全部楼层
概述

本节主要探讨虚幻引擎的UObject的实例化构造相关,并会简单探讨UObject保存到资源包与加载资源包等内容,主要对应原书第10章的10.1.1和10.1.2两个小节,让我们开始吧!
我们先来复习一下,UObject是UE引擎大部分类的基类,因为它有如下特点:


我们把新建一个类的对象的操作叫做实例化:


其中我们的UObject的实例化,需要调用 NewObject<T>()函数,而不能直接用C++的new操作符。
测试UObject类

我们先新建一个自己的UObject类的子类,并利用这个子类来理解UObject的实例化,与序列化操作。首先我们打开我们的MyGame项目,然后在内容浏览器的 All --> C++类 --> MyGame 那里右键,点击“新建C++”类:


这样,我们就创建一个UObject的子类,UPlayerObject类了!我们打开这个PlayerObject.h代码,并修改如下:


好的,我们的UObject测试子类准备好了。让我们开始测试它吧!首先是实例化,这里我们用之前的Automation System来做代码例子。
UObject的对象实例化

首先我们在项目目录的Source/MyGame/Private/Tests/ 文件夹创建了一个空的ObjectTest.cpp文件,并右击MyGame.uproject,重新生成了项目文件MyGame.sln,然后用Visual Studio打开项目,如下图:


我们打开ObjectTest.cpp并输入如下测试代码:
#include "CoreMinimal.h"
#include "../PlayerObject.h"

DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All);

IMPLEMENT_SIMPLE_AUTOMATION_TEST(FObjectTest, "MyTest.PublicTest.ObjectTest", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)


// 测试主函数
bool FObjectTest::RunTest(const FString& Param)
{
        UPlayerObject *Link = NewObject<UPlayerObject>();
        Link->CurPlayerName = TEXT("Link");
        Link->CurAge = 117;

        UE_LOG(TestLog, Display, TEXT("Done ......"));
        return true;
}
然后我们编译运行,并在UE5编辑器打开“工具”--> “会话前端”:


然后在打开的“会话前端”窗口中,选择“自动化”Tab按钮,然后勾上MyTest/PublicTest里面的ObjectTest,并点击“开始测试”,记得在UPlayerObject *Link = NewObject<UPlayerObject>(); 这行代码打上断点,并开始断点调试它。
经过我的断点调试发现,UE5的创建UObject的过程,跟书中的附图的过程基本上是一样的:



我们先看看上图的内存分配阶段


我们先来看看对象大小,首先我们这个UPlayerObject 的内存大小是72个字节,那么这72个字节是由哪些内容组成呢?我们可以调试打印一下sizeof(UObjectBase), 以及 sizeof(UObject), 发现两者都是48个字节,经过断点调试与变量的内存分析,可以得出UPlayerObject的内存布局如下:


所以我们GetPropertiesSize()得到72字节,然后我们调用FMemory::Malloc函数分配这72个字节的内存,并调用FMemory::Memzero函数把这72个字节的内容清空,这时它的所有成员变量的值为空,包括虚函数表指针:


然后我们给它调用UObjectBase的构造函数,初始化好它的UObectBase的各个成员变量:


这样就完成了我们的内存分配阶段。
然后我们看看对象构造阶段
这里我们先构造一个FObjectInitializer 对象作为构造函数参数,然后通过InClass->ClassConstructor()函数调用到这个类的默认构造函数,这里为什么不用PlacementNew来直接完成构造,而需要用函数指针呢?主要是为了灵活,虚幻引擎希望能够获得某个类的构造函数,就像一个点金手一样,划出一片内存,然后点一下就会模塑出一个类的对象。甚至在点金手的时候,不需要知道这个点金手所属的到底是哪个类。如果使用PlacementNew方案就地构造,就必然需要传入类型参数作为模板参数,不够灵活。
对象的保存与加载

我们先保存一个我们UPlayerObject对象到到一个资源包文件(*.uasset)出来,以便以后可以随时加载它。首先,修改之前的ObjectTest.cpp增加一个SavePlayerAsset来保存我们的PlayerObject,代码如下:
// 保存 一个UPlayerObject的对象到指定路径的.uasset资产
bool SavePlayerAsset(const FString &AssetPath, const FString &PackageFileName, const FString &ObjectName)
{
        // 创建一个空的资源Package
        UPackage* Package = CreatePackage(nullptr, *AssetPath);
        Package->FullyLoad();

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

        // 设置“林克”的对象属性
        Link->CurPlayerName = ObjectName;
        Link->CurAge = 117;
        Package->MarkPackageDirty();

        // 保存这个“林克”对象到一个指定路径的*.uasset资源文件
        bool bSaved = UPackage::SavePackage(Package, Link, EObjectFlags::RF_Public | EObjectFlags::RF_Standalone, *PackageFileName, GError, nullptr, true, true, SAVE_NoError);
        return bSaved;
}
然后我们修改测试入口函数:RunTest如下:
// 测试入口函数
bool FObjectTest::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");

        // 如果资源包MyGame/Content/Link.uasset已存在,则不需要重复保存
        if (FPaths::FileExists(PackageFileName))
        {
        }
        // 否则MyGame/Content/Link.uasset不存在,让我们来保存一下资源包
        else
        {
                bool bSaved = SavePlayerAsset(AssetPath, PackageFileName, ObjectName);

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

        UE_LOG(TestLog, Display, TEXT("Done ......"));
        return true;
}
然后我们我们编译运行,并在UE5编辑器打开“工具”--> “会话前端”,并在“会话前端”界面选择“自动化”Tab按钮,然后勾上MyTest/PublicTest里面的ObjectTest,并点击“开始测试”。
然后我们可以看到结果:


然后我们在MyGame/Content目录,也看到了这个Link.uasset文件了:


好的,让我们来尝试加载一下它,我们修改ObjectTest的测试入口函数RunTest,在if(FPaths::FileExists(PackageFileName))判断分支,增加如下代码:
        // 如果资源包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);
        }
然后我们运行,并打断点调试,可以看出Link对象也从加载出来,Link.uasset,并且它的CurPlayerName 与CurAge跟之前设置的是一致的。
总结

好的,这一节我们学习了UObject的实例化构造过程,并且了解了UObject如何保存到资源包,如何从之前保存的资源包中加载资源和对象的方法。
下一节我们会详细探讨UObject的序列化,敬请期待!~
回复

举报 使用道具

2

主题

3

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2022-12-2 14:56:57 | 显示全部楼层
厉害。[赞同][赞同][赞同]
回复

举报 使用道具

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