# UE 基础篇
# Unreal 中常见类说明
# UObject
所有 UE 对象的基类
# UClass
用于描述 UObject 类型的类型
# FField
字段,组成复杂结构的基本单元
# UField
成员(成员字段 | 成员函数 | 成员结构),于 Unreal4.25 更新后移除了成员字段的作用,仅作为成员函数和成员结构使用。
https://docs.unrealengine.com/4.27/en-US/WhatsNew/Builds/ReleaseNotes/4_25/
# Unreal 中反射和类关系
# 反射机制
通俗来说就是通过一个实例获取到其类型和该类的全部定义。例如通过 Class_A 构造了 Instance_A,反射就是通过 Instance_A 获取到 Class_A 的成员。
反射可以实现的功能包括但不限于:根据类名动态创建实例,根据函数名调用函数,遍历实例的所有属性等等。
# 反射的实现
由于 C++ 本身是不支持反射的,那么 Unreal 是怎么做到反射效果的呢?
答案就是 UClass,前面说到 UClass 是描述 UObject 类型的类,言外之意,UObject 类的信息被记录在 UClass 对象里。
但是我们有很多的 UObject 对象,难道需要给每个 UObject 都自己创建一个 UClass 吗?
答案:是的,但是 Unreal 尽可能的简化了这部分,通过宏及自动生成的 gen.h
和 gen.cpp
来帮助我们在无感知的情况下,自动创建和注册对应的 UClass 实例
// MyTestObject.h | |
#pragma once | |
#include "CoreMinimal.h" | |
#include "UObject/NoExportTypes.h" | |
#include "MyTestObject.generated.h" | |
/** | |
* | |
*/ | |
UCLASS() | |
class BLUEPRINT_API UMyTestObject : public UObject | |
{ | |
GENERATED_BODY() | |
}; | |
// MyTestObject.cpp | |
#include "MyTestObject.h" |
这里我们创建一个最简单的类,其继承自 UObject,Unreal 会生成 gen 文件:
// MyTestObject.generated.h | |
#include "UObject/ObjectMacros.h" | |
#include "UObject/ScriptMacros.h" | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS | |
#ifdef BLUEPRINT_MyTestObject_generated_h | |
#error "MyTestObject.generated.h already included, missing '#pragma once' in MyTestObject.h" | |
#endif | |
#define BLUEPRINT_MyTestObject_generated_h | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_SPARSE_DATA | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_RPC_WRAPPERS | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_RPC_WRAPPERS_NO_PURE_DECLS | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_INCLASS_NO_PURE_DECLS \ | |
private: \ | |
static void StaticRegisterNativesUMyTestObject(); \ | |
friend struct Z_Construct_UClass_UMyTestObject_Statics; \ | |
public: \ | |
DECLARE_CLASS(UMyTestObject, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/BLUEPRINT"), NO_API) \ | |
DECLARE_SERIALIZER(UMyTestObject) | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_INCLASS \ | |
private: \ | |
static void StaticRegisterNativesUMyTestObject(); \ | |
friend struct Z_Construct_UClass_UMyTestObject_Statics; \ | |
public: \ | |
DECLARE_CLASS(UMyTestObject, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/BLUEPRINT"), NO_API) \ | |
DECLARE_SERIALIZER(UMyTestObject) | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_STANDARD_CONSTRUCTORS \ | |
/** Standard constructor, called after all reflected properties have been initialized */ \ | |
NO_API UMyTestObject(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); \ | |
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyTestObject) \ | |
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, UMyTestObject); \ | |
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(UMyTestObject); \ | |
private: \ | |
/** Private move- and copy-constructors, should never be used */ \ | |
NO_API UMyTestObject(UMyTestObject&&); \ | |
NO_API UMyTestObject(const UMyTestObject&); \ | |
public: | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_ENHANCED_CONSTRUCTORS \ | |
/** Standard constructor, called after all reflected properties have been initialized */ \ | |
NO_API UMyTestObject(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \ | |
private: \ | |
/** Private move- and copy-constructors, should never be used */ \ | |
NO_API UMyTestObject(UMyTestObject&&); \ | |
NO_API UMyTestObject(const UMyTestObject&); \ | |
public: \ | |
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, UMyTestObject); \ | |
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(UMyTestObject); \ | |
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyTestObject) | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_PRIVATE_PROPERTY_OFFSET | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_12_PROLOG | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_GENERATED_BODY_LEGACY \ | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS \ | |
public: \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_PRIVATE_PROPERTY_OFFSET \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_SPARSE_DATA \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_RPC_WRAPPERS \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_INCLASS \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_STANDARD_CONSTRUCTORS \ | |
public: \ | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
#define Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_GENERATED_BODY \ | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS \ | |
public: \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_PRIVATE_PROPERTY_OFFSET \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_SPARSE_DATA \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_RPC_WRAPPERS_NO_PURE_DECLS \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_INCLASS_NO_PURE_DECLS \ | |
Demo_Source_BLUEPRINT_Public_MyTestObject_h_15_ENHANCED_CONSTRUCTORS \ | |
private: \ | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
template<> BLUEPRINT_API UClass* StaticClass<class UMyTestObject>(); | |
#undef CURRENT_FILE_ID | |
#define CURRENT_FILE_ID Demo_Source_BLUEPRINT_Public_MyTestObject_h | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
// MyTestObject.gen.cpp | |
#include "UObject/GeneratedCppIncludes.h" | |
#include "BLUEPRINT/Public/MyTestObject.h" | |
#ifdef _MSC_VER | |
#pragma warning (push) | |
#pragma warning (disable : 4883) | |
#endif | |
PRAGMA_DISABLE_DEPRECATION_WARNINGS | |
void EmptyLinkFunctionForGeneratedCodeMyTestObject() {} | |
// Cross Module References | |
BLUEPRINT_API UClass* Z_Construct_UClass_UMyTestObject_NoRegister(); | |
BLUEPRINT_API UClass* Z_Construct_UClass_UMyTestObject(); | |
COREUOBJECT_API UClass* Z_Construct_UClass_UObject(); | |
UPackage* Z_Construct_UPackage__Script_BLUEPRINT(); | |
// End Cross Module References | |
void UMyTestObject::StaticRegisterNativesUMyTestObject() | |
{ | |
} | |
UClass* Z_Construct_UClass_UMyTestObject_NoRegister() | |
{ | |
return UMyTestObject::StaticClass(); | |
} | |
struct Z_Construct_UClass_UMyTestObject_Statics | |
{ | |
static UObject* (*const DependentSingletons[])(); | |
#if WITH_METADATA | |
static const UE4CodeGen_Private::FMetaDataPairParam Class_MetaDataParams[]; | |
#endif | |
static const FCppClassTypeInfoStatic StaticCppClassTypeInfo; | |
static const UE4CodeGen_Private::FClassParams ClassParams; | |
}; | |
UObject* (*const Z_Construct_UClass_UMyTestObject_Statics::DependentSingletons[])() = { | |
(UObject* (*)())Z_Construct_UClass_UObject, | |
(UObject* (*)())Z_Construct_UPackage__Script_BLUEPRINT, | |
}; | |
#if WITH_METADATA | |
const UE4CodeGen_Private::FMetaDataPairParam Z_Construct_UClass_UMyTestObject_Statics::Class_MetaDataParams[] = { | |
{ "Comment", "/**\n * \n */" }, | |
{ "IncludePath", "MyTestObject.h" }, | |
{ "ModuleRelativePath", "Public/MyTestObject.h" }, | |
}; | |
#endif | |
const FCppClassTypeInfoStatic Z_Construct_UClass_UMyTestObject_Statics::StaticCppClassTypeInfo = { | |
TCppClassTypeTraits<UMyTestObject>::IsAbstract, | |
}; | |
const UE4CodeGen_Private::FClassParams Z_Construct_UClass_UMyTestObject_Statics::ClassParams = { | |
&UMyTestObject::StaticClass, | |
nullptr, | |
&StaticCppClassTypeInfo, | |
DependentSingletons, | |
nullptr, | |
nullptr, | |
nullptr, | |
UE_ARRAY_COUNT(DependentSingletons), | |
0, | |
0, | |
0, | |
0x001000A0u, | |
METADATA_PARAMS(Z_Construct_UClass_UMyTestObject_Statics::Class_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UClass_UMyTestObject_Statics::Class_MetaDataParams)) | |
}; | |
UClass* Z_Construct_UClass_UMyTestObject() | |
{ | |
static UClass* OuterClass = nullptr; | |
if (!OuterClass) | |
{ | |
UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_UMyTestObject_Statics::ClassParams); | |
} | |
return OuterClass; | |
} | |
IMPLEMENT_CLASS(UMyTestObject, 1743818385); | |
template<> BLUEPRINT_API UClass* StaticClass<UMyTestObject>() | |
{ | |
return UMyTestObject::StaticClass(); | |
} | |
static FCompiledInDefer Z_CompiledInDefer_UClass_UMyTestObject(Z_Construct_UClass_UMyTestObject, &UMyTestObject::StaticClass, TEXT("/Script/BLUEPRINT"), TEXT("UMyTestObject"), false, nullptr, nullptr, nullptr); | |
DEFINE_VTABLE_PTR_HELPER_CTOR(UMyTestObject); | |
PRAGMA_ENABLE_DEPRECATION_WARNINGS | |
#ifdef _MSC_VER | |
#pragma warning (pop) | |
#endif |
gen 里定义了非常多的宏,展开后大致就是描述如何创建出该 UObject 的 UClass 对象,生成工具通过读取 UCLASS()
、 GENERATED_BODY()
等标注,来解析类的各种信息,如:类名、继承关系、元素名称和类型、函数等,然后再根据 gen 文件在创建 UClass 对象的时候把这些信息也添加上。
UCLASS() | |
class BLUEPRINT_API UMyTestObject : public UObject | |
{ | |
GENERATED_BODY() | |
// 声明一下该属性 | |
UPROPERTY(EditAnywhere) | |
FString ClassName; | |
// 声明一下该函数 | |
UFUNCTION() | |
TWeakObjectPtr<UPrimaryDataAsset> GetDataAsset(const FString& ClassName); | |
}; |
至于 UClass 是怎么创建的呢?这个后面再讲。接下来让我们看看 FField 和 UField。
# FField 和 UField
前面说到 UObject 的类信息是通过 UClass 来进行描述的,那么 UClass 用于描述类型信息的基本单位就是 Field。
Function 是个 Field,Enum 是个 Field,Property 是个 Field,Struct 和 Class 也是个 Field。
因此,一个类实际上就是一个个的 Field 互相组合嵌套而成的~,下面是 UField 的关系类图:
值得一提的是 UField 被设计成了一个单链表结构,因此 MyTestObject 类的组织形式大概长这样(省略了很多细节):
可以发现 MyTestObject 里面分成了 ChildProperties 和 Children 两部分,一边存储 Property,另一边存储 Function、Struct 等。
而划分规则是在 UStruct 定义的也就是从 UStruct 往下的派生中,开始支持嵌套了。
那么 FField 在哪呢?它取代了 UProperty 的地位,自立门户了~
由图中可以看出其实 UField 是继承自 UObject 的,好处是它的 UClass 也可以通过宏定义给创建出来,而代价是承载了 UObject 的冗余数据。这些数据对于 UClass 和 UFuntion 来说其实还可以接受,但是 UProperty 本身使用频繁且小巧,这么一来就显得很累赘。因此 Unreal 在 4.25 版本单独定义了一个 FField,来替代 UProperty 的职责,并且定义了一个 FFieldClass 作为 FField 的 UClass 类来实现类型系统。
# 类型系统的构建流程
前面也提到了,类的信息实际上是定义在 UClass 对象里,那么 UClass 对象的构建流程,顺序,以及如何通过名字查询等就有必要在此说明一下了:
注:以下内容仅仅简单说明,其中的实现细节较为复杂,不做展开,有兴趣可以阅读源码。
# 初始化 UObject 的 UClass 对象
由于 UObject 是所有对象的基类,因此它的 UClass 对象应该最先被创建的,事实也正是如此:
- 创建 UClass 对象
- 设置属性
- SuperStruct:定义了类型的继承关系
- ClassWithin:定义了 UClass 包含在哪个 UClass 内,用于描述嵌套关系
注:这里的初始化其实并不完全,UClass 内的 UFunction 和 UStruct 等都还没办法创建,相当于做了个占位
# 初始化 CoreUObject 的 UClass 对象
由于 UObject 是所有 CoreUObject 的基类嘛~所以 UObject 初始化好了,自然就轮到了 CoreUObject
初始化的内容和 UObject 的 UClass 一致,都是半成品,只保留了 SuperStruct 和 ClassWithin 的关系网
注:这一步其实没有初始化 UScriptStruct 和 UEnum,因为他们结构信息里面依赖了 UClass 所以得先把其他类的 UClass 初始化完毕才能轮到它们
# 注册之前初始化的 UClass 对象
到此我们就得到了所有 CoreUObject 的 UClass 对象的半成品,剩下的就是对它们进行组装,变成完全体
但是在组装之前,还得把原来遗漏的两个类型的 UClass 补上 ——UEnum 和 UScriptStruct,并把这些 UClass 实例装填到一个全局字典和全局链表里方便后续查询和遍历。
因此这一步需要做三件事:
- 把 UClass 对象注册到全局字典和全局链表里,并设置一些属性:
- NamePrivate:定义了对象的名字
- ClassPrivate:定义了对象的类型关系,UClass 实例是从哪个 UClass 类创建的(这里都是 UClass 类,但是如果有其他继承的话,就不一样了
- OuterPrivate:定义了对象的从属关系,UObject 所在的 UPackage
void UObjectBase::DeferredRegister(UClass *UClassStaticClass,const TCHAR* PackageName,const TCHAR* InName) | |
{ | |
check(UObjectInitialized()); | |
// Set object properties. | |
UPackage* Package = CreatePackage(PackageName); | |
check(Package); | |
Package->SetPackageFlags(PKG_CompiledIn); | |
OuterPrivate = Package; | |
check(UClassStaticClass); | |
check(!ClassPrivate); | |
ClassPrivate = UClassStaticClass; | |
// Add to the global object table. | |
AddObject(FName(InName), EInternalObjectFlags::None); | |
// Make sure that objects disregarded for GC are part of root set. | |
check(!GUObjectArray.IsDisregardForGC(this) || GUObjectArray.IndexToObject(InternalIndex)->IsRootSet()); | |
UE_LOG(LogUObjectBootstrap, Verbose, TEXT("UObjectBase::DeferredRegister %s %s"), PackageName, InName); | |
} |
- 凭借现有的 UClass 对象初始化 UEnum 和 UScriptStruct 的 UClass 对象
/** | |
* Call StaticStruct for each struct...this sets up the internal singleton, and important works correctly with hot reload | |
*/ | |
static void UObjectLoadAllCompiledInStructs() | |
{ | |
// 创建 UPackage | |
// ... | |
// Load Structs | |
for (const FPendingEnumRegistrant& EnumRegistrant : PendingEnumRegistrants) | |
{ | |
EnumRegistrant.RegisterFn(); | |
} | |
for (const FPendingStructRegistrant& StructRegistrant : PendingStructRegistrants) | |
{ | |
StructRegistrant.RegisterFn(); | |
} | |
} |
- 补全 UClass 对象里的 Function、Property、Struct 等信息,并创建 UClass 实例的 CDO
// UObjectBase.cpp | |
static void UObjectLoadAllCompiledInDefaultProperties() | |
{ | |
TArray<UClass*> NewClasses; | |
TArray<UClass*> NewClassesInCoreUObject; | |
TArray<UClass*> NewClassesInEngine; | |
TArray<UClass* (*)()> PendingRegistrants = MoveTemp(DeferredCompiledInRegistration); | |
for (UClass* (*Registrant)() : PendingRegistrants) | |
{ | |
// 核心代码在这里,这里会补全 UClass 信息 Registrant 是之前初始化的时候传进来的 | |
// MyTestObject 类对应的就是 Z_CompiledInDefer_UClass_UMyTestObject | |
UClass* Class = Registrant(); | |
UE_LOG(LogUObjectBootstrap, Verbose, TEXT("UObjectLoadAllCompiledInDefaultProperties After Registrant %s %s"), *Class->GetOutermost()->GetName(), *Class->GetName()); | |
// 这里把 UClass 对象分三类 CoreUObject Engine Other | |
if (Class->GetOutermost()->GetFName() == GLongCoreUObjectPackageName) | |
{ | |
NewClassesInCoreUObject.Add(Class); | |
} | |
else if (Class->GetOutermost()->GetFName() == LongEnginePackageName) | |
{ | |
NewClassesInEngine.Add(Class); | |
} | |
else | |
{ | |
NewClasses.Add(Class); | |
} | |
} | |
{ | |
// 这里就是按照类别依次创建每个 UClass 实例 的 CDO | |
// 顺序依次是 CoreUObject > Engine > Other,因为 UClass 依赖关系也是这个关系 | |
for (UClass* Class : NewClassesInCoreUObject) | |
{ | |
Class->GetDefaultObject(); | |
} | |
// ... | |
} | |
} | |
// MyTestObject.gen.cpp | |
static FCompiledInDefer Z_CompiledInDefer_UClass_UMyTestObject(Z_Construct_UClass_UMyTestObject, &UMyTestObject::StaticClass, TEXT("/Script/Blueprint"), TEXT("UMyTestObject"), false, nullptr, nullptr, nullptr); | |
UClass* Z_Construct_UClass_UMyTestObject() | |
{ | |
static UClass* OuterClass = nullptr; | |
if (!OuterClass) | |
{ | |
UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_UMyTestObject_Statics::ClassParams); | |
} | |
return OuterClass; | |
} | |
// 这里就定义了 function property 等信息 | |
struct Z_Construct_UClass_UMyTestObject_Statics | |
{ | |
static UObject* (*const DependentSingletons[])(); | |
#if WITH_METADATA | |
static const UE4CodeGen_Private::FMetaDataPairParam Class_MetaDataParams[]; | |
#endif | |
static const FCppClassTypeInfoStatic StaticCppClassTypeInfo; | |
static const UE4CodeGen_Private::FClassParams ClassParams; | |
}; |
到此,所有 UObject 的 UClass 对象就都创建好了,并且 UClass 对象里面也记录了各个类的信息用于实现反射效果。
# 其他介绍
# CDO
ClassDefaultObject,说白了就是 UClass 实例通过默认参数构建出的一个 UObject 实例,这个 UObject 对象有着所有字段的默认值
# UPackage
UPackage 是 Unreal 对于资源的包装,一个 uasset 文件对应了一个 Package,而把这个 Package 加载到内存后,创建的实例就是 UPackage
而一个 uasset 可以包含多个类、函数、变量
# UObject 和 UClass 常用字段说明
UObject:
- ClassPrivate:这个对象对应的 UClass 实例
- NamePrivate:对象名称,一般是类名
- OuterPrivate:这个 UObject 是在哪个 UPackage 里
UClass:
- ClassWithin:这个类被包含在了其他类的时候,通过该字段指向上一层
- SuperStruct:描述 UObject 的父类
- Children:描述 UObject 里面有哪些 Function、Struct
- ChildProperties:描述 UObject 里面有哪些 Property
# 总结
最后做个总结,所有 Unreal 内的对象都派生自 UObject,并且通过对应的标注 —— UCLASS()、GENERATED_BODY()...
来生成宏定义,用于创建对应的 UClass 对象。
而 UClass 对象则保存了 UObject 类里的各种定义信息,并存储在全局字典内,通过名字查找的方式实现了反射。并且所有 UObject 类的继承关系也一并保存在了 UClass 对象里。
此外 UClass 还会存储一个 UObject 的 CDO 对象,并且负责 UObject 对象的构造,可谓是责任重大。
而 UClass 又是通过 UField 定义的 Function、Struct、Enum 以及 FField 定义的 Property 所组成。