UE4 泛型蓝图节点的实现及应用实例
1. Overview
本教程旨在讲解使用ue4 UFUNCTION说明符(Specifier)自定义泛型蓝图节点,实现UObject的任意类型属性(UProperty)GET/SET方法。如下四个泛型蓝图节点,依据UObject对象的PropertyName,GET/SET属性值,适用于v4.25之前版本。
2. Introduction
泛型蓝图节点也就是带有可变参数的蓝图节点,通用指蓝图系统中带有通配符(wildcard)类型参数的蓝图节点。实现泛型蓝图节点至少有二种方式,其一,继承UK2Node类,并根据需要实现其派生类,如常用的Cast节点,Set By-Ref Var节点等都属于此类;其二,使用UFUNCTION中CustomThunk说明符以及相应的类型说明符标识wildcard参数,并为该蓝图函数自定义DECLARE_FUNCTION()函数体,如Utilities|Array,Utilities|Map,Utilities|Set 目录下,三种容器Array、Map、Set各类操作的蓝图节点都属于此类。此外ue4内置的蓝图节点还存在着将二者结合的蓝图节点GetDataTableRow,UK2Node_GetDataTableRow类(基类UK2Node)在Editor状态下动态更新Pin,UDataTableFunctionLibrary类中GetDataTableRowFromName函数在Runtime状态下,执行具体的GetDataTableRowFromName方法,将DataTable中的RowStruct拷贝到OutRowStruct。以上二种方式各有其优缺点,本次讲解采用代码量少、实现难度低的第二种方式。
3. Required Knowledge
-
熟悉UE4 C++ UFUNCTION宏常用关键字,
-
自定义execFunction函数体;(预计下一讲)
4. Why
第1个问题:为什么要实现这样一类节点?
目的:(1)提升自身的编程水平,加深个人对于泛型蓝图节点的理解;(2)减少蓝图之间的引用,提升蓝图执行性能。
如下图所示,从BP_Actor蓝图(基类Actor)中获取ThirdPersonCharacter蓝图的Health属性,左边是一种常见的方式(使用Cast
节点),右边则是使用一个自定义实现的泛型蓝图节点GET/SET节点。二种获取Character属性的方法对BP_Actor的影响却显著不同,使用Cast
节点增加了BP_Actor的引用,使得SizeMap暴增,显著影响性能(附:把属性(Property)定义在c++基类,Cast to c++类,也可以获得到蓝图的该属性,同时也可以使二个蓝图的引用断开)。
那么断开二个蓝图的引用有什么好处?ue4 中c++和蓝图除了广为人知的效率差异,另外一个重要区别在于内存和加载(Memory and Loading)方式差异,蓝图引用增多可能导致内存消耗变大以及加载时间变长(详情见虚幻引擎官方视频,[FestEurope2019] 蓝图深入探讨 | Blueprints In-depth,因此减少蓝图之间相互引用对于提升性能有着关键的作用。
回到主题,在ue4中实现泛型蓝图节点,获取UObject的任意属性。如下图示例,以获取UProperty属性为例,根据变量名PropertyName获取Target(ActorBP)获取属性Value,并将获取到的属性值保存在This变量中,获取不同类型(bool、Byte、integer、float、String、Name、Vector、Rotator、Transform、任意UStruct)的属性都是使用同一个节点。
LogBlueprintUserMessages: [BP_Actor_2] Tooltip -> Before GET ,This Bool-> false ,This Byte-> 0 ,This Integer-> 0 ,This Float-> 0.0 ,This String-> ,This Transform-> Translation: X=0.000 Y=0.000 Z=0.000 Rotation: P=0.000000 Y=0.000000 R=0.000000 Scale X=1.000 Y=1.000 Z=1.000 ,This Struct-> {"B": 0, "G": 0, "R": 0, "A": 0}
LogBlueprintUserMessages: [BP_Actor_2] Tooltip -> After GET ,This Bool-> true ,This Byte-> 10 ,This Integer-> 10 ,This Float-> 15.0 ,This String-> Unreal Engine ,This Transform-> Translation: X=70.000 Y=80.000 Z=90.000 Rotation: P=69.999931 Y=120.000008 R=-79.999908 Scale X=0.100 Y=0.200 Z=0.300 ,This Struct-> {"B": 156, "G": 253, "R": 255, "A": 1}
第2个问题:为什么能实现一个这样的节点?
UObject对象支持根据属性名(PropertyName)查找属性(UProperty),然后可以取得PropertyValue。因此基于PropertyName获取UObject PropertyValue是理论可行的,并且ue4源码KismetSystemLibray
类中已经向我们展示一些UObject对象不同类型(int32 / uint8 / float / bool等等)属性的Set Property by name
函数的实现方式,但是这些函数仅供蓝图内部使用。
// --- 'Set property by name' functions ------------------------------
/** Set an int32 property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static void SetIntPropertyByName(UObject* Object, FName PropertyName, int32 Value);
/** Set an int64 property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static void SetInt64PropertyByName(UObject* Object, FName PropertyName, int64 Value);
/** Set an uint8 or enum property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static void SetBytePropertyByName(UObject* Object, FName PropertyName, uint8 Value);
/** Set a float property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static void SetFloatPropertyByName(UObject* Object, FName PropertyName, float Value);
/** Set a bool property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static void SetBoolPropertyByName(UObject* Object, FName PropertyName, bool Value);
/** Set an OBJECT property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true"))
static void SetObjectPropertyByName(UObject* Object, FName PropertyName, UObject* Value);
/** Set a CLASS property by name */
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
static void SetClassPropertyByName(UObject* Object, FName PropertyName, TSubclassOf<UObject> Value);
/** Set an INTERFACE property by name */
UFUNCTION(BlueprintCallable, Category = "Collision", meta = (BlueprintInternalUseOnly = "true"))
static void SetInterfacePropertyByName(UObject* Object, FName PropertyName, const FScriptInterface& Value);
/** Set a NAME property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetNamePropertyByName(UObject* Object, FName PropertyName, const FName& Value);
/** Set a SOFTOBJECT property by name */
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value"))
static void SetSoftObjectPropertyByName(UObject* Object, FName PropertyName, const TSoftObjectPtr<UObject>& Value);
/** Set a SOFTCLASS property by name */
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value"))
static void SetSoftClassPropertyByName(UObject* Object, FName PropertyName, const TSoftClassPtr<UObject>& Value);
/** Set a STRING property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetStringPropertyByName(UObject* Object, FName PropertyName, const FString& Value);
/** Set a TEXT property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetTextPropertyByName(UObject* Object, FName PropertyName, const FText& Value);
/** Set a VECTOR property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetVectorPropertyByName(UObject* Object, FName PropertyName, const FVector& Value);
/** Set a ROTATOR property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetRotatorPropertyByName(UObject* Object, FName PropertyName, const FRotator& Value);
/** Set a LINEAR COLOR property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetLinearColorPropertyByName(UObject* Object, FName PropertyName, const FLinearColor& Value);
/** Set a TRANSFORM property by name */
UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true", AutoCreateRefTerm = "Value" ))
static void SetTransformPropertyByName(UObject* Object, FName PropertyName, const FTransform& Value);
每一种类型Property都有二个基本属性,PropertyAddress(void*)
和Property Size。而GET方法的本质上可以描述为:在获取UObject的属性(Property)后,将该属性所指向内存区域的值拷贝到返回值变量的内存区域,即返回值变量获取到Object->Property Value;SET方法则是逆过程,同样在找到Object的属性后,将输入值变量所指向内存区域的值拷贝到Object的相应属性指向的内存区域。
不同类型的Property除了内存地址不一样,所占用的内存空间大小也不一样,只有保证内存空间一样,也就是同一类属性才可以相互复制而不至于引起程序崩溃。对于派生于UProperty类的类型,如UBoolProerty / UEnumProperty / UNumericProperty / UStructProperty等都可以直接用UProperty*指示Property占用内存空间的大小。派生于TProperty类的类型,如UArrayProperty / UMapProperty / USetProperty, 则需要分别使用UArrayProperty* / UMapProperty* / USetProperty*
来表示内存空间大小。
仿照源代码KismetSystemLibray类中Set Property by Name
函数,明显可以得出我们的泛型蓝图节点的引脚(PIN)参数应该按如下设计:
GET节点:
输入参数:UObjct* Object
输入参数:FName PropertyName
输出参数:void* Value
SET节点:
输入参数:UObjct* Object
输入参数:FName PropertyName
输入参数:void* Value
进一步分析以上设计存在二个问题:
其一: GET方法,仅传入Object和PropertyName,在编译阶段传入的Object Class可能并没有名为PropertyName的属性,故而无法确定返回值的类型,也就没法正确编译。因此需要在输入参数列表中添加一个指示返回值类型的输入参数。为了使用的方便,不妨直接将这个参数表示成void*类型,那么该参数不仅可以标识返回值类型,而且还可用来存储返回结果。综上:修改后的GET方法和SET方法形式上将会完全相同。
其二, 蓝图函数(BlueprintCallable),不支持void类型参数
如下示例代码:
UFUNCTION(BlueprintCallable, Category = "MyProject")
void FunctionName(void* Variable);
编译报错
error : Unrecognized type 'void' - type must be a UCLASS, USTRUCT or UENUM
解决方法:
UFUNCTION(BlueprintCallable, CustomThunk, meta = (CustomStructureParam = "Variable", AutoCreateRefTerm = "Variable"), Category = "MyProject")
void FunctionName(const int32& Variable);
DECLARE_FUNCTION(execFunctionName)
{
}
修改后的方法:
其中CustomThunk的作用是指示UHT不需要为蓝图函数生成exec方法,使用自定义的execFunction,如上所示的
DECLARE_FUNCTION(execFunctionName)
{
}
其中,CustomStructureParam = "Variable"
标识变量Variable为wildcard
类型参数,“const int32& Variable”
并非表示Variable变量类型为const int32,该参数在此处相当于一个占位符,const + & 指示该参数为输入参数;当然将int32替换成其他类型,也可以编译成功,此处为了与源代码保持一致。编译完成之后,可以在蓝图系统中找到如下节点:
5. Approach
以UBlueprintFunctionLibrary为基类,创建C++类,头文件(.h)
(附:为了减少文章中的显示长度(知乎代码块不支持折叠),以下仅列举其中一个蓝图节点实现代码,完整代码见:https://github.com/xusjtuer/NoteUE4)
//(1)声明泛型函数,在蓝图系统中产生了一个带Wildcard参数的蓝图节点(.h文件):
/**
* Get or Set object PROPERTY value by property name
*
* @param Object, object that owns this PROPERTY
* @param PropertyName, property name
* @param Value(return), save returned object property(Get Operation) as well as indicate property type
* @param bSetter, If true, write Value to object property(Set operation). Otherwise, read object property and assign it to Value(Get operation)
*/
UFUNCTION(BlueprintCallable, CustomThunk, Category = "Utilities|Variables", meta = (CustomStructureParam = "Value", AutoCreateRefTerm = "Value", DisplayName = "GET/SET (Property)", CompactNodeTitle = "GET/SET"))
static void AccessPropertyByName(UObject* Object, FName PropertyName, const int32& Value, bool bSetter = true);
// (2)自定义CustomThunk 函数体,实现从蓝图VM中获取输入参数值(.h文件)
DECLARE_FUNCTION(execAccessPropertyByName)
{
P_GET_OBJECT(UObject, OwnerObject);
P_GET_PROPERTY(UNameProperty, PropertyName);
Stack.StepCompiledIn<UStructProperty>(NULL);
void* SrcPropertyAddr = Stack.MostRecentPropertyAddress;
/// Reference: Plugins\Experimental\StructBox\Source\StructBox\Classes\StructBoxLibrary.h -> execSetStructInBox
UProperty* SrcProperty = Cast<UProperty>(Stack.MostRecentProperty);
P_GET_UBOOL(bSetter);
P_FINISH;
P_NATIVE_BEGIN;
Generic_AccessPropertyByName(OwnerObject, PropertyName, SrcPropertyAddr, SrcProperty, bSetter);
P_NATIVE_END;
}
// (3)声明实际执行GET/SET过程的函数(.h文件)
public: // Generic function
// Get or set a UPROPERTY of UObject by property name
static void Generic_AccessPropertyByName(UObject* OwnerObject, FName PropertyName, void* SrcPropertyAddr, UProperty* SrcProperty, bool bSetter = true);
// (4)实现Generic函数(.cpp文件)
void UParserPropertyLibrary::Generic_AccessPropertyByName(UObject* OwnerObject, FName PropertyName, void* SrcPropertyAddr, UProperty* SrcProperty, bool bSetter /*= true*/)
{
if (OwnerObject != NULL)
{
UProperty* FoundProp = FindField<UProperty>(OwnerObject->GetClass(), PropertyName);
if ((FoundProp != NULL) && (FoundProp->SameType(SrcProperty)))
{
void* Dest = FoundProp->ContainerPtrToValuePtr<void>(OwnerObject);
if (bSetter == true)
{
FoundProp->CopySingleValue(Dest, SrcPropertyAddr);
}
else
{
FoundProp->CopySingleValue(SrcPropertyAddr, Dest);
}
return;
}
}
UE_LOG(LogParserPropertyLibrary, Warning, TEXT("UParserPropertyLibrary::Generic_AccessPropertyByName: Failed to find %s variable from %s object"), *PropertyName.ToString(), *UKismetSystemLibrary::GetDisplayName(OwnerObject));
}
6. Conclusion
本文主要介绍了自定义泛型蓝图节点的实现以及它的应用实例,实现了对UObject对象的任意属性的GET/SET方法,依据PropertyName,GET/SET UObject对象的属性UProperty。拓展:(1)将bSetter
参数替换成操作符枚举,可以进一步扩充该泛型蓝图节点的应用,感兴趣的可以自己尝试。(2)是否可以将四个节点合并成一个节点。