1. Overview

本教程旨在讲解使用ue4 UFUNCTION说明符(Specifier)自定义泛型蓝图节点,实现UObject的任意类型属性(UProperty)GET/SET方法。如下四个泛型蓝图节点,依据UObject对象的PropertyName,GET/SET属性值,适用于v4.25之前版本。

图1. 泛型蓝图节点;

2. Introduction

泛型蓝图节点也就是带有可变参数的蓝图节点,通用指蓝图系统中带有通配符(wildcard)类型参数的蓝图节点。实现泛型蓝图节点至少有二种方式,其一,继承UK2Node类,并根据需要实现其派生类,如常用的Cast节点Set By-Ref Var节点等都属于此类;其二,使用UFUNCTIONCustomThunk说明符以及相应的类型说明符标识wildcard参数,并为该蓝图函数自定义DECLARE_FUNCTION()函数体,如Utilities|Array,Utilities|Map,Utilities|Set 目录下,三种容器Array、Map、Set各类操作的蓝图节点都属于此类。此外ue4内置的蓝图节点还存在着将二者结合的蓝图节点GetDataTableRowUK2Node_GetDataTableRow类(基类UK2Node)在Editor状态下动态更新Pin,UDataTableFunctionLibrary类中GetDataTableRowFromName函数在Runtime状态下,执行具体的GetDataTableRowFromName方法,将DataTable中的RowStruct拷贝到OutRowStruct。以上二种方式各有其优缺点,本次讲解采用代码量少、实现难度低的第二种方式。

3. Required Knowledge

  1. 熟悉UE4 C++ UFUNCTION宏常用关键字,

  2. 自定义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++类,也可以获得到蓝图的该属性,同时也可以使二个蓝图的引用断开)。

图2. 二种获取UProperty值得方法;

那么断开二个蓝图的引用有什么好处?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)的属性都是使用同一个节点。

图3. 泛型蓝图节点GET/SET使用示例;

图4. 输出日志LOG;

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);
源代码中SetPropertyByName函数;

每一种类型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. 泛型蓝图节点示例;

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)是否可以将四个节点合并成一个节点。