前言
在蓝图的宏中,设置引脚时,有一种引脚类型,叫做通配符(wildcard)。此类型允许在做为引脚时,不约束参数类型,在编译时(静态)检查引脚参数类型,类型如果不符合宏内执行逻辑,则编译报错。
蓝图创建宏内
通配符,有些类似C++中的泛型编程设定,参数类型为泛型,则不约束输入类型,当编译时,检查输入类型是否合理,即不关心类型,只关心逻辑。
需求分析
在使用蓝图宏内的通配符时,我有过这样的想法,是否可以通过输入引脚的内容,动态判定输入类型,来差异化执行逻辑?后来想想这是不行的,毕竟宏是静态过程的动作,所以运行时已经确定了结构,无法完成这样的设定。如果你知道方法,可以告诉我。
在C++中,向蓝图暴露函数时,遇到了一种设计诉求:编写工厂方法,完成对象创建,工厂方法提供参数输入,参数的类型不同,创建对象会出现差异。
这样的诉求如果在C++中,可以通过函数重载来实现。重载函数允许函数名字相同,类型不同。但是向蓝图暴露函数是无法做到函数重载的。
其实可以换个思路,虽然无法通过类似重载来实现需求,但是蓝图本身是解释型语言,当C++通过反射将函数“暴露”到蓝图中时,是可以在运行时推断传递参数类型的。这就可以解决我们的需求,即,通过传入的参数类型不同,来选取不同的执行逻辑。
如何将传入的参数不限制类型?这就可以借助通配符的特性!虽然在蓝图的宏里无法动态推断参数类型,但是虚幻C++暴露给蓝图的函数,传入的参数在C++内接收,是可以推断类型的!这样就可以解决我们的问题了!
反射参数
UFUNCTION宏,标注在函数上,将函数增加反射特性。当添加函数说明符BlueprintCallable
时,此函数将暴露到蓝图中。通过查看UHT生成中间gen编译文件,就可以看到将参数进行转换传递到C++中。
例如:在蓝图函数库头文件“UMyFunctionLibrary”中添加如下函数
1
2
3
|
//示例代码
UFUNCTION(BlueprintCallable)
static void MyFunc(int32 Number);
|
在项目路径“\Intermediate\Build\Win64\UnrealEditor\Inc\ProtobufToturial\UHT”下可以查到生成中间文件
- MyFunctionLibrary.generated.h
- MyFunctionLibrary.gen.cpp
打开头文件“MyFunctionLibrary.generated.h”可以找到对应的创建的反射执行函数的声明(摘抄)
1
2
3
4
5
6
7
8
|
#define FID_Projectes_UE_ProtobufToturial_Source_ProtobufToturial_Public_MyFunctionLibrary_h_15_RPC_WRAPPERS \
\
DECLARE_FUNCTION(execMyFunc);
#define FID_Projectes_UE_ProtobufToturial_Source_ProtobufToturial_Public_MyFunctionLibrary_h_15_RPC_WRAPPERS_NO_PURE_DECLS \
\
DECLARE_FUNCTION(execMyFunc);
|
打开源文件“MyFunctionLibrary.gen.cpp”,可以找到定义函数代码(摘抄)
1
2
3
4
5
6
7
8
|
DEFINE_FUNCTION(UMyFunctionLibrary::execMyFunc)
{
P_GET_PROPERTY(FIntProperty,Z_Param_Number);
P_FINISH;
P_NATIVE_BEGIN;
UMyFunctionLibrary::MyFunc(Z_Param_Number);
P_NATIVE_END;
}
|
从以上代码可以看出,execMyFunc函数是反射调用函数,函数内通过宏将传入参数进行解析,然后将参数再次传入到C++函数。对于exec标注函数,是由UHT生成,如果我们希望在中间插入逻辑,则需要跳过UHT生成。
此函数被调用时,所有的参数会仿照函数调用特性,将参数进行数据压栈,然后逐一从栈内将数据解析,查看P_GET_PROPERTY宏,即可发现解析参数是从栈(Stack)获取数据,参照如下源码。
1
2
3
|
#define P_GET_PROPERTY(PropertyType, ParamName)\
PropertyType::TCppType ParamName = PropertyType::GetDefaultPropertyValue();\
Stack.StepCompiledIn<PropertyType>(&ParamName);
|
操作
首先需要使用UFUNCTION中两个反射标记(点击标记名称跳转到官方说明)
编写函数
1
2
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(CustomStructureParam="InputValue"))
static void WildcardFunc(const int& InputValue);
|
此函数声明在到自定义蓝图函数库的头文件中,因函数中标记了CustomThunk,所以无需进行定义。
添加exec函数
在反射设定中,当函数标记CustomThunk则需要手动编写执行exec函数,下面是手动编写对应的函数,名字必须是exec前缀加反射函数名。
头文件中声明
1
|
DECLARE_FUNCTION(execWildcardFunc);
|
源文件中定义
1
2
3
4
5
|
DEFINE_FUNCTION(UMyFunctionLibrary::execWildcardFunc)
{
//编写逻辑
}
|
添加类型检测
由于蓝图和C++的交互是通过反射实现了,虚幻实现了反射机制,故对于蓝图中的数据类型,C++是完成了一层包裹。所以在运行时,我们就可以通过传入参数进行类型推断。
所有蓝图的传入参数会仿照语言函数调用进行压栈(此栈不是计算机中的栈),所有解析时需要从栈内取出所有数据。由于我的函数只带有一个参数,故只需要取出一次,如果希望了解多参数取出,可以定义反射函数,编译后,查看生成中间gen文件即可。
参考代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
DEFINE_FUNCTION(UMyFunctionLibrary::execWildcardFunc)
{
//移动栈参数索引到第一个参数
Stack.MostRecentProperty = nullptr;
Stack.MostRecentPropertyAddress = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
FProperty* Property = Stack.MostRecentProperty;
//检查类型
if (Property->IsA(FIntProperty::StaticClass()))
{
int32* pNumber = Property->ContainerPtrToValuePtr<int32>(Stack.MostRecentPropertyContainer);
}
else if (Property->IsA(FBoolProperty::StaticClass()))
{
bool* pBool = Property->ContainerPtrToValuePtr<bool>(Stack.MostRecentPropertyContainer);
}
else if (Property->IsA(FObjectProperty::StaticClass()))
{
AActor** Object = Property->ContainerPtrToValuePtr<AActor*>(Stack.MostRecentPropertyContainer);
}
else if (Property->IsA(FArrayProperty::StaticClass()))
{
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property);
//检查元素类型
//ArrayProperty->Inner->IsA();
//检查数组元素个数
ArrayProperty->Inner->ElementSize;
//获取数组内容(假定数组中存放的是Actor)
TArray<AActor*> Actors;
ArrayProperty->CopyCompleteValueToScriptVM(&Actors, Stack.MostRecentPropertyAddress);
}
P_FINISH;
P_NATIVE_BEGIN;
//编写本地逻辑
P_NATIVE_END;
}
|
上面的参考案例中,针对数组的解析其实比较特殊,虚幻提供了数组的专用通配符,元说明符“ArrayParm”,这部分内容本文不再说明,可以后面再开文章讨论。
引擎版本:5.2