|
发表于 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 &#34;CoreMinimal.h&#34;
#include &#34;../PlayerObject.h&#34;
DEFINE_LOG_CATEGORY_STATIC(TestLog, Log, All);
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FObjectTest, &#34;MyTest.PublicTest.ObjectTest&#34;, EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
// 测试主函数
bool FObjectTest::RunTest(const FString& Param)
{
UPlayerObject *Link = NewObject<UPlayerObject>();
Link->CurPlayerName = TEXT(&#34;Link&#34;);
Link->CurAge = 117;
UE_LOG(TestLog, Display, TEXT(&#34;Done ......&#34;));
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(&#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;);
// 如果资源包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(&#34;Save Package Success! %s &#34;), *PackageFileName);
}
}
UE_LOG(TestLog, Display, TEXT(&#34;Done ......&#34;));
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(&#34;Load Package Success! %s &#34;), *PackageFileName);
}
然后我们运行,并打断点调试,可以看出Link对象也从加载出来,Link.uasset,并且它的CurPlayerName 与CurAge跟之前设置的是一致的。
总结
好的,这一节我们学习了UObject的实例化构造过程,并且了解了UObject如何保存到资源包,如何从之前保存的资源包中加载资源和对象的方法。
下一节我们会详细探讨UObject的序列化,敬请期待!~ |
|