UE里的反射(Reflections)机制

年底了,把之前的草稿文章整理一下,整理好的发出来

参考:https://zhuanlan.zhihu.com/p/518907376
参考:https://www.cnblogs.com/ghl_carmack/p/5698438.html
参考:https://www.unrealengine.com/zh-CN/blog/unreal-property-system-reflection
参考:https://gwb.tencent.com/community/detail/121618
参考:https://blog.csdn.net/qq_24951423/article/details/105932183?ydreferer=aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8%3D

前言

由于C++语言是不支持反射的,像UE这种引擎为了支持反射,就只能自己设计一套系统了,UE里这套系统叫做Unreal Header Tool (UHT),实现反射的核心思路也比较清晰明了,就是让用户在需要支持反射的类代码里加对应的宏,然后UHT在编译的时候会为其生成相关代码,比如要给一个变量加到反射系统里,就在它前面给它加一个UPROPERTY的宏(有点类似于C#里的给Property加Attribute)。除此之外,在cpp和h文件里写一些简单的宏,每次build工程时还能让UHT能帮助生成一堆方便的函数,比如构造函数、StaticClass等

有一些比较常见的规则,比如说:

  • 用UE提供的UENUM()UCLASS()USTRUCT()UFUNCTION()UPROPERTY()等宏去给代码添加metadata
  • UObject是所有支持反射类的基类
  • 如果想让哪个类支持反射,那么需要在头文件里添加相关的宏,再在对应的cpp里就加入#include "FileName.generated.h"

下面再来深入探讨一下UE的反射机制,在此之前,提出一些问题:

  • .generated.h.gen.cpp文件是怎么产生的
  • .generated.h.gen.cpp文件有什么用
  • UE里创建新类时,里面的那些宏(比如UCLASS()GENERATE_BODY())是怎么加到代码里的
  • UE里有很多UClass类似的空宏,这些宏具体是怎么参与到代码里

我觉得一个学习反射的很好的思路,就是在UE的Editor里新建一个Actor类,然后从源码级别进行解析,看看UE具体做了什么。UE里创建Actor类时,会依次做以下事情:

  • 根据用户输入的fileName,创建对应的头文件和源文件
  • 调用UE的UHT,为对应的类生成.generated.h.gen.cpp文件

先看看UE做了什么,后面再去研究为什么这么做。

UE Editor里创建新的类

在UE Editor里选择New C++ Class时,根据用户输入的fileName,UE会基于自己的模板文件,创建对应的头文件和源文件,给类添加GENERATE_BODY()这些宏。

UE提供了很多类模板的文件,如下图所示:
在这里插入图片描述
这里的模板文件既包括.h文件,也包括.cpp文件,随便打开一个,就知道UE是怎么做的了:

// ActorClass.h.template
%COPYRIGHT_LINE%

#pragma once

#include "CoreMinimal.h"
%BASE_CLASS_INCLUDE_DIRECTIVE%
#include "%UNPREFIXED_CLASS_NAME%.generated.h"

UCLASS(%UCLASS_SPECIFIER_LIST%)
class %CLASS_MODULE_API_MACRO%%PREFIXED_CLASS_NAME% : public %PREFIXED_BASE_CLASS_NAME%
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	%PREFIXED_CLASS_NAME%();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	%CLASS_FUNCTION_DECLARATIONS%
	%CLASS_PROPERTIES%
};


// ActorClass.cpp.template
%COPYRIGHT_LINE%

%PCH_INCLUDE_DIRECTIVE%
%MY_HEADER_INCLUDE_DIRECTIVE%
%ADDITIONAL_INCLUDE_DIRECTIVES%

// Sets default values
%PREFIXED_CLASS_NAME%::%PREFIXED_CLASS_NAME%()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void %PREFIXED_CLASS_NAME%::BeginPlay()
{
	Super::BeginPlay();
	%CURSORFOCUSLOCATION%
}

// Called every frame
void %PREFIXED_CLASS_NAME%::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

%ADDITIONAL_MEMBER_DEFINITIONS%

这个还是挺简单的,无非就是创建C++类时,根据指定的类名等信息,把这些内容往这个模板上一套,然后更新一下项目文件,把它们Include进来应该就行了,具体的代码在GameProjectUtils::AddCodeToProject_Internal里,就不深究了


.generated.h.gen.cpp文件是怎么产生的

当我在Unreal Editor创建一个新的C++的Actor类时,创建的MyActor.h头文件里会自动加入#include "MyActor.generated.h",看了下,对应的文件也在下面的目录里被创建出来了:
在这里插入图片描述
如果创建一个类型为None的类,那么不会有这些数据,因为它不是UObject

