Unreal蓝图进阶

正确认识蓝图

在深入定制化和利用蓝图之前,我们应该先正确认识Unreal中的蓝图。首先蓝图属于一种资产即uasset,我们创建蓝图的时候实际上是创建了一个直接或间接派生自UBlueprint类型的资产,当我们编译蓝图的时候,它会生成我们实际所需要的蓝图类,生成的类型和你创建蓝图时选择的BlueprintTypeParentClass有关。

UENUM()
enum EBlueprintType : int
{
	/** Normal blueprint. */
	BPTYPE_Normal				UMETA(DisplayName="Blueprint Class"),
	/** Blueprint that is const during execution (no state graph and methods cannot modify member variables). */
	BPTYPE_Const				UMETA(DisplayName="Const Blueprint Class"),
	/** Blueprint that serves as a container for macros to be used in other blueprints. */
	BPTYPE_MacroLibrary			UMETA(DisplayName="Blueprint Macro Library"),
	/** Blueprint that serves as an interface to be implemented by other blueprints. */
	BPTYPE_Interface			UMETA(DisplayName="Blueprint Interface"),
	/** Blueprint that handles level scripting. */
	BPTYPE_LevelScript			UMETA(DisplayName="Level Blueprint"),
	/** Blueprint that serves as a container for functions to be used in other blueprints. */
	BPTYPE_FunctionLibrary		UMETA(DisplayName="Blueprint Function Library"),

	BPTYPE_MAX,
};
UCLASS(config=Engine)
class ENGINE_API UBlueprint : public UBlueprintCore, public IBlueprintPropertyGuidProvider
{
	GENERATED_UCLASS_BODY()
	UPROPERTY(meta=(NoResetToDefault))
	TSubclassOf<UObject> ParentClass;

	UPROPERTY(AssetRegistrySearchable)
	TEnumAsByte<enum EBlueprintType> BlueprintType;
}

我们日常使用最多的就是Normal类型的蓝图,即选择一个父类,生成派生自它的子类;除此外经常使用的几种蓝图也都是直接派生自UBlueprint
在这里插入图片描述
了解到这些后,其实一个经常问到的问题也就迎刃而解,那就是为什么加载蓝图类的时候要在路径后面加"_C"
蓝图生成的类型名称默认就是在蓝图名称后加"_C"表示Class,并且和蓝图在同一个路径下。
获取到蓝图后,可以通过UBlueprint->GeneratedClass获取其生成的类;相反也可以通过UClass->ClassGeneratedBy获取其蓝图资产

自定义蓝图节点

所有蓝图节点类型都直接或间接派生自UK2Node,更进一步的说是派生自UEdGraphNode,所有编辑器节点(行为树节点、状态机节点等)的基类,每个节点上的输入输出都是一个UEdGraphPin,Pin可以分为Input和Output分为两类,也可以按照数据类型分为若干中甚至包括容器等。
在对这些有了基础的了解后,我们可以开始定制一些自己的蓝图节点,虽然UK2Node中提供了大量的虚函数可以重载,但这样做未免太复杂了,实际上有大量的节点类型我们可以去参考甚至直接使用。这里以比较常见的需求即怎么创建一个有ExecPin类型输出的节点为例,分析一下UK2Node_AIMoveToUK2Node_BaseAsyncTask的实现方式以及派生我们自己的异步节点
在这里插入图片描述

UK2Node_AIMoveTo::UK2Node_AIMoveTo(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	ProxyFactoryFunctionName = GET_FUNCTION_NAME_CHECKED(UAIBlueprintHelperLibrary, CreateMoveToProxyObject);
	ProxyFactoryClass = UAIBlueprintHelperLibrary::StaticClass();
	ProxyClass = UAIAsyncTaskBlueprintProxy::StaticClass();
}

创建一个异步节点需要准备一个工厂函数和一个ProxyObject,当节点执行时会调用工厂函数,工厂函数应当创建ProxyObject来等待异步任务的完成,当任务完成时广播ProxyObject内声明的委托来实现异步的蓝图节点输出
对于这里的AIMoveTo来说,工厂函数就是CreateMoveProxyObject,它的参数决定了该蓝图节点的输入Pin,而它会返回一个ProxyObject,ProxyObject内的委托决定了该蓝图节点的输出Pin

UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", BlueprintInternalUseOnly = "TRUE"))
static UAIAsyncTaskBlueprintProxy* CreateMoveToProxyObject(UObject* WorldContextObject, APawn* Pawn, FVector Destination, AActor* TargetActor = NULL, float AcceptanceRadius = 5.f, bool bStopOnOverlap = false);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOAISimpleDelegate, EPathFollowingResult::Type, MovementResult);
class UAIAsyncTaskBlueprintProxy : public UObject
{
	GENERATED_UCLASS_BODY()

	UPROPERTY(BlueprintAssignable)
	FOAISimpleDelegate	OnSuccess;

	UPROPERTY(BlueprintAssignable)
	FOAISimpleDelegate	OnFail;
}

然后我们只需要再重写几个函数,就能自定义节点的目录和名称啦

	virtual FText GetTooltipText() const override;
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	virtual FText GetMenuCategory() const override;

在这里插入图片描述

再深入一点我们可以简单看看UK2Node_BaseAsyncTask是怎么根据我们的FactoryFunctionProxyObject定制节点的Pin
自定义节点的pin需要重写AllocateDefaultPin这个函数

void UK2Node_BaseAsyncTask::AllocateDefaultPins()
{
	InvalidatePinTooltips();

	const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

	CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);

	if (!bHideThen)
	{
		CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
	}
}

首先创建最基础的输入和输出节点,都是Exec类型,但Execute特指输入,Then特指输出节点

	UFunction* Function = GetFactoryFunction();
	if (!bHideThen && Function)
	{
		for (TFieldIterator<FProperty> PropIt(Function); PropIt && (PropIt->PropertyFlags & CPF_Parm); ++PropIt)
		{
			FProperty* Param = *PropIt;
			const bool bIsFunctionOutput = Param->HasAnyPropertyFlags(CPF_OutParm) && !Param->HasAnyPropertyFlags(CPF_ReferenceParm) && !Param->HasAnyPropertyFlags(CPF_ReturnParm);
			if (bIsFunctionOutput)
			{
				UEdGraphPin* Pin = CreatePin(EGPD_Output, NAME_None, Param->GetFName());
				K2Schema->ConvertPropertyToPinType(Param, /*out*/ Pin->PinType);
			}
		}
	}
	if (Function)
	{
		TSet<FName> PinsToHide;
		FBlueprintEditorUtils::GetHiddenPinsForFunction(GetGraph(), Function, PinsToHide);
		for (TFieldIterator<FProperty> PropIt(Function); PropIt && (PropIt->PropertyFlags & CPF_Parm); ++PropIt)
		{
			FProperty* Param = *PropIt;
			const bool bIsFunctionInput = !Param->HasAnyPropertyFlags(CPF_OutParm) || Param->HasAnyPropertyFlags(CPF_ReferenceParm);
			if (!bIsFunctionInput)
			{
				// skip function output, it's internal node data 
				continue;
			}

			UEdGraphNode::FCreatePinParams PinParams;
			PinParams.bIsReference = Param->HasAnyPropertyFlags(CPF_ReferenceParm) && bIsFunctionInput;
			UEdGraphPin* Pin = CreatePin(EGPD_Input, NAME_None, Param->GetFName(), PinParams);
			const bool bPinGood = (Pin && K2Schema->ConvertPropertyToPinType(Param, /*out*/ Pin->PinType));

			if (bPinGood)
			{
				// Check for a display name override
				const FString& PinDisplayName = Param->GetMetaData(FBlueprintMetadata::MD_DisplayName);
				if (!PinDisplayName.IsEmpty())
				{
					Pin->PinFriendlyName = FText::FromString(PinDisplayName);
				}
				
				//Flag pin as read only for const reference property
				Pin->bDefaultValueIsIgnored = Param->HasAllPropertyFlags(CPF_ConstParm | CPF_ReferenceParm) && (!Function->HasMetaData(FBlueprintMetadata::MD_AutoCreateRefTerm) || Pin->PinType.IsContainer());

				const bool bAdvancedPin = Param->HasAllPropertyFlags(CPF_AdvancedDisplay);
				Pin->bAdvancedView = bAdvancedPin;
				if(bAdvancedPin && (ENodeAdvancedPins::NoPins == AdvancedPinDisplay))
				{
					AdvancedPinDisplay = ENodeAdvancedPins::Hidden;
				}

				FString ParamValue;
				if (K2Schema->FindFunctionParameterDefaultValue(Function, Param, ParamValue))
				{
					K2Schema->SetPinAutogeneratedDefaultValue(Pin, ParamValue);
				}
				else
				{
					K2Schema->SetPinAutogeneratedDefaultValueBasedOnType(Pin);
				}

				if (PinsToHide.Contains(Pin->PinName))
				{
					Pin->bHidden = true;
				}
			}

			bAllPinsGood = bAllPinsGood && bPinGood;
		}
	}

