UE5中C++实现蓝图节点通配符Wildcard参数引脚(CustomStructureParam,CustomThunk)

前言

在蓝图的中,设置引脚时,有一种引脚类型,叫做通配符(wildcard)。此类型允许在做为引脚时,不约束参数类型,在编译时(静态)检查引脚参数类型,类型如果不符合宏内执行逻辑,则编译报错。

蓝图创建宏内

通配符,有些类似C++中的泛型编程设定,参数类型为泛型,则不约束输入类型,当编译时,检查输入类型是否合理,即不关心类型,只关心逻辑

需求分析

在使用蓝图宏内的通配符时,我有过这样的想法,是否可以通过输入引脚的内容,动态判定输入类型,来差异化执行逻辑?后来想想这是不行的,毕竟宏是静态过程的动作,所以运行时已经确定了结构,无法完成这样的设定。如果你知道方法,可以告诉我。

在C++中,向蓝图暴露函数时,遇到了一种设计诉求:编写工厂方法,完成对象创建,工厂方法提供参数输入,参数的类型不同,创建对象会出现差异。

这样的诉求如果在C++中,可以通过函数重载来实现。重载函数允许函数名字相同,类型不同。但是向蓝图暴露函数是无法做到函数重载的。

其实可以换个思路,虽然无法通过类似重载来实现需求,但是蓝图本身是解释型语言,当C++通过反射将函数“暴露”到蓝图中时,是可以在运行时推断传递参数类型的。这就可以解决我们的需求,即,通过传入的参数类型不同,来选取不同的执行逻辑

如何将传入的参数不限制类型?这就可以借助通配符的特性!虽然在蓝图的宏里无法动态推断参数类型,但是虚幻C++暴露给蓝图的函数,传入的参数在C++内接收,是可以推断类型的!这样就可以解决我们的问题了!

反射参数

UFUNCTION宏,标注在函数上,将函数增加反射特性。当添加函数说明符BlueprintCallable时,此函数将暴露到蓝图中。通过查看UHT生成中间gen编译文件,就可以看到将参数进行转换传递到C++中。

例如:在蓝图函数库头文件“UMyFunctionLibrary”中添加如下函数

1//示例代码
2UFUNCTION(BlueprintCallable)
3static void MyFunc(int32 Number);

在项目路径“\Intermediate\Build\Win64\UnrealEditor\Inc\ProtobufToturial\UHT”下可以查到生成中间文件

  • MyFunctionLibrary.generated.h
  • MyFunctionLibrary.gen.cpp

打开头文件“MyFunctionLibrary.generated.h”可以找到对应的创建的反射执行函数的声明(摘抄)

1#define FID_Projectes_UE_ProtobufToturial_Source_ProtobufToturial_Public_MyFunctionLibrary_h_15_RPC_WRAPPERS \
2 \
3	DECLARE_FUNCTION(execMyFunc);
4
5
6#define FID_Projectes_UE_ProtobufToturial_Source_ProtobufToturial_Public_MyFunctionLibrary_h_15_RPC_WRAPPERS_NO_PURE_DECLS \
7 \
8	DECLARE_FUNCTION(execMyFunc);

打开源文件“MyFunctionLibrary.gen.cpp”,可以找到定义函数代码(摘抄)

1DEFINE_FUNCTION(UMyFunctionLibrary::execMyFunc)
2{
3	P_GET_PROPERTY(FIntProperty,Z_Param_Number);
4	P_FINISH;
5	P_NATIVE_BEGIN;
6	UMyFunctionLibrary::MyFunc(Z_Param_Number);
7	P_NATIVE_END;
8}

从以上代码可以看出,execMyFunc函数是反射调用函数,函数内通过宏将传入参数进行解析,然后将参数再次传入到C++函数。对于exec标注函数,是由UHT生成,如果我们希望在中间插入逻辑,则需要跳过UHT生成。