在创建MyActor类的时候,我发现Console窗口有这么几行提示:

Creating makefile for UnrealHeaderTool (no existing makefile)
Parsing headers for MyDemoEditor
    Running UnrealHeaderTool "F:\MyDemo\MyDemo.uproject" "F:\MyDemo\Intermediate\Build\Win64\MyDemoEditor\Debug\MyDemo.uhtmanifest" -LogCmds="loginit warning, logexit warning, logdatabase error" -Unattended -WarningsAsErrors -abslog="F:\UnrealSource\UnrealEngine\Engine\Programs\UnrealBuildTool\Log_UHT.txt"
  Reflection code generated for MyDemoEditor in 2.2587637 seconds
...

核心就是调用了UnrealHeaderTool.exe,命令行参数输入了uproject的路径,以及对应的uhtmanifest文件的路径,我去看了下这俩文件的内容,发现:

  • uhtmanifest文件:貌似是代表了了整个工程依赖module情况的manifest
  • 输出的Log_UHT文件记录了调用UTH创建.cpp.h文件的情况

我删掉了相应的generated文件,然后重新命令行按这个操作调用了一次UnrealHeaderTool .exe,发现相关文件被重新生成了,说明Unreal Editor就是在创建新文件时,通过调用UnrealHeaderTool.exe为新文件创建对应的generated文件的

我自己尝试把之前的参数作为ARGC和ARGV,Debug调用了一下UHT对应的project,发现也能生成对应的cpp和h文件,这就很舒服了,这意味着相关的代码也是可以Debug的。


分析创建的AMyActor类

至此,UE在创建代码时做的工作都已经清楚了,还不清楚的是它为什么要这么做,这里先来看看创建的类的头文件:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"

UCLASS()
class MYDEMO_API AMyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMyActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

这里参与反射的特殊代码,其实就是:

  • #include "MyActor.generated.h"这条include指令
  • UCLASSGENERATED_BODY()俩宏

观察了一下这些代码,发现了以下信息:

  1. UCLASS()是个空宏,但虽然它是空宏,但这两个宏和这一个include指令,三者去掉任何一个,项目buid都会失败
  2. GENERATED_BODY只是一个文本的替换宏,本质上为CURRENT_FILE_ID___LINE___GENERATED_BODY
  3. MyActor.generated.h里定义了CURRENT_FILE_ID宏为FID_MyDemo_Source_MyDemo_MyActor_h

所以在我们翻译完这两个宏后,此时的函数可以替换为:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"

#define UCLASS
class MYDEMO_API AMyActor : public AActor
{
	FID_MyDemo_Source_MyDemo_MyActor_h_12_GENERATED_BODY // 直接把_LINE_替换为所在的行数12
	
	...
};

不过虽然可以替换成上面的代码,但是直接去build仍然会失败:

1>  Running Internal UnrealHeaderTool F:\MyDemo\MyDemo.uproject F:\MyDemo\Intermediate\Build\Win64\MyDemoEditor\Development\MyDemoEditor.uhtmanifest -WarningsAsErrors -installed
1>F:\MyDemo\Source\MyDemo\MyActor.h(26): error : Expected a GENERATED_BODY() at the start of the class

这倒不是本身项目编译的问题,而是UHT会随着这里的.h文件去更新对应的.generated.h代码,它这里只认GENERATED_BODY,所以替换后它找不到了。


generated.h的作用

所以这里的重点就全在.generated.h文件里怎么定义这个FID_MyDemo_Source_MyDemo_MyActor_h_12_GENERATED_BODY了,进去就能看到这个宏又等于一系列的小宏:
在这里插入图片描述
注意上面还有个GENERATED_BODY_LEGACY()的版本,但是我们这里是用的GENERATED_BODY(),所以可以忽略这一部分的代码

关于MyActor.generated.h文件,里面除了声明StaticClass的模板特化函数,其他的全是定义的宏,这里定义了一些空宏:

#define X_h_12_SPARSE_DATA                           // 第12行是GENERATE_BODY()
#define X_h_12_RPC_WRAPPERS
#define X_h_12_RPC_WRAPPERS_NO_PURE_DECLS
#define X_h_12_ACCESSORS
#define X_h_9_PROLOG								 // 第9行是UCLASS()	

X_h_数字代表文件路径加代码行数,这里看上去复杂,但是为了避免不同类里文件声明的宏不同,只能采用这种方法了,代码的路径,加上头、源文件的标识,再加上所在的行数,能有效确保宏名的唯一性。

然后定义了几个重要的宏:

  • X_NO_PURE_DECLS:用于定义一些Traits
  • X_INCLASS
  • X_STANDARD_CONSTRUCTORS:构造函数、虚函数相关
  • X_ENHANCED_CONSTRUCTORS:构造函数、虚函数相关

