Unreal的蓝图和C++一样,也是一种静态类型的编程语言,它又不像其他静态类型语言那样支持模板,有些时候就觉得很不方便。思考了一下这个问题。想要蓝图节点支持任意类型的参数,主要分为两种情况:

  • UObject派生类对象:那很简单了,使用基类指针作为参数就好,在C++里面可以Cast,或者取得对象的UClass,就可以根据反射信息做很多事了;
  • Struct类型,或者TArray<MyStruct>类型:这个是本文的重点。

其实说蓝图完全不支持“模板”也是不对的,引擎中其实已经有很多能够处理任意Struct或者TArray<MyStruct>类型的节点了!官方文档中把这种情况叫做参数“Wildcard”(通配符)。感谢Unreal开源,通过阅读源代码,加上一点实验,就能够搞清楚具体实现方法和背后的细节。

下面主要探讨使用UFUNCTION的CustomThunk描述符,实现自定义的Thunk函数;然后通过指定meta的CustomStructureParamArrayParm参数,来实现参数类型“通配符”!这中间的难点是:需要明确蓝图Stack的处理方式。Demo如下图所示:

在上图的Demo中:

  1. 自定义了一个蓝图Struct:MyStruct
  2. 使用C++实现了一个蓝图节点“Show Struct Fields”:可以接受任意UStruct的引用,具体类型可以由C++或者蓝图定义;
  3. 蓝图节点“Array Numeric Field Average”:可以接受任意类型的TArray<MyStruct>,并对数组中指定的数值型字段求平均;

完整的Demo工程可以从我的GitHub下载:https://github.com/neil3d/UnrealCookBook/tree/master/MyBlueprintNode

实现蓝图功能节点的几种方式

在Unreal开发中可以使用C++对蓝图进行扩展,生成Unreal蓝图节点最方便的方法就是写一个UFUNCTION,无论是定义在UBlueprintFunctionLibrary派生类里面的static函数,还是定义在UObject、AActor派生类里面的类成员函数,只要加上UFUNCTION宏修饰,并在宏里面添加BlueprintCallable标识符,就可以自动完成蓝图编辑节点、蓝图节点执行调用的整个过程。不过,由于C++和蓝图都属于“静态类型”编程语言,这种形式编写的蓝图节点,所有的输入、输出参数的类型都必须是固定的,这样引擎才能自动处理蓝图虚拟机的栈。

先来总结一下C++实现蓝图节点的几种方式:

  1. UFUNCTION,上面已经说过了;
  2. 实现class UK2Node的派生类,这是最强大的方式,是对蓝图节点最深入的定制开发,如果你需要动态的添加、删除蓝图节点的针脚,就只能用这种方式了。例如我们常用的“Format Text”节点,可以根据输入字符串中的“{index}”来动态增加输入节点,输入节点的类型也是动态的,这个就是通过class UK2Node_FormatText这个类来实现的;
  3. 还有介于上面两者之间的一种方式,就是在UFUNCTION中使用“CustomThunk”标识,告诉UHT(Unreal Header Tool)不要生成默认的蓝图包装函数,而是由我们手工实现。这种方式,需要手工控制蓝图虚拟机的“栈”,但是不用处理蓝图编辑器UI部分,相对第2种来说代码量要少很多,相对第1种来说,又多了很多控制力;
  4. 另外,蓝图的“宏”–Macros,也可以实现自己的节点。

使用第3种方式,结合UFUNCTION的其它meta标识符,可以实现参数类型的“通配符”,就可以实现模板函数,也就是输入、输出参数可以处理多种数据类型,类似C++的泛型。这些meta标识符主要有:

  1. ArrayParm="Parameter1, Parameter2, ..":说明 BlueprintCallable 函数应使用一个Call Array Function节点,且列出的参数应被视为通配符数组属性;
  2. ArrayTypeDependentParams="Parameter":使用 ArrayParm 时,此说明符将指定一个参数,其将确定 ArrayParm 列表中所有参数的类型;
  3. CustomStructureParam="Parameter1, Parameter2, ..":列出的参数都会被视为通配符。

