通過本系列文章上篇的介紹,我們已經可以創建一個“沒什麼用”的藍圖節點了。要想讓它有用,關鍵還是上篇中說的典型應用場景:動態添加Pin,這篇博客就來解決這個問題。
目標
和上篇一樣,我還將通過一個儘量簡單的節點,來說明"可動態添加Pin的藍圖節點"的實現過程,讓大家儘量聚焦在“藍圖自定義節點”這個主題上。
設想這樣一個節點:Say Something,把輸入的N個字符串連接起來,然後打印輸出。也就是說,這個節點的輸入Pin是可以動態添加的。我們將在上篇的那個工程基礎上實現這個自定義節點。最終實現的效果如下圖所示:
下面我們還是來仔細的過一遍實現步驟吧!
創建Blueprint Graph節點類型
首先,我們還是需要創建一個class UK2Node的派生類,這個過程在上篇中已經詳細說過了,照單炒菜,很容易就創建了下圖這樣一個空的自定義節點,這裏就不贅述了。不清楚的話,可以返回去在照着上篇做就好了。
創建自定義的節點Widget
我們要動態增加Pin的話,需要在節點上顯示一個"加號按鈕",點擊之後增加一個“input pin”。這就不能使用默認的Blueprint Graph Node Widget了,需要對其進行擴展。這個擴展的思路和前面一樣,也是找到特定的基類,重載其虛函數即可,這個基類就是class SGraphNodeK2Base。我們要重載的兩個核心的函數是:
- CreateInputSideAddButton(),創建我們需要的添加輸入Pin的按鈕;
- OnAddPin(),響應這個按鈕的操作;
來看一下最簡化的代碼吧:
SGraphNodeSaySomething.h
class SGraphNodeSaySomething : public SGraphNodeK2Base
{
public:
SLATE_BEGIN_ARGS(SGraphNodeSaySomething){}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, UBPNode_SaySomething* InNode);
protected:
virtual void CreateInputSideAddButton(TSharedPtr<SVerticalBox> InputBox) override;
virtual FReply OnAddPin() override;
};
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,上面我們已經創建了兩個類,分別是:
- class UBPNode_SaySomething : public UK2Node
- class SGraphNodeSaySomething : public SGraphNodeK2Base
下面我們就需要讓藍圖編輯器知道:創建UBPNode_SaySomething對象的時候,需要使用SGraphNodeSaySomething這個Widget。
添加自定義Node Widget的兩種方式(參見引擎源碼class FNodeFactory):
- 重載UEdGraphNode::CreateVisualWidget()函數,例如:
TSharedPtr<SGraphNode> UNiagaraNode::CreateVisualWidget()
{
return SNew(SNiagaraGraphNode, this);
}
- 使用 class FEdGraphUtilities 註冊 class FGraphPanelNodeFactory對象,例如:
void FBehaviorTreeEditorModule::StartupModule()
{
GraphPanelNodeFactory_BehaviorTree = MakeShareable( new FGraphPanelNodeFactory_BehaviorTree() );
FEdGraphUtilities::RegisterVisualNodeFactory(GraphPanelNodeFactory_BehaviorTree);
}
在這裏,我們使用第一種方式,也就是在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 if
Context.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 node
FName 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 pins
CompilerContext.MovePinLinksToIntermediate(*ExecPin, *(CallFuncNode->GetExecPin()));
CompilerContext.MovePinLinksToIntermediate(*ThenPin, *(CallFuncNode->GetThenPin()));
// create a "Make Array" node to compile all args
UK2Node_MakeArray* MakeArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this, SourceGraph);
MakeArrayNode->AllocateDefaultPins();
// Connect Make Array output to function arg
UEdGraphPin* 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 input
for (int32 i = 0; i < ArgPinNames.Num(); i++) {
// Make Array node has one input by default
if (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 node
BreakAllNodeLinks();
}
核心步驟來講解一下:
- 創建了一個class UK2Node_CallFunction的實例,然後把自身節點的兩端的Exec Pin重定向到這個Node的兩端;
- 使用“函數參數名稱”找到UK2Node_CallFunction節點的輸入Pin,把它連接到一個新建的UK2Node_MakeArray的節點實例上;
- 把自己所有的輸入變量Pin重定向到UK2Node_MakeArray的輸入上(需要爲它動態添加新的Pin);
結束語
今天涉及到的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這個問題說明白之後,下篇將寫什麼呢?先賣個關子,且待下回分解吧~