此时注解一下,可以得到:

PRAGMA_DISABLE_DEPRECATION_WARNINGS 
public: 
	#define FID_MyDemo_Source_MyDemo_MyActor_h_12_SPARSE_DATA // 空宏
	#define FID_MyDemo_Source_MyDemo_MyActor_h_12_RPC_WRAPPERS_NO_PURE_DECLS // 空宏
	#define FID_MyDemo_Source_MyDemo_MyActor_h_12_ACCESSORS 	// 空宏
	FID_MyDemo_Source_MyDemo_MyActor_h_12_INCLASS_NO_PURE_DECLS // 重要宏
	FID_MyDemo_Source_MyDemo_MyActor_h_12_ENHANCED_CONSTRUCTORS // 重要宏
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS

所以这里的GENERATED_BODY()其实主要是用X_INCLASSX_ENHANCED_CONSTRUCTORS为MyActor类加了这么些代码,多的就不写了,
我这里调整了一下代码layout,删除了一些无关代码,最终整理为:

class __declspec(dllexport) AMyActor : public AActor
{
	// 类的一些Traits定义在了X_NO_PURE_DECLS里
	__pragma(warning(push)) __pragma(warning(disable: 4995)) __pragma(warning(disable: 4996));
private: 
	__pragma(warning(pop));

	static void StaticRegisterNativesAMyActor(); 
	friend struct Z_Construct_UClass_AMyActor_Statics; 

	AMyActor& operator=(AMyActor&&); 
	AMyActor& operator=(const AMyActor&);  
	static UClass* GetPrivateStaticClass(); 
	public: enum { StaticClassFlags = (0 | CLASS_Config | CLASS_Intrinsic) };
	typedef AActor Super; typedef AMyActor ThisClass; 
	inline static UClass* StaticClass() { return GetPrivateStaticClass(); } 
	inline static const TCHAR* StaticPackage() { return L"/Script/MyProjectUE5_1_0"; } 
	inline static EClassCastFlags StaticClassCastFlags() { return CASTCLASS_None; } 
	inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) { return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags); } 
	inline void* operator new(const size_t InSize, EInternal* InMem) { return (void*)InMem; } 
	inline void operator delete(void* InMem) { ::operator delete(InMem); } 
	friend FArchive& operator<<(FArchive& Ar, AMyActor*& Res) { return Ar << (UObject*&)Res; } 
	friend void operator<<(FStructuredArchive::FSlot InSlot, AMyActor*& Res) { InSlot << (UObject*&)Res; } 

    // 构造函数、虚函数相关的代码均来自X_INCLASS
public:  
	AMyActor(FVTableHelper& Helper);
	virtual ~AMyActor(); 
	
	static UObject* __VTableCtorCaller(FVTableHelper& Helper) { return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) AMyActor(Helper); }; 
	static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())AMyActor; }  	

private:
	AMyActor(AMyActor&&);
	AMyActor(const AMyActor&);


// 剩余的原本的内容
public:	
	// Sets default values for this actor's properties
	AMyActor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
}

总结GENERATE_BODY()

前面加的UCLASS是个空宏,所以实质性有影响的代码就只有GENERATE_BODY()了,它主要为MyActor添加了:

  1. 带参数的公有构造函数AMyActor(FVTableHelper& Helper),暂时不清楚参数代表着什么
  2. 公有的虚析构函数
  3. 私有的move constructor、copy constructor、move assignment operator和copy assignmen operator:这意味着这个类是无法被拷贝和move的
  4. 两个typedef,定义了父类为Super,自身类为ThisClass


GENERATE_BODYGENERATED_UCLASS_BODY

参考:https://forums.unrealengine.com/t/generated-body-vs-generated-uclass-body/322489

区别不大,这里就不细剖源码,区别有:

  • GENERATE_BODY后面默认是private,GENERATED_UCLASS_BODY后面默认是public
  • 有了GENERATE_BODY就可以不用额外定义构造函数了,但GENERATED_UCLASS_BODY必须自定义构造函数

区别不大,当你想要创建的类不需要用到CTOR时,可以用GENERATE_BODY宏,否则可以使用GENERATED_UCLASS_BODY,当然了,感觉一直用GENERATE_BODY貌似也是没毛病的。貌似在派生类调用父类时这俩宏的区别比较大,以后再研究吧

但我发现,创建这么个类是可以编译的:

UCLASS()
class SHIPGAMEDEMO_API UChangePlayRateAnimNotify : public UAnimNotify
{
	GENERATED_BODY()
}