引擎源代码中,这种编程方式的典型的例子有:

  • 蓝图编辑器中的“Utilities”->“Array”菜单中的所有节点,他们可以处理任意的UStruct类型的数组。这些节点对应的源代码是:class UKismetArrayLibrary
  • class UDataTableFunctionLibrary::GetDataTableRowFromName(UDataTable* Table, FName RowName, FTableRowBase& OutRow)

详见官方文档:UFunctions

CustomThunk函数

如果在UFUNCTION宏里面指定了CustomThunk,那么UHT就不会自动生成这个函数的“thunk”,而需要开发者自己实现。这里的“thunk”是什么呢?我们看个例子。

我们来做个最简单的小试验,在工程中建立一个Blueprint Function Library,添加一个简单的UFUNCTION:

#pragma once#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"UCLASS()
class MYBLUEPRINTNODES_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{GENERATED_BODY()
public:UFUNCTION(BlueprintCallable)static int Sum(int a, int b);
};

然后在对应的cpp文件中,使用C++实现这个函数:

#include "MyBlueprintFunctionLibrary.h"int UMyBlueprintFunctionLibrary::Sum(int a, int b) {return a + b;
}

项目build一下,然后你就可以在“Intermediate”目录找到这个"MyBlueprintFunctionLibrary.generated.h"文件。在这个文件里面,你可以找到这样一段代码:

    DECLARE_FUNCTION(execSum) \{ \P_GET_PROPERTY(UIntProperty,Z_Param_a); \P_GET_PROPERTY(UIntProperty,Z_Param_b); \P_FINISH; \P_NATIVE_BEGIN; \*(int32*)Z_Param__Result=UMyBlueprintFunctionLibrary::Sum(Z_Param_a,Z_Param_b); \P_NATIVE_END; \}

这段代码就是蓝图函数节点的thunk了!这段代码做了这样几件事:

  1. 声明了一个名为“execSum”的函数,函数的签名为:void func( UObject* Context, FFrame& Stack, RESULT_DECL )
  2. 使用P_GET_PROPERTY宏,从“FFrame& Stack”(也就是蓝图虚拟机的栈)中取出函数参数;
  3. 调用P_FINISH宏;
  4. 使用取出的这些参数调用我们实现的UMyBlueprintFunctionLibrary::Sum()函数;

“thunk”函数是一个包装,它完成的核心任务就是处理蓝图虚拟机的Stack,然后调用我们使用C++实现的函数。

我们还可以看一下UHT帮我们生成的另外一个文件:MyBlueprintFunctionLibrary.gen.cpp,在其中有这样一段代码:

void UMyBlueprintFunctionLibrary::StaticRegisterNativesUMyBlueprintFunctionLibrary(){UClass* Class = UMyBlueprintFunctionLibrary::StaticClass();static const FNameNativePtrPair Funcs[] = {{ "Sum", &UMyBlueprintFunctionLibrary::execSum },};FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));}

这段代码把刚才"MyBlueprintFunctionLibrary.generated.h"中声明的excSum函数注册到了UMyBlueprintFunctionLibrary::StaticClass()这个UClass对象之中,并指定它的名字为“Sum”,也就是我们原始C++代码中声明的函数名,也是在蓝图编辑器中显示的名字。

看清楚了什么是“thunk函数”,“CustomThunk函数”也就不言自明了。在UFUNCTION中指定“CustomThunk”标识符,就是告诉UHT,不要在.generated.h中生成DECLARE_FUNCTION那部分代码,这部分代码改由手写。为啥要抛弃自动生成,而手写呢?回到本文主题:要实现“参数类型通配符”(或者叫做“蓝图模板节点”),就必须手写thunk!

蓝图Stack探索

要实现自己的thunk函数,核心任务就是“准确的处理蓝图虚拟机的栈”,可惜的是官方并没有这方面的文档!下面我就把自己的一些探索记录下来,请大家指正。

