您的位置:首页 > 产品设计 > UI/UE

UE4反射系统简析(含实例过程分析)

2016-10-23 15:44 507 查看

一、UE4中的反射系统


1.简述:


1.1 什么是UE4反射

在UE4里面,你无时无刻都会看到类似UFUNCTION()这样的宏。官方文档告诉你,只要在一个函数的前面加上这个宏,然后在括号里面加上BlueprintCallable就可以在编辑器里面调用了。按照他的指示,我们就能让我们的函数实现各种各样特别的功能,那这个效果就是通过UE4的反射系统来实现的。这看起来确实非常棒,不过同时给UE4的反射系统增添了一点神秘感。我们可能一开始尝试着去找一下这个宏的定义,但是翻了几层发现没有头绪,可能也就懒得再去研究他是怎么实现的了。

其实,所谓反射,是程序在运行时进行自检的一种能力,自检什么呢?我认为就是检查自己的C++类,函数,成员变量,结构体等等(对应起来也就是大家在UE4能看到的UCLASS,UFUNCTON,UPROPERTY,USTRUCT后面还会提到)。

那检查这些东西做什么呢?最明显的就是支持蓝图和C++的交互功能,说的更通俗一点,就是可以更自由的控制这些结构,让他在我们想出现的地方出现,让他在我们想使用的地方使用。要知道我们在虚幻4中声明的任意一个类,都是继承于UObject类的,所以他远远不是我们所以为的那个普通的C++类。我们可以使用这个类进行网络复制,执行垃圾回收,让他和蓝图交互等等。而这一切原生的C++是并不支持的,也正是因此虚幻4才构建了一个这样的反射系统。

1.2反射一般用于哪些地方

在UE4里面, 基本上所有的游戏工程的类都需要用到。比如,你用编辑器新建一个类,类的前面会自动添加UCLASS();新建一个结构体,需要使用USTRUCT();新建一个枚举变量,需要在前面声明UENUM();在类的里面,也必须要加上GENERATED_UCLASS_BODY()才行。

如果你想让你的变量能显示在编辑器里面,想让你的函数可以被蓝图调用或者通过让这个函数实现RPC网络通信功能,或者你想让你的变量被系统自动的回收,这些都离不开反射系统以及这些宏定义。

所以,我们这里起码能认识到,在网络通信,蓝图交互以及垃圾回收方面,这与反射系统是密不可分的。

另外,如果要说引擎中哪部分使用到反射系统功能的话,那基本上整个引擎都脱不了干系了。

2.反射系统的基本原理:

在了解反射系统前,我们必须要知道两个UE4独特的文件类型—“.generate.h”以及“.generate.cpp”。“.generate.h”文件在每一个类的声明前面都会被包含到对应的头文件里面。(这也是官方建议我们要用编辑器来创建类的原因,他们并不是常规的C++类)而“.generate.cpp”对于一整个项目只会有一个。这两种文件可以说是反射系统的关键所在,他们是通过Unreal Build Tool(UBT) 和UnrealHeaderTool(UHT)来生成的。

(补充:UnrealBuildTool(简称 UBT)是一个自定义工具,管理多个编译配置中的 虚幻引擎 4(UE4)源代码编译过程。

UnrealHeaderTool(简称 UHT)是支持 UObject 系统的自定义解析和代码生成工具。代码编译在两个阶段中进行:1.HT 被调用。它将解析 C++ 头中引擎相关类元数据,并生成自定义代码,以实现诸多 UObject 相关的功能。2.普通 C++ 编译器被调用,以便对结果进行编译。)

“.generate.h”与“.generate.cpp”文件都是做什么的?“.generate.h”与“.generate.cpp”文件里面都是什么?“.generate.h”里面是宏,而且包含一个非常庞大的宏,这个宏把所有和反射相关的方法(包括定义)和结构体连接到一起。而“.generate.cpp”里面是许多的函数定义,UnrealHeaderTool根据你在头文件里面使用的宏(UFUNCTION等)自动的生成这个文件,所以这个文件并不需要你去修改,也不允许修改。UBT属性通过扫描头文件,记录任何至少有一个反射类型的头文件的模块。如果其中任意一个头文件从上一次编译起发生了变化,那么
UHT就会被调用来利用和更新反射数据。UHT分析头文件,创建一系列反射数据,并且生成包含反射数据的C++代码(也就是“.generate.cpp”)以及各种辅助函数与thunk函数(“.generate.h”)。