但这么写就报错了:

UCLASS()
class SHIPGAMEDEMO_API UChangePlayRateAnimNotify : public UAnimNotify
{
	GENERATED_UCLASS_BODY()
}

报错信息为:

 unresolved external symbol "public: __cdecl UChangePlayRateAnimNotify::UChangePlayRateAnimNotify(class FObjectInitializer const &)" (??

我这里直接用VS2022提供的宏翻译功能把代码贴出来:

// ============================ 展开GENERATED_BODY ==========================
__pragma (warning(push)) __pragma (warning(disable: 4995)) __pragma (warning(disable: 4996)) public: private: static void StaticRegisterNativesUChangePlayRateAnimNotify(); friend struct Z_Construct_UClass_UChangePlayRateAnimNotify_Statics; public: private: UChangePlayRateAnimNotify& operator=(UChangePlayRateAnimNotify&&); UChangePlayRateAnimNotify& operator=(const UChangePlayRateAnimNotify&); static UClass* GetPrivateStaticClass(); public: static constexpr EClassFlags StaticClassFlags = EClassFlags((0 | CLASS_Intrinsic)); typedef UAnimNotify Super; typedef UChangePlayRateAnimNotify ThisClass; inline static UClass* StaticClass() {
        return GetPrivateStaticClass();
    } inline static const TCHAR* StaticPackage() {
        return L"/Script/ShipGameDemo";
    } inline static EClassCastFlags StaticClassCastFlags() {
        return CASTCLASS_None;
    } inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) {
        return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags);
    } inline void* operator new(const size_t InSize, EInternal* InMem) {
        return (void*)InMem;
    } inline void operator delete(void* InMem) {
        ::operator delete(InMem);
    } friend FArchive& operator<<(FArchive& Ar, UChangePlayRateAnimNotify*& Res) {
        return Ar << (UObject*&)Res;
    } friend void operator<<(FStructuredArchive::FSlot InSlot, UChangePlayRateAnimNotify*& Res) {
        InSlot << (UObject*&)Res;
    } UChangePlayRateAnimNotify(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); private: UChangePlayRateAnimNotify(UChangePlayRateAnimNotify&&); UChangePlayRateAnimNotify(const UChangePlayRateAnimNotify&); public: UChangePlayRateAnimNotify(FVTableHelper& Helper);; static UObject* __VTableCtorCaller(FVTableHelper& Helper) {
        return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) UChangePlayRateAnimNotify(Helper);
    }; static void __DefaultConstructor(const FObjectInitializer& X) {
        new((EInternal*)X.GetObj())UChangePlayRateAnimNotify(X);
    } virtual ~UChangePlayRateAnimNotify(); private: __pragma (warning(pop));
	


// ============================ 展开GENERATED_UCLASS_BODY ==========================
 __pragma (warning(push)) __pragma (warning(disable: 4995)) __pragma (warning(disable: 4996)) public: private: static void StaticRegisterNativesUChangePlayRateAnimNotify(); friend struct Z_Construct_UClass_UChangePlayRateAnimNotify_Statics; public: private: UChangePlayRateAnimNotify& operator=(UChangePlayRateAnimNotify&&); UChangePlayRateAnimNotify& operator=(const UChangePlayRateAnimNotify&); static UClass* GetPrivateStaticClass(); public: static constexpr EClassFlags StaticClassFlags = EClassFlags((0 | CLASS_Intrinsic)); typedef UAnimNotify Super; typedef UChangePlayRateAnimNotify ThisClass; inline static UClass* StaticClass() {
        return GetPrivateStaticClass();
    } inline static const TCHAR* StaticPackage() {
        return L"/Script/ShipGameDemo";
    } inline static EClassCastFlags StaticClassCastFlags() {
        return CASTCLASS_None;
    } inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) {
        return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags);
    } inline void* operator new(const size_t InSize, EInternal* InMem) {
        return (void*)InMem;
    } inline void operator delete(void* InMem) {
        ::operator delete(InMem);
    } friend FArchive& operator<<(FArchive& Ar, UChangePlayRateAnimNotify*& Res) {
        return Ar << (UObject*&)Res;
    } friend void operator<<(FStructuredArchive::FSlot InSlot, UChangePlayRateAnimNotify*& Res) {
        InSlot << (UObject*&)Res;
    } UChangePlayRateAnimNotify(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); static void __DefaultConstructor(const FObjectInitializer& X) {
        new((EInternal*)X.GetObj())UChangePlayRateAnimNotify(X);
    } UChangePlayRateAnimNotify(FVTableHelper& Helper);; static UObject* __VTableCtorCaller(FVTableHelper& Helper) {
        return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) UChangePlayRateAnimNotify(Helper);
    }; private: UChangePlayRateAnimNotify(UChangePlayRateAnimNotify&&); UChangePlayRateAnimNotify(const UChangePlayRateAnimNotify&); public: virtual ~UChangePlayRateAnimNotify(); public: __pragma (warning(pop));


