正确认识蓝图
在深入定制化和利用蓝图之前,我们应该先正确认识Unreal中的蓝图。首先蓝图属于一种资产即uasset,我们创建蓝图的时候实际上是创建了一个直接或间接派生自UBlueprint类型的资产,当我们编译蓝图的时候,它会生成我们实际所需要的蓝图类,生成的类型和你创建蓝图时选择的BlueprintType和ParentClass有关。
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_AIMoveTo和UK2Node_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是怎么根据我们的FactoryFunction和ProxyObject定制节点的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一般就是节点中最主要的逻辑了,其他的函数大多数都是一些选中节点后的操作、定义节点样式等。
创建自己的蓝图类型
本来想写这部分的,因为像AnimBlueprint、EditorWidgetBlueprint还有市场上有一些状态机等插件都创造了自己的蓝图类型,可以仿照他们去创建一个自己的蓝图类型。但看了一下似乎比创建自定义节点要复杂的多,还是等之后遇到有关需求再补充吧