本文将对蓝图类UBlueprint的几个UClass成员变量NativeClass、GeneratedClass、BlueprintClass、ParentClass进行比较深入的讲解,看完之后对蓝图会有全新的理解。本篇的内容很多很干货,同时学习起来有一定难度,建议看完我之前写的对UClass有关概念的讲解:
【UE·底层篇】一文搞懂StaticClass、GetClass和ClassDefaultObject
github工程地址
首先先自定义一个UserWidget(MyUserWidget),然后新建两个UMG蓝图,第一个的ParentClass选为MyUserWidget,名称叫UMG_Parent。第二个蓝图名称为UMG_Child,ParentClass选为UMG_Parent。然后在关卡蓝图里生成这个UMG,后面的逻辑我们将在MyUserWidget的NativeConstruct函数里进行编写。
ParentClass是我们经常需要在蓝图操作的参数,可以说再熟悉不过了。那么,它是父类吗?是。是蓝图的父类吗?不是。
ParentClass从命名和使用方式来看,很容易让我们误解是蓝图的父类。这是完全错误的。ParentClass是蓝图要编辑的对象的父类。这需要知道蓝图的本质才能理解这句话。我后面要讲的GenerateClass的时候会说的更细。现在只需要知道蓝图,以及我们通过蓝图编辑的对象是两个东西。比如一个Actor,我们游戏运行时其实不需要Actor的蓝图,而只需要Actor的信息。
下面进行代码测试,首先用LoadObject加载蓝图UMG_Child,注意这里路径没有_C,分别输出ParentClass和蓝图类的SuperClass信息:
UObject* Obj = LoadObject(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child'"));
if(UBlueprint* BpObj = Cast(Obj))
{
if (auto BpParent = BpObj->ParentClass)
{
FString ObjName = BpParent->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("ParentName %s"), *ObjName);
}
if (auto BpSuper = BpObj->GetClass()->GetSuperClass())
{
FString ObjName = BpSuper->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("SuperClassName %s"), *ObjName);
}
}
输出结果:
LogTemp: Warning: ParentName UMG_Parent_C
LogTemp: Warning: SuperClassName BaseWidgetBlueprint
ParentClass结果就是我们在蓝图编辑器里选的类,多了一个_C(原因也是后面会讲)。SuperClass结果是BaseWidgetBlueprint,这个才是蓝图的父类。因为我们测试用的是UMG,UMG对应的蓝图类是UWidgetBlueprint,而它的父类是UBaseWidgetBlueprint,所以输出结果就是BaseWidgetBlueprint(UE4会隐藏首字母)。
首先捋一下蓝图的几个类,WidgetBlueprint、BaseWidgetBlueprint、Blueprint、BlueprintCore。
关系如下,其中蓝图类会被派生成各种特定类型的蓝图类,除了UMG蓝图还有动画蓝图等等:
GeneratedClass(蓝图生成类)是BlueprintCore蓝图核心类的一个成员变量,它是一个TSubclassOf类型,TSubclassOf是UClass的模板化。TSubclassOf表示这是T类型的UClass而不是其他类型。
现在回到前面的问题:
蓝图的本质是什么?蓝图的本质是类的编辑器。
哪个类的编辑器?GeneratedClass。
蓝图和GeneratedClass就好像修改器和存档的关系,如果我们把UE4运行起来的世界看做是一个没有实时存档功能的上古游戏,那么,我们需要游戏存档(GeneratedClass)我们才能游玩,而我们要修改存档只能使用特定的修改器(特定类型的Blueprint)。而游戏运行起来后完全不需要修改器,只需要游戏存档。每次当我们点击蓝图的Compile编译按钮时,相当于我们使用修改器编辑存档后点击保存,只有点击保存了我们的修改才有效。
那么还有一个小问题:
_C后缀到底是个什么玩意?没加_C是蓝图,加了_C是GeneratedClass。
说了这么多,
GeneratedClass在哪里?让我康康!
让我们回到UE4资源浏览器界面,我们保存文件时,这里的uasset实际上序列化了蓝图的信息,蓝图里的GeneratedClass也会一起序列化,也就是说虽然看上去只有一个文件,实际上有两个东西。而通过路径上加上_C后缀我们可以只加载GenerateClass而不是蓝图。
接下来是代码测试环节,加载UMG_Child_C和UMG_Parent_C并打印:
UObject* ChildObj = LoadObject(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child_C'"));
UObject* ParentObj = LoadObject(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Parent.UMG_Parent_C'"));
if (ChildObj && ParentObj)
{
FString ChildObjName = ChildObj->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("ChildObjName %s"), *ChildObjName);
FString ParentObjName = ParentObj->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("ParentObjName %s"), *ParentObjName);
}
输出结果:
LogTemp: Warning: ChildObjName UMG_Child_C
LogTemp: Warning: ParentObjName UMG_Parent_C
现在我们已经知道_C和GeneratedClass是什么了,让我们回到上一节ParentClass的内容。之前我们讲ParentClass是蓝图要编辑的类的父类,那么也就是说,它是蓝图的GeneratedClass的父类。为了验证这个结论,继续写代码测试,依然是加载蓝图UMG_Child(注意没有_C):
UObject* Obj = LoadObject(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child'"));
if(UBlueprint* BpObj = Cast(Obj))
{
//蓝图的ParentClass
if (auto BpParent = BpObj->ParentClass)
{
FString ObjName = BpParent->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("ParentName %s"), *ObjName);
}
//蓝图的GeneratedClass
if (auto BpGen = BpObj->GeneratedClass)
{
FString ObjName = BpGen->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("GenClassName %s"), *ObjName);
//蓝图的GeneratedClass的父类
FString GenSuperClassName = BpGen->GetSuperClass()->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("GenSuperClassName %s"), *GenSuperClassName);
}
}
输出结果:
LogTemp: Warning: ParentName UMG_Parent_C
LogTemp: Warning: GenClassName UMG_Child_C
LogTemp: Warning: GenSuperClassName UMG_Parent_C
完美符合猜想,即ParentClass是GeneratedClass的父类。
想必看了前面那么多内容,有些读者已经晕了,所以这里讲一个比较简单的概念NativeClass。NativeClass并不是一个变量,也不是一种类型,而是一种概念。每一个UClass里面都有一个函数IsNative()判断这个UClass是不是Native,如果是我们通常叫这个UClass为NativeClass。
那么,什么情况下Native是True?
简单来说,它是C++类生成的UClass就是NativeClass,是蓝图类生成的UClass就不是NativeClass(比如上面提到的GeneratedClass)。
NativeClass就像纯正血统的中国人,在C++的世界里怎么生孩子都是纯正的中国人。但是如果跑到国外去了,和蓝图这个外地人生孩子了,他就是混血儿了(指新建一个蓝图,ParentClass选择为C++类),而这个混血儿又和外国人生孩子(指新建一个蓝图,ParentClass选择为上一个蓝图)那肯定不是纯正血统的中国人了(指不是NativeClass)。
在UObject里有一个方法GetParentNativeClass可以帮我们找到这个混血儿祖上哪一代父母都是纯正的中国人。继续写代码测试:
//前面依旧是加载蓝图,省略
...
if (auto BpGenNative = GetParentNativeClass(BpObj->GeneratedClass))
{
FString ObjName = BpGenNative->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("ParentGenNativeName %s"), *ObjName);
}
输出结果:
LogTemp: Warning: ParentGenNativeName MyUserWidget
也就是说我们可以通过GetParentNativeClass来找到蓝图的GeneratedClass一开始是由哪个C++类派生的。那么这里再留个小作业,对于Blueprint蓝图来说,它是不是NativeClass呢?答案在我提供的工程源码里有,这里留给大家自己解决。
Blueprint蓝图类还有一个函数GetBlueprintClass这里也一起提一下。这次我们先写代码测试再去研究它:
if (auto BpClass = BpObj->GetBlueprintClass())
{
FString ObjName = BpClass->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("BpClassName %s"), *ObjName);
}
输出结果:
LogTemp: Warning: BpClassName WidgetBlueprintGeneratedClass
WidgetBlueprintGeneratedClass又是个什么玩意?首先F12看看UBlueprint::GetBlueprintClass()和UWidgetBlueprint::GetBlueprintClass()的代码:
再看看UBlueprintGeneratedClass和UWidgetBlueprintGeneratedClass的定义:
所以蓝图的GetBlueprintClass返回的是它对应的特定GeneratedClass的StaticClass。我们之前讲的GeneratedClass(就是xxx_C那种)它是一种UClass,而它同时也是UWidgetBlueprintGeneratedClass。关系图如下:
(注意区分Generate的SuperClass和它的父类是不一样的,文末会展开讲)
继续写测试代码:
//加载蓝图,省略
...
if (auto BpGen = BpObj->GeneratedClass)
{
if (Cast(BpGen))
{
FString ObjName = BpGen->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("GenClassName %s"), *ObjName);
}
//蓝图的GeneratedClass的父类
UClass* BpGenSuperClass = BpGen->GetSuperClass();
if (Cast(BpGenSuperClass))
{
FString GenSuperClassName = BpGenSuperClass->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("GenSuperClassName %s"), *GenSuperClassName);
}
//蓝图的GeneratedClass的父类的父类
UClass* BpGenSSClass = BpGen->GetSuperClass()->GetSuperClass();
if (Cast(BpGenSSClass) == nullptr)
{
FString GenSSClassName = BpGenSSClass->GetFName().ToString();
UE_LOG(LogTemp, Warning, TEXT("GenSSClassName %s"), *GenSSClassName);
}
}
输出结果:
LogTemp: Warning: GenClassName UMG_Child_C
LogTemp: Warning: GenSuperClassName UMG_Parent_C
LogTemp: Warning: GenSSClassName MyUserWidget
可以看到,只有最后一种MyUserWidget我们不能转化为UWidgetBlueprintGeneratedClass。
运行时对象和GeneratedClass的关系就像游戏角色和游戏存档的关系。还是之前类似的比喻,这次把UE4运行起来的世界看成是一个没有存档功能,但是可以多人在线的破网游。由某个技术大佬已经用修改器改了一份牛逼存档出来(指用蓝图修改了GeneratedClass)。然后每个玩家用这个存档进入游戏游玩自己的游戏角色(指生成多个运行时对象,每个运行时对象的UClass相同,且和GeneratedClass都是同一个)。虽然说在游戏里面可以打怪赚钱升级让角色属性变成不一样(指运行时修改类变量),但是退出游戏以后每个人的存档都会和一开始拿到的一样(指蓝图里的GeneratedClass信息不会被改变)。
为了区分修改器,存档,游戏角色名称方便,同时减少命名的麻烦,约定俗成了一套命名方案:
在上一篇文章开头,我就跟大家说UClass就是C#的Type。其实这句话并不完全准确,这句话只体现了UClass记录类信息的功能,没有体现出UClass也是一个对象的事实。所以上一篇文章的后面我无法理解GetClass多次调用的结果。
游戏运行中的对象有什么特点呢?当然有很多特点,我这里只提一条:由同一个UClass生成的对象,对这些对象调用GetClass它都是相等的。不管是上一篇的BP_Actor还是这一篇的UMG_Child。只要你用同一个蓝图在场景里生成多个对象,它们调用GetClass一定是相等的。
那么如果是UClass的情况呢?回顾一下上一篇文章的类图:
UClass也是UObject派生的,所以对于上面的特性是通用的。即由同一个UClass生成的对象,对这些对象调用GetClass它都是相等的。那么,在我们这一篇案例里,UMG_Parent_C和UMG_Child_C这两个蓝图GeneratedClass都是由WidgetBlueprintGeneratedClass这个UClass生成的。所以对这两个对象调用GetClass一定是相等的。而WidgetBlueprintGeneratedClass也是UClass生成的,它和其他由UClass生成的BlueprintGeneratedClass都调用GetClass也一定是相同的。
测试代码如下:
//加载UMG蓝图和普通蓝图
UObject* ChildObj = LoadObject(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child_C'"));
UObject* ParentObj = LoadObject(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Parent.UMG_Parent_C'"));
UObject* ActorObj = LoadObject(nullptr,TEXT("Blueprint'/Game/BP/BP_MyActor.BP_MyActor_C'"));
if (this->GetClass() == ChildObj)
{
UE_LOG(LogTemp, Warning, TEXT("运行时的UClass和蓝图的GeneratedClass相同"),);
}
//不相同
if(ChildObj != ParentObj)
{
UE_LOG(LogTemp, Warning, TEXT("Child和Parent不同"),);
}
//相同
if(ChildObj->GetClass() == ParentObj->GetClass())
{
UE_LOG(LogTemp, Warning, TEXT("Child的UClass和Parent的UClass相同"),);
}
//相同
if(ChildObj->GetClass()->GetClass() == ActorObj->GetClass()->GetClass())
{
UE_LOG(LogTemp, Warning, TEXT("Child的UClass的UClass和Actor的UClass的UClass相同"),);
}
这也是容易混淆的概念之一。首先GeneratedClass是UClass类型,UClass里有个方法叫GetSuperClass()。这个方法返回的结果是UClass所记录的类信息的父类,GeneratedClass所记录的类就是运行时在游戏世界里的实例对象。在这个案例中指的就是我们一开始创建的那个UMG的复制体。所以GetParentNativeClass最后一定能找到MyUserWidget这个类的UClass。而且这个UClass必定等于MyUserWidget::StaticClass()。
GeneratedClass的父类就很好理解了。GeneratedClass是UClass,UClass自己也有一堆派生类的。其中一个派生类WidgetBlueprintGeneratedClass,UMG蓝图的GeneratedClass的父类都是WidgetBlueprintGeneratedClass这个类型。前面对GeneratedClass进行类型转换也证实了这个结论。
这篇文章到这里就结束了,不知不觉写了这么多。内容量确实很多,大概是上一篇文章的两倍了。其实像这种讲底层的文章网络上也不少,不过很难看得下去。一个是概念上很抽象,一个是没有写代码进行测试。所以这篇文章我在写的过程中尽量使用一些比喻来让大家容易理解,同时带大家写一些代码进行测试。在之后的文章我还会使用这篇文章提到的知识开发一个实用蓝图功能。
深入Unreal蓝图开发:理解蓝图技术架构
UE4蓝图解析(一)
关于作者
CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
游戏同行聊天群:891809847