关于UClass

可以看看UE里的定义:

// 我电脑上的UE_BUILD_DOCS为0, defined(__INTELLISENSE__ )为1
#if UE_BUILD_DOCS || defined(__INTELLISENSE__ )
#define UCLASS(...)								// 我电脑下走的是这个分支
#else
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#endif

这里的BODY_MACRO_COMBINE是个单纯的字符拼接的宏,这里相当于:

#define UCLASS(...) CURRENT_FILE_ID___LINE__,PROLOG

不过我这里走的是上面的这个分支,此时有#define UCLASS(...) ,说明UCLASS是个空宏,不产生任何实际的Binary代码。

事实上,UE里很多这样的空宏,它们不参与真正的编译过程,只是用于帮助UHT自动生成代码,这些空宏还有:

// These macros wrap metadata parsed by the Unreal Header Tool, and are otherwise
// ignored when code containing them is compiled by the C++ compiler
#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)

至于它们怎么帮助UHT作用于反射系统的,后面再分析,这里就知道它们是空宏就可以了。


UCLASS(abstract)

貌似加了这个宏的类,里面的函数会变成纯虚函数,但我又没看到virtual XXXX() = 0这种语法,所以研究一下

参考:https://www.reddit.com/r/unrealengine/comments/l3mo5h/how_to_create_an_abstract_c_class_in_ue4/

So UE4 Blueprints don’t really have a system for pure virtual functions. The Abstract tag in the UCLASS macro makes it so u cannot drag this actor into the level.
What you can do is to implement the pure virtual functions and then log an fatal error with some text message, this will crash the engine upon execution and give you the provided error message.

貌似UE里还不支持带有纯虚函数的类,这里建议的是实现基类的纯虚函数,让它打一些Error的Log来提示错误,所以并不是加了此Tag的类会提供纯虚函数的接口,而是标了UCLASS(abstract)的Actor不可以拖拽到level里而已


BlueprintNativeEvent

这是个UFUNCTION的specifier,很神奇,加了这个UFUNCTION(BlueprintNativeEvent)的函数,假设叫函数Func,如下所示:

UCLASS(abstract, Blueprintable, const, hidecategories=Object, collapsecategories)
class ENGINE_API UAnimNotify : public UObject
{
	GENERATED_UCLASS_BODY()

	// Implementable event to get a custom name for the notify
	UFUNCTION(BlueprintNativeEvent)
	FString GetNotifyName() const;
	...
}

这个宏标记的函数是可以被蓝图override的,build之后,反射系统会生成一个Native的虚函数:

virtual FString GetNotifyName_Implementation() const;

再自行去cpp文件里实现此虚函数即可




分析具体的类

这里有个最简单的类

// MyActor.h
#include "GameFramework/Actor.h"
 #include "MyActor.generated.h" 
UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor();
    virtual void Tick( float DeltaSeconds ) override;

protected:
    virtual void BeginPlay() override;

private:
    UPROPERTY()
    int32 Something;
};

找了半天,终于学会了如何看UE里cpp被预处理为.i文件的方法(见附录),看了下,输出文件有67.9W行代码,不过里面很多空白行。

这里来看看它的宏被翻译后的实际类代码,对于GENERATED_BODY,编辑器把它的内容全部展开到了一行里:

__pragma(warning(push)) __pragma(warning(disable: 4995)) __pragma(warning(disable: 4996))


template<> __declspec(dllexport) UClass* StaticClass<class AMyActor>();


__pragma(warning(pop))
#line 8 "F:\\UnrealProjects\\MyProjectUE5_1_0\\Source\\MyProjectUE5_1_0\\Public\\MyActor.h"