以上面的int Sum(int a, int b)函数为例,thunk函数使用P_GET_PROPERTY宏从Stack取值,这个宏P_GET_PROPERTY(UIntProperty,Z_Param_a)展开之后的代码如下所示:

 UIntProperty::TCppType Z_Param_a = UIntProperty::GetDefaultPropertyValue();Stack.StepCompiledIn<UIntProperty>(&Z_Param_a);

其中UIntProperty派生自TProperty_Numeric<int32>UIntProperty::TCppType就是“int32”无疑!

我们还需要处理TArray<MyStruct>这样的数据,所以我们重点要看一下这种参数类型的栈处理。
假设我们有一个C++的UStruct:

USTRUCT(Blueprintable)
struct FMyStruct {GENERATED_USTRUCT_BODY()UPROPERTY(EditAnywhere, BlueprintReadWrite)FString Name;UPROPERTY(EditAnywhere, BlueprintReadWrite)int Value;
};

类似这样一个UFUNCTION:

UFUNCTION(BlueprintCallable)
static void PrintMyStructArray(const TArray<FMyStruct>& MyStructArray);

则在.h中的thunk函数为:

DECLARE_FUNCTION(execPrintMyStructArray) \{ \P_GET_TARRAY_REF(FMyStruct,Z_Param_Out_MyStructArray); \P_FINISH; \P_NATIVE_BEGIN; \UMyBlueprintFunctionLibrary::PrintMyStructArray(Z_Param_Out_MyStructArray); \P_NATIVE_END; \} \

其中P_GET_TARRAY_REF(FMyStruct,Z_Param_Out_MyStructArray);这个宏展开之后的代码为:

PARAM_PASSED_BY_REF(Z_Param_Out_MyStructArray, UArrayProperty, TArray<FMyStruct>)

最终展开为:

TArray<FMyStruct> Z_Param_Out_MyStructArrayTemp;
TArray<FMyStruct>& Z_Param_Out_MyStructArray = Stack.StepCompiledInRef<UArrayProperty, TArray<FMyStruct> >(&Z_Param_Out_MyStructArrayTemp);

综合上面两个例子,我们发现核心操作都是调用template<class TProperty> void FFrame::StepCompiledIn(void*const Result)这个模板函数。通过跟踪这个函数的执行,发现它实际调用了UObject::execInstanceVariable()函数。

  1. 更新"FFrame::PropertyChainForCompiledIn"这个成员变量;
  2. 使用更新后的“FFrame::PropertyChainForCompiledIn”值,更新了"FFrame::MostRecentPropertyAddress"成员变量。

再结合引擎中CustomThunk函数的实现源码,可以得出这样的结论:

  1. 通过调用Stack.StepCompiledIn()函数,就可以更新蓝图虚拟机的栈顶指针;

  2. Stack.MostRecentPropertyAddressStack.MostRecentProperty这两个变量,就是当前参数值的内存地址和反射信息。

有了具体变量的内存地址和类型的反射信息,就足够做很多事了。下面我们就开始实践。

实践1:接受任意UStruct类型参数

下面我们就看一下文章开头的这张图里面的蓝图节点“Show Struct Fields”是如何接受任意类型UStruct参数的。

先上代码, BlueprintWildcardLibrary.h

USTRUCT(BlueprintInternalUseOnly)
struct FDummyStruct {GENERATED_USTRUCT_BODY()};UCLASS()
class UNREALCOOKBOOK_API UBlueprintWildcardLibrary : public UBlueprintFunctionLibrary {GENERATED_BODY()public:UFUNCTION(BlueprintCallable, CustomThunk, Category = "MyDemo", meta = (CustomStructureParam = "CustomStruct"))static void ShowStructFields(const FDummyStruct& CustomStruct);static void Generic_ShowStructFields(const void* StructAddr, const UStructProperty* StructProperty);DECLARE_FUNCTION(execShowStructFields) {Stack.MostRecentPropertyAddress = nullptr;Stack.MostRecentProperty = nullptr;Stack.StepCompiledIn<UStructProperty>(NULL);void* StructAddr = Stack.MostRecentPropertyAddress;UStructProperty* StructProperty = Cast<UStructProperty>(Stack.MostRecentProperty);P_FINISH;P_NATIVE_BEGIN;Generic_ShowStructFields(StructAddr, StructProperty);P_NATIVE_END;}
};