这里简单举个例子,假如你有个类方法前面用宏定义(UFUNCTION(BlueprintNativeEvent))声明(BlueprintNativeEvent在ObjectBase.h中有定义,表示蓝图本地要实现的事件),你的cpp文件里面就不能去写这个函数的定义而是_
Implementation,因为UE4会认为你这个方法只想在蓝图实现,所以这个函数会在生成工具生成的过程中被定义在“.generate.cpp”里面,如果你在cpp定义就会提示编译错误(当然,因为你定义了两个一样的函数)。当你去调用这个函数的时候,首先会执行“.generate.cpp”里面的定义,之后会通过反射系统调用到蓝图。如果蓝图没有定义,那么最后会调用到GENERATED_UCLASS_BODY(后面会介绍这个宏),进而定位到.generate.h文件里面,最后执行“函数名_Implementation”,这也就是为什么要求我们在必须在.cpp声明为_
Implementation的原因。(该现象在4.9之前是经过验证的,不过最新版本的在写法上可能略有区别,等博主了解完新版本的区别后再做修改)

3.反射系统的类型层次:

按顺序依次是UField ,UStruct,UClass,UScriptStruct,UFunction,UEnum,Uproperty
UStruct是所有聚合结构体的基础类型(包含其它成员的类型,比如一个C++类、结构体、或者函数),不应该跟C++中的结构体(struct)混为一谈(那是UScriptStruct)。UClass可以包含函数、属性以及它们的子类,而UFunction和UStriptStruct只能包含属性。
你可以通过使用UTypeName::StaticClass()或者FTypeName::StaticStruct()来获取反射类型对应的UClass以及UScriptStruct,你也通过 一个UObject的实例通过Instance->GetClass()来获取类型(不能通过一个结构体实例的获取类型,因为结构体没有一个通用的基类或者需要的存储空间)。

二、反射流程实例


1.简述:

    

前面简单描述了一个UFUNCTION(BlueprintNativeEvent)的例子。这里我们以游戏中玩家使用道具的流程UFUNCTION(Reliable,Server)
ServerRequestUseItem作为例子来解析一下反射的执行过程。首先需要知道的是,宏的UFUNCTION(Reliable,Server)意义。这是RPC通信,表示这个方法在客户端调用,服务器执行。而且我们在对应.cpp文件的函数实现必须要以“函数名_Implementation”来命名。   

       “工程名.generated.cpp”的文件一般位于“Project\工程名\Intermediate\Build\Win64\ Inc\工程名\工程名.generate.cpp”。

       “工程名.generated.h”的文件一般位于“Project\工程名\Intermediate\Build\Win64\ Inc\工程名\类名.generate.h”。

2.流程描述:

UE4Editor—ProjectName.dll! APlayerController::ServerRequestUseItem_Implementation(parameter1)
UE4Editor—ProjectName.dll!APlayerController::execServerRequestUseItem_Implementation(FFrame&Stack,void*const Result)
UE4Editor—CoreUObject.dll! UFunction::Invoke(Object*Obj,FFrame&Stack,void*const Result)
UE4Editor—CoreUObject.dll! UObject::ProcessEvent(UFunction*Function,void* Parms)
UE4Editor—Engine.dll! AActor::ProcessEvent(UFunction*Function,void* Parameters)
UE4Editor—ProjectName.dll! APlayerController::ServerRequestUseItem (parameter1)
UE4Editor—ProjectName.dll! APlayer::ClickItemUI ()
……..

 

请参考上面这个代码执行栈来阅读下面的内容。UHT会给工程自动生成“工程名.generated.cpp ”的文件,里面包含了所有类的包含前置标记的的类方法,如网络通信UFUNCTION(Reliable,Server)以及C++和蓝图交互UFUNCTION(BlueprintNativeEvent)。因为我们在该类对应的.cpp文件只能声明_imlementation的函数,所以引擎在调用有这种标记的函数的时候,会执行以下的步骤。

         ①会去.generated.cpp去找函数的具体实现

         ②通过引擎工具自动生成的函数会调用位于“.generate.cpp”文件中的方法定义并执行