class __declspec(dllexport) AMyActor : public AActor
{
	__pragma(warning(push)) __pragma(warning(disable: 4995)) __pragma(warning(disable: 4996));
	public:    private: static void StaticRegisterNativesAMyActor(); friend struct Z_Construct_UClass_AMyActor_Statics; public: private: AMyActor& operator=(AMyActor&&); AMyActor& operator=(const AMyActor&);  static UClass* GetPrivateStaticClass(); public: enum {StaticClassFlags=(0 | CLASS_Config | CLASS_Intrinsic)}; typedef AActor Super; typedef AMyActor ThisClass; inline static UClass* StaticClass() { return GetPrivateStaticClass(); } inline static const TCHAR* StaticPackage() { return L"/Script/MyProjectUE5_1_0"; } inline static EClassCastFlags StaticClassCastFlags() { return CASTCLASS_None; } inline void* operator new(const size_t InSize, EInternal InInternalOnly, UObject* InOuter = (UObject*)GetTransientPackage(), FName InName = NAME_None, EObjectFlags InSetFlags = RF_NoFlags) { return StaticAllocateObject(StaticClass(), InOuter, InName, InSetFlags); } inline void* operator new( const size_t InSize, EInternal* InMem ) { return (void*)InMem; } inline void operator delete(void* InMem) { ::operator delete(InMem); } friend FArchive &operator<<( FArchive& Ar, AMyActor*& Res ) { return Ar << (UObject*&)Res; } friend void operator<<(FStructuredArchive::FSlot InSlot, AMyActor*& Res) { InSlot << (UObject*&)Res; } private:  AMyActor(AMyActor&&);  AMyActor(const AMyActor&); public:  AMyActor(FVTableHelper& Helper);; static UObject* __VTableCtorCaller(FVTableHelper& Helper) { return new (EC_InternalUseOnlyConstructor, (UObject*)GetTransientPackage(), NAME_None, RF_NeedLoad | RF_ClassDefaultObject | RF_TagGarbageTemp) AMyActor(Helper); }; static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())AMyActor; }  virtual ~AMyActor(); private: __pragma(warning(pop));
	
public:	
	
	AMyActor();

protected:
	
	virtual void BeginPlay() override;

public:	
	
	virtual void Tick(float DeltaTime) override;

};
#line 5 "F:\\UnrealProjects\\MyProjectUE5_1_0\\Source\\MyProjectUE5_1_0\\Private\\MyActor.cpp"


AMyActor::AMyActor()
{
 	
	PrimaryActorTick.bCanEverTick = true;

}

问题的起因是我看到UE源码里有众多UPROPERTY这种东西,拿CharacterMovementComponent.h举个例子:

#pragma once
#include "CoreMinimal.h"
...
// 注意这个头文件的include
#include "CharacterMovementComponent.generated.h"

// 类的前面有UCLASS()
UCLASS()
class ENGINE_API UCharacterMovementComponent : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface
{
	GENERATED_BODY()
	...
protected:

	// 属性的前面有UPROPERTY(...)
	/** Character movement component belongs to */
	UPROPERTY(Transient, DuplicateTransient)
	TObjectPtr<ACharacter> CharacterOwner;

	...
public:
	/** Fraction of JumpZVelocity to use when automatically "jumping off" of a base actor that's not allowed to be a base for a character. (For example, if you're not allowed to stand on other players.) */
	UPROPERTY(Category="Character Movement: Jumping / Falling", EditAnywhere, BlueprintReadWrite, AdvancedDisplay, meta=(ClampMin="0", UIMin="0"))
	float JumpOffJumpZFactor;
	...
	
	/** Ignores size of acceleration component, and forces max acceleration to drive character at full velocity. */
	UPROPERTY()
	uint8 bForceMaxAccel:1;    
	...

	// 少数变量前面没有UPROPERTY
	int32 NumJumpApexAttempts;
}

由于C++语言本身没有反射机制,UE就用这种方式为每个需要参与反射的代码加这种宏了,UPROPERTY这种东西相当于把变量登记到UE的反射系统里。后面再来看看具体的实现。



UE文档里对反射的介绍

参考:https://docs.unrealengine.com/5.1/en-US/unreal-engine-uproperties/

看了下文档,写的比较水,没有什么干货。

UPROPERTY写法如下:

UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)])
Type VariableName;

这里就记录了下所有的specifier和meta specifier。

可供选择的specifier有:

  • AdvancedDisplay
  • AssetRegistrySearchable
  • BlueprintAssignable
  • BlueprintAuthorityOnly
  • BlueprintCallable
  • BlueprintGetter = GetterFunctionName
  • BlueprintReadOnly
  • BlueprintReadWrite
  • BlueprintSetter = SetterFunctionName
  • Category = “TopCategory|SubCategory|…”
  • Config
  • DuplicateTransient
  • EditAnywhere
  • EditDefaultsOnly
  • EditFixedSize
  • EditInline
  • EditInstanceOnly
  • Export
  • GlobalConfig
  • Instanced
  • Interp
  • Localized
  • Native
  • NoClear
  • NoExport
  • NonPIEDuplicateTransient
  • NonTransactional
  • NotReplicated
  • Replicated
  • ReplicatedUsing = FunctionName
  • RepRetry
  • SaveGame
  • SerializeText
  • SkipSerialization
  • SimpleDisplay
  • TextExportTransient
  • Transient
  • VisibleAnywhere
  • VisibleDefaultsOnly
  • VisibleInstanceOnly

