深入Unreal蓝图开发:自定义蓝图节点(中)
通過本系列文章上篇的介紹,我們已經可以創建一個“沒什么用”的藍圖節點了。要想讓它有用,關鍵還是上篇中說的典型應用場景:動態添加Pin,這篇博客就來解決這個問題。
目標
和上篇一樣,我還將通過一個盡量簡單的節點,來說明"可動態添加Pin的藍圖節點"的實現過程,讓大家盡量聚焦在“藍圖自定義節點”這個主題上。
設想這樣一個節點:Say Something,把輸入的N個字符串連接起來,然后打印輸出。也就是說,這個節點的輸入Pin是可以動態添加的。我們將在上篇的那個工程基礎上實現這個自定義節點。最終實現的效果如下圖所示:
下面我們還是來仔細的過一遍實現步驟吧!
創建Blueprint Graph節點類型
首先,我們還是需要創建一個class UK2Node的派生類,這個過程在上篇中已經詳細說過了,照單炒菜,很容易就創建了下圖這樣一個空的自定義節點,這里就不贅述了。不清楚的話,可以返回去在照著上篇做就好了。
創建自定義的節點Widget
我們要動態增加Pin的話,需要在節點上顯示一個"加號按鈕",點擊之后增加一個“input pin”。這就不能使用默認的Blueprint Graph Node Widget了,需要對其進行擴展。這個擴展的思路和前面一樣,也是找到特定的基類,重載其虛函數即可,這個基類就是class SGraphNodeK2Base。我們要重載的兩個核心的函數是:
來看一下最簡化的代碼吧:
SGraphNodeSaySomething.h
SGraphNodeSaySomething.cpp
void SGraphNodeSaySomething::Construct(const FArguments& InArgs, UBPNode_SaySomething* InNode) {this->GraphNode = InNode;this->SetCursor( EMouseCursor::CardinalCross );this->UpdateGraphNode(); }void SGraphNodeSaySomething::CreateInputSideAddButton(TSharedPtr<SVerticalBox> InputBox) {FText Tmp = FText::FromString(TEXT("Add word"));TSharedRef<SWidget> AddPinButton = AddPinButtonContent(Tmp, Tmp);FMargin AddPinPadding = Settings->GetInputPinPadding();AddPinPadding.Top += 6.0f;InputBox->AddSlot().AutoHeight().VAlign(VAlign_Center).Padding(AddPinPadding)[AddPinButton]; }FReply SGraphNodeSaySomething::OnAddPin() { }如果你接觸過Unreal Slate的話,上面這個Slate Widget的代碼很容易看懂啦,如果你沒有玩過Slate。。。。Slate是虛幻自己的一套 Immediate Mode UI framework,建議先過一下官方文檔。
最后,因為這個基類:SGraphNodeK2Base,屬于GraphEditor模塊,所以要修改MyBlueprintNodeEditor.Build.cs,把它添加到PrivateDependencyModuleNames:
PrivateDependencyModuleNames.AddRange(new string[] {"UnrealEd","GraphEditor","BlueprintGraph","KismetCompiler","MyBlueprintNode"});擴展藍圖編輯器的節點Widget
OK,上面我們已經創建了兩個類,分別是:
下面我們就需要讓藍圖編輯器知道:創建UBPNode_SaySomething對象的時候,需要使用SGraphNodeSaySomething這個Widget。
添加自定義Node Widget的兩種方式(參見引擎源碼class FNodeFactory):
在這里,我們使用第一種方式,也就是在class UBPNode_SaySomething中重載父類的虛函數CreateVisualWidget()。
TSharedPtr<SGraphNode> UBPNode_SaySomething::CreateVisualWidget() {return SNew(SGraphNodeSaySomething, this); }完成上述代碼之后,運行藍圖編輯器,添加Say Something節點,就可以看到這個Widget了:
動態增加輸入參數變量
當用戶點擊“Add Word +”按鈕時,SGraphNodeSaySomething::OnAddPin()會被調用,下面是它的實現代碼:
FReply SGraphNodeSaySomething::OnAddPin() {UBPNode_SaySomething* BPNode = CastChecked<UBPNode_SaySomething>(GraphNode);const FScopedTransaction Transaction(NSLOCTEXT("Kismet", "AddArgumentPin", "Add Argument Pin"));BPNode->Modify();BPNode->AddPinToNode();FBlueprintEditorUtils::MarkBlueprintAsModified(BPNode->GetBlueprint());UpdateGraphNode();GraphNode->GetGraph()->NotifyGraphChanged();return FReply::Handled(); }上面這段代碼主要是響應用戶的UI操作,添加Pin的核心操作,還是放在UBPNode_SaySomething::AddPinToNode()這個函數里面去實現的:
void UBPNode_SaySomething::AddPinToNode() {TMap<FString, FStringFormatArg> FormatArgs= {{TEXT("Count"), ArgPinNames.Num()}};FName NewPinName(*FString::Format(TEXT("Word {Count}"), FormatArgs));ArgPinNames.Add(NewPinName);CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, NewPinName); }現在我們就可以在藍圖編輯器里面操作添加輸入Pin了 :
動態刪除Pin
如果用戶想要刪除某個輸入變量Pin,他需要在那個Pin上點擊鼠標右鍵,呼出Context Menu,選擇“刪除”菜單項將其移除。下面我們就看看這個操作是如何實現的。
我們可以通過重載void UEdGraphNode::GetContextMenuActions(const FGraphNodeContextMenuBuilder& Context) const來定制Context Menu。
void UBPNode_SaySomething::GetContextMenuActions(const FGraphNodeContextMenuBuilder & Context) const {Super::GetContextMenuActions(Context);if (Context.bIsDebugging)return;Context.MenuBuilder->BeginSection("UBPNode_SaySomething", FText::FromString(TEXT("Say Something")));if (Context.Pin != nullptr){if (Context.Pin->Direction == EGPD_Input && Context.Pin->ParentPin == nullptr){Context.MenuBuilder->AddMenuEntry(FText::FromString(TEXT("Remove Word")),FText::FromString(TEXT("Remove Word from input")),FSlateIcon(),FUIAction(FExecuteAction::CreateUObject(this, &UBPNode_SaySomething::RemoveInputPin, const_cast<UEdGraphPin*>(Context.Pin))));}}// end of ifContext.MenuBuilder->EndSection(); }這個函數的實現很直白啦,就是操作MenuBuilder,添加菜單項,并綁定UIAction到成員函數UBPNode_SaySomething::RemoveInputPin,接下來就是實現這個函數了。
void UBPNode_SaySomething::RemoveInputPin(UEdGraphPin * Pin) {FScopedTransaction Transaction(FText::FromString("SaySomething_RemoveInputPin"));Modify();ArgPinNames.Remove(Pin->GetFName());RemovePin(Pin);FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint()); }也很簡單,就是直接調用父類的RemovePin(),并同步處理一下自己內部的狀態變量就好了。
實現這個藍圖節點的編譯
通過前面的步驟,藍圖編輯器的擴展就全部完成了,接下來就是最后一步了,通過擴展藍圖編譯過程來實現這個節點的實際功能。
我們延續上篇的思路來實現這個節點的功能,也就是重載UK2Node::ExpandNode()函數。
核心的問題是如何把當前的所有的輸入的Pin組合起來? 答案很簡單,把所有輸入的Pin做成一個TArray<>,這樣就可以傳入到一個UFunction來調用。
首先我們在 class UMyBlueprintFunctionLibrary 中添加一個函數:
UCLASS() class MYBLUEPRINTNODE_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary {GENERATED_BODY()public:UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))static void SaySomething_Internal(const TArray<FString>& InWords); };然后,仍然與上篇相同,使用一個 class UK2Node_CallFunction 節點實例對象來調用這個UFunction,不同的是,我們需要使用一個 class UK2Node_MakeArray 節點的實例來把收集所有的動態生成的輸入Pin。下面是實現的代碼:
void UBPNode_SaySomething::ExpandNode(FKismetCompilerContext & CompilerContext, UEdGraph * SourceGraph) {Super::ExpandNode(CompilerContext, SourceGraph);UEdGraphPin* ExecPin = GetExecPin();UEdGraphPin* ThenPin = GetThenPin();if (ExecPin && ThenPin) {// create a CallFunction nodeFName MyFunctionName = GET_FUNCTION_NAME_CHECKED(UMyBlueprintFunctionLibrary, SaySomething_Internal);UK2Node_CallFunction* CallFuncNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);CallFuncNode->FunctionReference.SetExternalMember(MyFunctionName, UBPNode_SaySomething::StaticClass());CallFuncNode->AllocateDefaultPins();// move exec pinsCompilerContext.MovePinLinksToIntermediate(*ExecPin, *(CallFuncNode->GetExecPin()));CompilerContext.MovePinLinksToIntermediate(*ThenPin, *(CallFuncNode->GetThenPin()));// create a "Make Array" node to compile all argsUK2Node_MakeArray* MakeArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this, SourceGraph);MakeArrayNode->AllocateDefaultPins();// Connect Make Array output to function argUEdGraphPin* ArrayOut = MakeArrayNode->GetOutputPin();UEdGraphPin* FuncArgPin = CallFuncNode->FindPinChecked(TEXT("InWords"));ArrayOut->MakeLinkTo(FuncArgPin);// This will set the "Make Array" node's type, only works if one pin is connected.MakeArrayNode->PinConnectionListChanged(ArrayOut);// connect all arg pin to Make Array inputfor (int32 i = 0; i < ArgPinNames.Num(); i++) {// Make Array node has one input by defaultif (i > 0)MakeArrayNode->AddInputPin();// find the input pin on the "Make Array" node by index.const FString PinName = FString::Printf(TEXT("[%d]"), i);UEdGraphPin* ArrayInputPin = MakeArrayNode->FindPinChecked(PinName);// move input word to array UEdGraphPin* MyInputPin = FindPinChecked(ArgPinNames[i], EGPD_Input);CompilerContext.MovePinLinksToIntermediate(*MyInputPin, *ArrayInputPin);}// end of for}// break any links to the expanded nodeBreakAllNodeLinks(); }核心步驟來講解一下:
結束語
今天涉及到的class稍微有點多,我整理了一個UML靜態結構圖,看看這幾個classes直接的關系以及它們所在的模塊。完整源代碼仍然是在我的GitHub:https://github.com/neil3d/UnrealCookBook/tree/master/MyBlueprintNode
至此,通過派生class UK2Node和class SGraphNodeK2Base來擴展Blueprint Graph Editor,我們可以自己定義藍圖節點,以及編輯器中的Node Widget,可以添加按鈕,以及其他任何你想要做的東西。通過這個定制化的Node Widget,可以實現編輯時對Blueprint Graph Node的交互控制。至此,我們已經掌握了最強大的藍圖節點的擴展方法。動態添加Pin這個問題說明白之后,下篇將寫什么呢?先賣個關子,且待下回分解吧~
總結
以上是生活随笔為你收集整理的深入Unreal蓝图开发:自定义蓝图节点(中)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络虚拟化技术
- 下一篇: 深度|华为的产品质量与可靠性是如何炼成的