BlueprintWildcardLibrary.cpp

#include "BlueprintWildcardLibrary.h"
#include "Engine/Engine.h"void UBlueprintWildcardLibrary::Generic_ShowStructFields(const void* StructAddr, const UStructProperty* StructProperty) {UScriptStruct* Struct = StructProperty->Struct;for (TFieldIterator<UProperty> iter(Struct); iter; ++iter) {FScreenMessageString NewMessage;NewMessage.CurrentTimeDisplayed = 0.0f;NewMessage.Key = INDEX_NONE;NewMessage.DisplayColor = FColor::Blue;NewMessage.TimeToDisplay = 5;NewMessage.ScreenMessage = FString::Printf(TEXT("Property: [%s].[%s]"),*(Struct->GetName()),*(iter->GetName()));NewMessage.TextScale = FVector2D::UnitVector;GEngine->PriorityScreenMessages.Insert(NewMessage, 0);}
}

解释一下这段代码:

  1. 首先声明了一个UFunction:static void ShowStructFields(const FDummyStruct& CustomStruct);,其参数类型是“FDummyStruct”,这只是一个占位符;
  2. 在UFUNCTION宏里面指定“CustomThunk”和“CustomStructureParam”;
  3. 实现一个execShowStructFields函数。这个函数很简单,主要是处理蓝图的Stack,从中取出需要的参数,然后对用C++的实现;
  4. 具体功能实现在:static void Generic_ShowStructFields(const void* StructAddr, const UStructProperty* StructProperty)这个函数中。

实践2:对数组中的Struct的数值型求平均

下面我们再来一下文章开头的这张图里面的“Array Numeric Field Average”蓝图节点是如何通过“CustomThunk”函数来实现的。

参照引擎源代码,我定义了这样一个宏,用来从栈上取出泛型数组参数,并正确的移动栈指针:

#define P_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty) Stack.MostRecentProperty = nullptr;\Stack.StepCompiledIn<UArrayProperty>(NULL);\void* ArrayAddr = Stack.MostRecentPropertyAddress;\UArrayProperty* ArrayProperty = Cast<UArrayProperty>(Stack.MostRecentProperty);\if (!ArrayProperty) {    Stack.bArrayContextFailed = true;  return; }

通过这个宏,可以得到两个局部变量:

  • void* ArrayAddr: 数组的起始内存地址;
  • UArrayProperty* ArrayProperty: 数组的反射信息,ArrayProperty->Inner就是数组成员对应的类型了;

有了这个宏,我们就可以很方便的写出thunk函数了:

DECLARE_FUNCTION(execArray_NumericPropertyAverage) {// get TargetArrayP_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty);// get PropertyNameP_GET_PROPERTY(UNameProperty, PropertyName);P_FINISH;P_NATIVE_BEGIN;*(float*)RESULT_PARAM = GenericArray_NumericPropertyAverage(ArrayAddr, ArrayProperty, PropertyName);P_NATIVE_END;}

经过以上的准备,我们就已经可以正确的处理“泛型数组”了。下一步就是对这个数组中指定的数“值类型成员变量”求均值了,这主要依靠Unreal的反射信息,一步步抽丝剥茧,找到数组中的每个变量即可。反射系统的使用不是本文的重点,先看完整代码吧。

BlueprintWildcardLibrary.h

#pragma once#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "BlueprintWildcardLibrary.generated.h"#define P_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty) Stack.MostRecentProperty = nullptr;\Stack.StepCompiledIn<UArrayProperty>(NULL);\void* ArrayAddr = Stack.MostRecentPropertyAddress;\UArrayProperty* ArrayProperty = Cast<UArrayProperty>(Stack.MostRecentProperty);\if (!ArrayProperty) {  Stack.bArrayContextFailed = true;  return; }UCLASS()
class UNREALCOOKBOOK_API UBlueprintWildcardLibrary : public UBlueprintFunctionLibrary {GENERATED_BODY()public:UFUNCTION(BlueprintPure, CustomThunk, meta = (DisplayName = "Array Numeric Property Average", ArrayParm = "TargetArray", ArrayTypeDependentParams = "TargetArray"), Category = "MyDemo")static float Array_NumericPropertyAverage(const TArray<int32>& TargetArray, FName PropertyName);static float GenericArray_NumericPropertyAverage(const void* TargetArray, const UArrayProperty* ArrayProperty, FName ArrayPropertyName);public:DECLARE_FUNCTION(execArray_NumericPropertyAverage) {// get TargetArrayP_GET_GENERIC_ARRAY(ArrayAddr, ArrayProperty);// get PropertyNameP_GET_PROPERTY(UNameProperty, PropertyName);P_FINISH;P_NATIVE_BEGIN;*(float*)RESULT_PARAM = GenericArray_NumericPropertyAverage(ArrayAddr, ArrayProperty, PropertyName);P_NATIVE_END;}
};

BlueprintWildcardLibrary.cpp


#include "BlueprintWildcardLibrary.h"
#include "Engine/Engine.h"float UBlueprintWildcardLibrary::Array_NumericPropertyAverage(const TArray<int32>& TargetArray, FName PropertyName) {// We should never hit these!  They're stubs to avoid NoExport on the class.  Call the Generic* equivalent insteadcheck(0);return 0.f;
}float UBlueprintWildcardLibrary::GenericArray_NumericPropertyAverage(const void* TargetArray, const UArrayProperty* ArrayProperty, FName PropertyName) {UStructProperty* InnerProperty = Cast<UStructProperty>(ArrayProperty->Inner);if (!InnerProperty) {UE_LOG(LogTemp, Error, TEXT("Array inner property is NOT a UStruct!"));return 0.f;}UScriptStruct* Struct = InnerProperty->Struct;FString PropertyNameStr = PropertyName.ToString();UNumericProperty* NumProperty = nullptr;for (TFieldIterator<UNumericProperty> iter(Struct); iter; ++iter) {if (Struct->PropertyNameToDisplayName(iter->GetFName()) == PropertyNameStr) {NumProperty = *iter;break;}}if (!NumProperty) {UE_LOG(LogTemp, Log, TEXT("Struct property NOT numeric = [%s]"),*(PropertyName.ToString()));}FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);int Count = ArrayHelper.Num();float Sum = 0.f;if(Count <= 0)return 0.f;if (NumProperty->IsFloatingPoint())for (int i = 0; i < Count; i++) {void* ElemPtr = ArrayHelper.GetRawPtr(i);const uint8* ValuePtr = NumProperty->ContainerPtrToValuePtr<uint8>(ElemPtr);Sum += NumProperty->GetFloatingPointPropertyValue(ValuePtr);}else if (NumProperty->IsInteger()) {for (int i = 0; i < Count; i++) {void* ElemPtr = ArrayHelper.GetRawPtr(i);const uint8* ValuePtr = NumProperty->ContainerPtrToValuePtr<uint8>(ElemPtr);Sum += NumProperty->GetSignedIntPropertyValue(ValuePtr);}}// TODO: else if(enum类型)return Sum / Count;
}

