【UE·蓝图底层篇】一文搞懂NativeClass、GeneratedClass、BlueprintClass、ParentClass

本文将对蓝图类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从命名和使用方式来看,很容易让我们误解是蓝图的父类。这是完全错误的。ParentClass是蓝图要编辑的对象的父类。这需要知道蓝图的本质才能理解这句话。我后面要讲的GenerateClass的时候会说的更细。现在只需要知道蓝图,以及我们通过蓝图编辑的对象是两个东西。比如一个Actor,我们游戏运行时其实不需要Actor的蓝图,而只需要Actor的信息。
下面进行代码测试,首先用LoadObject加载蓝图UMG_Child,注意这里路径没有_C,分别输出ParentClass和蓝图类的SuperClass信息:

UObject* Obj = LoadObject<UObject>(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child'"));
	
if(UBlueprint* BpObj = Cast<UBlueprint>(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会隐藏首字母)。
在这里插入图片描述


Blueprint和GeneratedClass 游戏修改器和存档

首先捋一下蓝图的几个类,WidgetBlueprint、BaseWidgetBlueprint、Blueprint、BlueprintCore

  • WidgetBlueprint UMG蓝图类
  • BaseWidgetBlueprint UMG蓝图基类
  • 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<UObject>(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child_C'"));
UObject* ParentObj = LoadObject<UObject>(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<UObject>(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child'"));
if(UBlueprint* BpObj = Cast<UBlueprint>(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。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呢?答案在我提供的工程源码里有,这里留给大家自己解决。


BlueprintClass

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()的代码:
在这里插入图片描述
在这里插入图片描述

再看看UBlueprintGeneratedClassUWidgetBlueprintGeneratedClass的定义:
在这里插入图片描述
在这里插入图片描述

所以蓝图的GetBlueprintClass返回的是它对应的特定GeneratedClass的StaticClass。我们之前讲的GeneratedClass(就是xxx_C那种)它是一种UClass,而它同时也是UWidgetBlueprintGeneratedClass。关系图如下:
在这里插入图片描述

注意区分Generate的SuperClass和它的父类是不一样的,文末会展开讲
继续写测试代码:

//加载蓝图,省略
...
if (auto BpGen = BpObj->GeneratedClass)
{
	if (Cast<UWidgetBlueprintGeneratedClass>(BpGen))
	{
		FString ObjName = BpGen->GetFName().ToString();
		UE_LOG(LogTemp, Warning, TEXT("GenClassName %s"), *ObjName);
	}
	//蓝图的GeneratedClass的父类
	UClass* BpGenSuperClass = BpGen->GetSuperClass();
	if (Cast<UWidgetBlueprintGeneratedClass>(BpGenSuperClass))
	{
		FString GenSuperClassName = BpGenSuperClass->GetFName().ToString();
		UE_LOG(LogTemp, Warning, TEXT("GenSuperClassName %s"), *GenSuperClassName);
	}
	//蓝图的GeneratedClass的父类的父类
	UClass* BpGenSSClass = BpGen->GetSuperClass()->GetSuperClass();
	if (Cast<UWidgetBlueprintGeneratedClass>(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 游戏角色和游戏存档

运行时对象和GeneratedClass的关系就像游戏角色和游戏存档的关系。还是之前类似的比喻,这次把UE4运行起来的世界看成是一个没有存档功能,但是可以多人在线的破网游。由某个技术大佬已经用修改器改了一份牛逼存档出来(指用蓝图修改了GeneratedClass)。然后每个玩家用这个存档进入游戏游玩自己的游戏角色(指生成多个运行时对象,每个运行时对象的UClass相同,且和GeneratedClass都是同一个)。虽然说在游戏里面可以打怪赚钱升级让角色属性变成不一样(指运行时修改类变量),但是退出游戏以后每个人的存档都会和一开始拿到的一样(指蓝图里的GeneratedClass信息不会被改变)。

为了区分修改器,存档,游戏角色名称方便,同时减少命名的麻烦,约定俗成了一套命名方案:

  • 修改器由技术大佬自定义名称(指开发者给蓝图起名UMG_Child
  • 游戏存档名称由修改器后面加上_C后缀(UMG_Child_C)
  • 游戏中的角色名称由游戏存档名称加上数字(UMG_Child_C_5)

它们的关系图如下:
在这里插入图片描述


GeneratedClass 即是UClass也是UObject

在上一篇文章开头,我就跟大家说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<UObject>(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Child.UMG_Child_C'"));
UObject* ParentObj = LoadObject<UObject>(nullptr,TEXT("WidgetBlueprint'/Game/UMG/UMG_Parent.UMG_Parent_C'"));
UObject* ActorObj = LoadObject<UObject>(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 GetSuperClass和它的父类

这也是容易混淆的概念之一。首先GeneratedClass是UClass类型,UClass里有个方法叫GetSuperClass()。这个方法返回的结果是UClass所记录的类信息的父类,GeneratedClass所记录的类就是运行时在游戏世界里的实例对象。在这个案例中指的就是我们一开始创建的那个UMG的复制体。所以GetParentNativeClass最后一定能找到MyUserWidget这个类的UClass。而且这个UClass必定等于MyUserWidget::StaticClass()
GeneratedClass的父类就很好理解了。GeneratedClass是UClass,UClass自己也有一堆派生类的。其中一个派生类WidgetBlueprintGeneratedClass,UMG蓝图的GeneratedClass的父类都是WidgetBlueprintGeneratedClass这个类型。前面对GeneratedClass进行类型转换也证实了这个结论。
在这里插入图片描述


总结

这篇文章到这里就结束了,不知不觉写了这么多。内容量确实很多,大概是上一篇文章的两倍了。其实像这种讲底层的文章网络上也不少,不过很难看得下去。一个是概念上很抽象,一个是没有写代码进行测试。所以这篇文章我在写的过程中尽量使用一些比喻来让大家容易理解,同时带大家写一些代码进行测试。在之后的文章我还会使用这篇文章提到的知识开发一个实用蓝图功能。


学习资料

深入Unreal蓝图开发:理解蓝图技术架构
UE4蓝图解析(一)


关于作者

  • 水曜日鸡,喜欢ACG的游戏程序员。曾参与索尼中国之星项目《硬核机甲》的开发。 目前在某大厂做UE4项目。

CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
游戏同行聊天群:891809847

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript中的类(class)和Java中的类有一些相似之处,但也存在一些区别。 1. 语法差异:在Java中,类的定义是使用关键字"class",后跟类名。而在JavaScript中,类的定义是通过关键字"class",后跟类名和一个类体(包含类的属性和方法)。 Java示例: ```java public class MyClass { // 类的属性 // 类的方法 } ``` JavaScript示例: ```javascript class MyClass { // 类的属性 // 类的方法 } ``` 2. 继承机制:Java中的类可以通过继承来扩展现有类的功能,使用关键字"extends"。而JavaScript中的类也支持继承,但使用的是关键字"extends"和"super"。 Java示例: ```java public class ChildClass extends ParentClass { // 子类的属性 // 子类的方法 } ``` JavaScript示例: ```javascript class ChildClass extends ParentClass { constructor() { super(); // 调用父类的构造函数 // 子类的属性 } // 子类的方法 } ``` 3. 类成员:Java中的类成员(属性和方法)可以使用访问修饰符(例如public、private、protected)来控制其可见性。而JavaScript中的类成员默认是公开的,可以使用关键字"public"、"private"、"protected"来控制可见性。 Java示例: ```java public class MyClass { private int myPrivateVar; // 私有属性 public void myPublicMethod() { // 公共方法 } } ``` JavaScript示例: ```javascript class MyClass { #myPrivateVar; // 私有属性 myPublicMethod() { // 公共方法 } } ``` 这些是JavaScript类和Java类之间的一些主要差异。虽然它们有共同之处,但也有一些不同之处,因此在使用时需要注意。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值