此函数被调用时,所有的参数会仿照函数调用特性,将参数进行数据压栈,然后逐一从栈内将数据解析,查看P_GET_PROPERTY宏,即可发现解析参数是从栈(Stack)获取数据,参照如下源码。

1#define P_GET_PROPERTY(PropertyType, ParamName)\
2	PropertyType::TCppType ParamName = PropertyType::GetDefaultPropertyValue();\
3	Stack.StepCompiledIn<PropertyType>(&ParamName);

操作

首先需要使用UFUNCTION中两个反射标记(点击标记名称跳转到官方说明)

  • CustomThunk:函数说明符,标记函数跳过UHT工具生成编码,需要手动添加exec执行函数用于反射。
  • CustomStructureParam:函数元说明符,将指定的参数暴露为通配符。

编写函数

1UFUNCTION(BlueprintCallable, CustomThunk, meta=(CustomStructureParam="InputValue"))
2static void WildcardFunc(const int& InputValue);

此函数声明在到自定义蓝图函数库的头文件中,因函数中标记了CustomThunk,所以无需进行定义

添加exec函数

在反射设定中,当函数标记CustomThunk则需要手动编写执行exec函数,下面是手动编写对应的函数,名字必须是exec前缀加反射函数名

头文件中声明

1DECLARE_FUNCTION(execWildcardFunc);

源文件中定义

1DEFINE_FUNCTION(UMyFunctionLibrary::execWildcardFunc)
2{
3	//编写逻辑
4
5}

添加类型检测

由于蓝图和C++的交互是通过反射实现了,虚幻实现了反射机制,故对于蓝图中的数据类型,C++是完成了一层包裹。所以在运行时,我们就可以通过传入参数进行类型推断。

所有蓝图的传入参数会仿照语言函数调用进行压栈(此栈不是计算机中的栈),所有解析时需要从栈内取出所有数据。由于我的函数只带有一个参数,故只需要取出一次,如果希望了解多参数取出,可以定义反射函数,编译后,查看生成中间gen文件即可。

参考代码如下

 1DEFINE_FUNCTION(UMyFunctionLibrary::execWildcardFunc)
 2{
 3	//移动栈参数索引到第一个参数
 4	Stack.MostRecentProperty = nullptr;
 5	Stack.MostRecentPropertyAddress = nullptr;
 6	Stack.StepCompiledIn<FProperty>(nullptr);
 7	
 8	FProperty* Property = Stack.MostRecentProperty;
 9	//检查类型
10	if (Property->IsA(FIntProperty::StaticClass()))
11	{
12		int32* pNumber = Property->ContainerPtrToValuePtr<int32>(Stack.MostRecentPropertyContainer);
13	}
14	else if (Property->IsA(FBoolProperty::StaticClass()))
15	{
16		bool* pBool = Property->ContainerPtrToValuePtr<bool>(Stack.MostRecentPropertyContainer);
17	}
18	else if (Property->IsA(FObjectProperty::StaticClass()))
19	{
20		AActor** Object = Property->ContainerPtrToValuePtr<AActor*>(Stack.MostRecentPropertyContainer);
21	}
22	else if (Property->IsA(FArrayProperty::StaticClass()))
23	{
24		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property);
25		//检查元素类型
26		//ArrayProperty->Inner->IsA();
27		//检查数组元素个数
28		ArrayProperty->Inner->ElementSize;
29		//获取数组内容(假定数组中存放的是Actor)
30		TArray<AActor*> Actors;
31		ArrayProperty->CopyCompleteValueToScriptVM(&Actors, Stack.MostRecentPropertyAddress);
32	}
33	P_FINISH;
34	P_NATIVE_BEGIN;
35	//编写本地逻辑
36	P_NATIVE_END;
37}

上面的参考案例中,针对数组的解析其实比较特殊,虚幻提供了数组的专用通配符,元说明符“ArrayParm”,这部分内容本文不再说明,可以后面再开文章讨论。

引擎版本:5.2

文章评论