meta栏位里也有对应的Specifiers:

  • AllowAbstract = “true/false”
  • AllowedClasses = “Class1, Class2, …”
  • AllowPreserveRatio
  • ArrayClamp = “ArrayProperty”
  • AssetBundles
  • BlueprintBaseOnly
  • BlueprintCompilerGeneratedDefaults
  • ClampMin = “N”
  • ClampMax = “N”
  • ConfigHierarchyEditable
  • ContentDir
  • DisplayAfter = “PropertyName”
  • DisplayName = “Property Name”
  • DisplayPriority = “N”
  • DisplayThumbnail = “true”
  • EditCondition = “BooleanPropertyName”
  • EditFixedOrder
  • ExactClass = “true”
  • ExposeFunctionCategories = “Category1, Category2, …”
  • ExposeOnSpawn = “true”
  • FilePathFilter = “FileType”
  • GetByRef
  • HideAlphaChannel
  • HideViewOptions
  • InlineEditConditionToggle
  • LongPackageName
  • MakeEditWidget
  • NoGetter
// 比如
/*~ BasicBits appears as a list of generic flags in the editor, instead of an integer field. */
UPROPERTY(EditAnywhere, Meta = (Bitmask))
int32 BasicBits;

这里的Meta = (Bitmask)会让编辑器把它当一个类似枚举的flag处理:

在这里插入图片描述


UE的常见反射宏

基本就这么些:

  • UCLASS
  • UENUM
  • USTRUCT
  • UFUNCTION
  • UNPROPERTY
  • GENERATED_UCLASS_BODY、GENERATED_UCLASS_BODY


关于UCLASS

可以看看代码里对该宏的定义:

#if UE_BUILD_DOCS || defined(__INTELLISENSE__ )
#define UCLASS(...)// UCLASS(...), 应该意味着所有的UCLASS宏, 不管里面填的啥, 都是没用的
#else
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#endif

#define UINTERFACE(...) UCLASS()

附录

命令行调用cl.exe的语法