深入Unreal蓝图开发:实现蓝图模板函数相关推荐

  1. 深入Unreal蓝图开发:自定义蓝图节点(下)

    通过前面的文章,我们已经能够创建自己的蓝图节点,并可以动态添加.删除Pins,但是感觉好像有什么地方不太对劲啊.你发现没有?那就是前面两篇文章中,我们自定义的蓝图节点都是通过UK2Node::Expa ...

  2. 深入Unreal蓝图开发:理解蓝图技术架构

    前面几篇博客谈了几种常用的蓝图扩展方式,其中也对蓝图的底层机制进行了部分的解析,但是还不够整体.这篇文章谈一下目前我对蓝图技术架构的系统性的理解,包括蓝图从编辑到运行的整个过程. 蓝图的发展历程 蓝图 ...

  3. C++ 模版类和模板函数介绍及使用

    1. 简介 模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数.返回值取得任意类型. 模板是泛型编程的基础,泛型编程即以一种独立 ...

  4. java定义类模板_定义模板——函数模板和类模板

    面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况.不同之处在于:OOP能处理类型在程序运行之前都未知的情况:而在泛型编程中,在编译时就能获知类型了. 前面介绍的容器.迭代器和算法都 ...

  5. 【C++】模板-函数模板、类模板

    文章目录 泛型编程 函数模板 函数模板的原理 函数模板的实例化 模板参数的匹配原则 类模板 类模板的定义格式 类模板的实例化 泛型编程 如果我们想要实现一个通用的交换函数,我们可以通过函数重载来实现, ...

  6. [UE4蓝图教程]蓝图入门之蓝图通信机制入门

    最近正好学习蓝图碰撞和蓝图间通信的有关部分,所以,我正好在此记录一下,首先是蓝图和角色间的碰撞,比如下面这个蓝图 蓝图与玩家间的碰撞,其实和触发器触发事件有点像,首先,通过OnComPonentBeg ...

  7. Django 3.2.5博客开发教程:实现模板之前的分析与准备

    在之前的体验django模板.体验数据查询以及一些常用的模板使用方法文章里,向大家介绍了如何将数据库的数据展现到网页上,和一些简单的模板使用方法.之后我们就开始实现各种页面的展现. 在此之前,我们先从 ...

  8. C语言中比较大小的函数模板,C语言中实现模板函数小结 : 不敢流泪

    --by boluor 2009/5/20 如果要写个函数支持多种数据类型,首先想到的就是C++的模板了,但是有时候只能用C语言,比如在linux内核开发中,为了减少代码量,或者是某面试官的要求- 考 ...

  9. php使用模版开发的实例,PHP-Web应用程序开发:使用模板_php

    每个进行过较大型的php-web应用程序设计的开发人员大概都有如下的经历:花大量的时间写超文本语句,为页面排版,兼作美工等:或在整合的程序代码在和html静态页面时花费大量的时间.的确,用脚本语言开发 ...

  10. 基于jQuery开发的javascript模板引擎-jTemplates

    这里介绍一个基于jQuery开发的模板引擎. jTemplates目前最新的版本是0.7.8,由tPython开发.官方网站:http://jtemplates.tpython.com 两个附件,一个 ...

最新文章

  1. 接口,抽象类与内部类
  2. 好的物联网开源框架_通过开源文化实现更好的物联网
  3. docker rabbitmq_Docker部署RabbitMQ集群
  4. vs2003 打开VS.NET项目时遇到 “无法从web服务器获取项目文件” 解决方案
  5. Vijos P1335 数独验证【谜题】
  6. 加一条平行于y轴的直线_为什么龙门式桁架机器人采用V型导向滚轮直线导轨
  7. Android Studio教程– Hello World App
  8. 网站前端_KindEditor.基础入门.0002.KindEditor_3.4.2配置参数?
  9. 信号与系统(十七)—— 傅里叶变换及性质(1)常用函数的傅里叶变换
  10. ps里怎么给透明背景填充颜色
  11. 句子迷,语录,苏引华
  12. 如何添加打印机到电脑
  13. mac抹掉磁盘重装系统未能与服务器取得联系_如何用U盘自制Mac笔记本系统重装盘...
  14. acme + acme-dns + google domains 签发泛域名证书
  15. 《InnoDB存储引擎》第五章——索引与算法
  16. EXCEL表格序列号
  17. 什么是XSL?(XSL概述)
  18. python模拟生态系统
  19. python版openvino使用
  20. 重t2加权是什么意思_LOL:每周半价恢复,幸好有一款T2等级皮肤撑场面

热门文章

  1. [Unity] UGUI学习笔记
  2. 判断某个字符串是否为数字
  3. 【Web技术】1395- Esbuild Bundler HMR
  4. day12摇色子游戏--笔记
  5. 刷脸支付实现人与钱包或银行卡的合二为一
  6. java der decode_支付宝进行签名时爆DER input, Integer tag error异常
  7. 2016年,你要学习这些移动开发技术
  8. 张孝祥张老师一路走好!
  9. 初学C语言的感受(张森)
  10. python获取本月第一天 最后一天