然后利用反射去获取我们提供的FactoryFunction的参数,分别根据反射信息标签将参数映射为输入和输出节点ConvertPropertyToPinType可以根据属性类型映射为相应的Pin类型,还包括一些获取默认值等操作,它这里专门没有按照一般函数节点将工厂函数的返回值CPF_OutParm作为输出节点,不过它这里把CPF_ReferenceParm也不当作输出节点还是挺特别的,想看常规函数的节点Pin构造的话可以去看UK2Node_CallFunction

	UFunction* DelegateSignatureFunction = nullptr;
	for (TFieldIterator<FProperty> PropertyIt(ProxyClass); PropertyIt; ++PropertyIt)
	{
		if (FMulticastDelegateProperty* Property = CastField<FMulticastDelegateProperty>(*PropertyIt))
		{
			UEdGraphPin* ExecPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, Property->GetFName());
			ExecPin->PinToolTip = Property->GetToolTipText().ToString();
			ExecPin->PinFriendlyName = Property->GetDisplayNameText();

			if (!DelegateSignatureFunction)
			{
				DelegateSignatureFunction = Property->SignatureFunction;
			}
		}
	}

	if (DelegateSignatureFunction)
	{
		for (TFieldIterator<FProperty> PropIt(DelegateSignatureFunction); PropIt && (PropIt->PropertyFlags & CPF_Parm); ++PropIt)
		{
			FProperty* Param = *PropIt;
			const bool bIsFunctionInput = !Param->HasAnyPropertyFlags(CPF_OutParm) || Param->HasAnyPropertyFlags(CPF_ReferenceParm);
			if (bIsFunctionInput)
			{
				UEdGraphPin* Pin = CreatePin(EGPD_Output, NAME_None, Param->GetFName());
				K2Schema->ConvertPropertyToPinType(Param, /*out*/ Pin->PinType);

				UK2Node_CallFunction::GeneratePinTooltipFromFunction(*Pin, DelegateSignatureFunction);
			}
		}
	}

最后根据反射查找ProxyObject中的委托类型属性,并为每一个属性创建Exec输出Pin,但这里默认所有属性都属于同一个委托类型,如果不同委托类型的话只会以第一个属性的委托类型为准,利用反射获取委托参数来创建其他类型的输出Pin,和前面反射函数的操作完全一样
至此创建节点接口(Pin)的操作就完成了,像其他类型的节点创建接口基本也都跟这个差不多,基于反射去访问Property。创建Pin一般就是节点中最主要的逻辑了,其他的函数大多数都是一些选中节点后的操作、定义节点样式等。

创建自己的蓝图类型

本来想写这部分的,因为像AnimBlueprintEditorWidgetBlueprint还有市场上有一些状态机等插件都创造了自己的蓝图类型,可以仿照他们去创建一个自己的蓝图类型。但看了一下似乎比创建自定义节点要复杂的多,还是等之后遇到有关需求再补充吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值