参考:MS C/C++: The cl Command
参考:Compiler options listed by category
参考:How do I see a C/C++ source file after preprocessing in Visual Studio?
参考:/EP (Preprocess to stdout Without #line Directives)

比如说:

  • /I:寻找对应的目录,include进来
  • /external
  • FI:对加进来的头文件进行预处理(Preprocesses the specified include file.)
  • /c:不需要link
  • /Yu:使用pch文件(Uses a precompiled header file during build.)
  • /P:把预处理的output写到文件里
  • /sourceDependencies:列出所有源码级别的依赖
  • /Zc:inline:Remove unreferenced functions or data if they’re COMDAT or have internal linkage only (off by default).
  • /Gw:Enables whole-program global data optimization.
  • /Gy:Enables function-level linking.
  • /Oi[-]:Generates intrinsic functions
  • /external:I <path>: Specify location of external headers.
  • /external:W数组:Set warning level for external headers.

常见例子:

// 直接编译test.cpp,-c意味着不需要link
cl -c test.cpp

// -D后面的名字代表宏,相当于#defineVERBOSE
cl -DVERBOSE test.c

// 加个头文件路径后再编译
cl -I\myfiles\include test.c

这里的/EP貌似是执行#line这个宏,貌似是为了去除.i文件里的相关#line信息,但好像不能结合pch使用


生成UE的单独cpp的预处理结果.i文件

参考:Where can i get the preprocessing source file

步骤是:

  • 找到cpp编译过后得到的.cpp.object.response函数(貌似有的cpp的后缀是.rsp(可能是编辑下的代码?)),我的路径在F:\UnrealProjects\MyProject\Intermediate\Build\Win64\UnrealEditor\DebugGame\MyProject\MyActor.cpp.obj.response
  • 打开x64 Native Tools Command Prompt for VS2022,cd到对应引擎源码的目录,我的是D:\UE_5.1\Engine\Source,cd到这个目录是因为response文件里记录的头文件引用都是相对于这个路径的
  • 命令行执行cl.exe @F:\UnrealProjects\MyProject\Intermediate\Build\Win64\UnrealEditor\DebugGame\MyProject\MyActor.cpp.obj.response,这个@用于表示后面的是路径,会在当前cd的地方生成一个MyActor.i的预处理文件

额外注意几个Tips:

  • 打开的是x64 Native Tools Command Prompt for VS2022,我一开始打开的是Developer Command Prompt for VS 2022,就搞错了
  • 生成的文件其实放到response文件那个文件夹更好,应该改成cl.exe /P @pathA @targetPath应该就行了
  • 生成的文件貌似很多空行,可以用replace功能干掉

我后来操作的时候遇到了一个问题,编译不过,显示:

..\Intermediate\Build\Win64\x64\UnrealEditor\Debug\UnrealEd\SharedPCH.UnrealEd.ShadowErrors.EngineDefault.h.pch' precompiled header file is from a different version of the compiler, or the precompiled header is C++ and you are using it from C (or vice versa)

做了如下尝试:

  • 删除了Build\Win64\x64\UnrealEditor\Debug\UnrealEd文件夹下的所有内容,重新编译源码工程,发现要重新build,等了好久编译完,还是相同的报错
  • 删除对应代码的Binary和intermediate文件,然后发现原本的rsp文件没有了,此时需要改动对应的cpp文件,触发重新编译(要重新编译2K多个文件,应该是全部的Editor代码,不包括Runtime),但还是相同的报错,显示编译器版本不同
  • Binary和intermediate全删了,sln也删了,重新Build,妈的还是不行。。。。算了。。。

uhtmanifest文件

这个文件很长,全在一行里,我整理了下部分格式,如下所示,可以看出,它前面是一个项目的总览情况,后面是对每个Module的详细描述:

{
	"IsGameTarget":true,
	"RootLocalPath":"F:\\UnrealSource\\UnrealEngine",
	"TargetName":"MyDemoEditor",
	"ExternalDependenciesFile":"F:\\MyDemo\\Intermediate\\Build\\Win64\\MyDemoEditor\\Debug\\MyDemoEditor.deps",
	"Modules":
	[
		{
		    "Name":"CoreUObject","ModuleType":"EngineRuntime","OverrideModuleType":"None",
		    "BaseDirectory":"F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\CoreUObject",
		    "IncludeBase":"F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime",
		    "OutputDirectory":"F:\\UnrealSource\\UnrealEngine\\Engine\\Intermediate\\Build\\Win64\\UnrealEditor\\Inc\\CoreUObject",
		    "ClassesHeaders":[],
		    "PublicHeaders":["F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\CoreUObject\\Public\\UObject\\CoreNetTypes.h",
		         		     "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\CoreUObject\\Public\\UObject\\NoExportTypes.h"],
		    "InternalHeaders":[],
		    "PrivateHeaders":[],
		    "GeneratedCPPFilenameBase":"F:\\UnrealSource\\UnrealEngine\\Engine\\Intermediate\\Build\\Win64\\UnrealEditor\\Inc\\CoreUObject\\CoreUObject.gen",
		    "SaveExportedHeaders":true,"UHTGeneratedCodeVersion":"None"
		},
		{
		    "Name":"AnimationCore",
		    "ModuleType":"EngineRuntime",
			"OverrideModuleType":"None",
			"BaseDirectory":"F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore",
			"IncludeBase":"F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime",
			"OutputDirectory":"F:\\UnrealSource\\UnrealEngine\\Engine\\Intermediate\\Build\\Win64\\UnrealEditor\\Inc\\AnimationCore",
			"ClassesHeaders":[],
			"PublicHeaders":["F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\AnimationDataSource.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\CCDIK.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\CommonAnimTypes.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\Constraint.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\EulerTransform.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\TransformNoScale.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\NodeChain.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\FABRIK.h",
			                 "F:\\UnrealSource\\UnrealEngine\\Engine\\Source\\Runtime\\AnimationCore\\Public\\NodeHierarchy.h"],
			"InternalHeaders":[],
			"PrivateHeaders":[],
			"GeneratedCPPFilenameBase":"F:\\UnrealSource\\UnrealEngine\\Engine\\Intermediate\\Build\\Win64\\UnrealEditor\\Inc\\AnimationCore\\AnimationCore.gen",
			"SaveExportedHeaders":true,
			"UHTGeneratedCodeVersion":"None"
		},
		
	    ...// 其他Modules		 
    ]		
}

UPROPERTY里的Instanced Specifier

主要是说这个东西是不是该类对象独有的,举个例子,假设多个Actor都有一个Texture类型的UPROPERTY,它们都只是对这个贴图进行Sample操作,那么它就不需要加Instanced Specifier,但如果多个Actor都有各自的SkeletalMesh要进行绘制和播放动画,那么它们自然就是Instanced类型了


关于UE里的package

参考:https://docs.unrealengine.com/udk/Three/UnrealPackages.html
参考:https://zhuanlan.zhihu.com/p/152201635

The Unreal Engine concept of a package is a file which contains blobs of binary data that can be manipulated and accessed via the Unreal Engine. Content packages contain various types of game assets.

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值