ProcessEvent(FindFunctionChecked(SHOOTERGAME_ServerRequestUseItem),&Parms);接下来之所以会调用voidAActor::ProcessEvent(UFunction*Function,void*Parameters)。是因为一般的charactercontroller和pawn等实际存在于游戏中的实体都是继承自Actor的,所以他可以直接调用父类的ProcessEvent方法。

        ③由于AActor调用Super::ProcessEvent(Function,Parameters);所以会执行UObject的ProcessEvent,该函数的具体实现在ScriptCore.cppclassCOREUOBJECT_APIUObject
: publicUObjectBaseUtility,Uobject的具体实现分布在几个不同的文件里面,如obj.cpp、CoreUObjectPrivate.cpp等

        ④Uboject会调用Function->Invoke(this,NewStack,(uint8*)Parms
+Function->ReturnValueOffset);NewStack是根据前面的Funtion参数创建的一个执行堆。

       ⑤在每个类的声明前我们都能看到UCLASS,这是UE4规定的格式。其实我们可以在看到对应类的.generated.cpp 文件中看到如下的宏定义。最后其实都是一些类与结构体的定义,

#undef UCLASS
#undef UINTERFACE
#if UE_BUILD_DOCS
#define UCLASS(…)
#else
#define UCLASS(…) \
APlayerController_EVENTPARMS
#endif

#define APlayerController_EVENTPARMS\
struct PlayerController_eventCheckCheatPassword_Parms\
{ \
FString Pass;
} \
……

在类的定义的前面我们还会看到GENERATED_UCLASS_BODY()。这里的宏我们进一步展开,

#undef GENERATED_UCLASS_BODY
#undef GENERATED_INTERFACE_BODY
#define GENERATED_UCLASS_BODY() \
public: \
APlayerController_RPC_WRAPPERS\
APlayerController_CALLBACK_WRAPPERS\
APlayerController_INCLASS \

DECLARE_FUCTION(execServerRequestUseItem)\
{
P_GET_OBJECT(UComponent,itemComp); \
P_GET_STRUCT(struct FItemID,itemID); \
P_FINISH; \
this->ServerRequestUseItem_Implmentation(itemComp);
}

我们最后发现这个execServerRequestInventoryUseItem其实就是在GENERATED_UCLASS_BODY()定义的一个函数,在这里我们找到了ServerRequestUseItem_Implementation找到了也就是在.cpp文件服务器最终执行的函数。

3.难点简述:

    

在上面流程的第三步和第四步,涉及到了ProcessEvent和Invoke两个函数。这里面的机制比较复杂,整体来说是通过新建的结构体和代理来保存和传递函数指针。ProcessEvent会传递函数的名称以及参数,函数的地址与名称通过类UFuntion来保存,参数这回通过定义一个新的类来保存。然后将函数的名称与参数类的地址传递。要注意的是,在执行ProcessEvent(FindFuntionChecked(SHOOTERGAME_ServerRequestUseItem),
&Params)的时候,FindFuntionChecked执行后所返回的UFuntion里包含一个成员函数指针,这个指针指向的就是execServerRequestUseItem。这些内容都是在UE4的编译工具编译时所产生的。

4.综述:

      

这个文档只是简单的从一个反射方法的实现流程上做了分析,而UE4的反射系统还是非常复杂和强大的。这里面涉及到宏的使用,蓝图系统,编译工具,成员函数指针以及一些特别的存储类,想完全理解这些可能需要非常长的时间,也确实比较难。我们平时写逻辑时也许根部不需要了解,但是对于一个程序员来说,理解总比不懂强,也许会给我们以后的设计增加一些灵感,有助于我们进一步理解UE4,以后如果有更深的理解会进一步完善这个博客的。

参考文档链接:https://www.unrealengine.com/blog/unreal-property-system-reflection

参考文档翻译链接:http://www.cnblogs.com/ghl_carmack/p/5698438.html#

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息