摘要
:虛幻引擎雖然內建了許多的藍圖節點,能夠用於實現各種各樣的功能,但仍然會遇到內建節點無法滿足需求的情形。K2Node藍圖節點是引擎一種自定義高階藍圖節點的機制,用於擴充套件藍相簿。本教程以K2Node_ArraySort藍圖節點實現過程為例,旨在說明K2Node節點的建立規則和要求,並且介紹如何給藍圖節點的引腳新增下拉列表。
關鍵字
:虛幻引擎;藍圖節點;K2Node;自定義引腳;
Chapter 9 Design and Implementation of Custom K2Node Blueprint Node
Abstract
:Although Unreal Engine has many built-in blueprint nodes that can implement various functions, it will still encounter the situation that these engine nodes cannot meet the needs。 The K2Node is a mechanism for the engine to customize advanced blueprint nodes, which is used to expand the Blueprint system。 This tutorial takes the implementation process of the K2Node_ArraySort node as an example to illustrate the rules and requirements for creating the K2Node node, and introduces how to add a drop-down list to the pins of K2Node node。
1.Overview
本教程以K2NodeArraySort實現過程為例,簡要的說明如何在UE4/5中實現一個深度定製的K2Node藍圖節點,實現對泛型陣列進行快速排序(QuickSort);特別說明:以下程式碼使用UE5 EA版引擎實現。
2. Introduction
眾所周知,虛幻引擎(Unreal Engine)擁有強大的藍圖系統(Blueprint System),利用引擎內建的藍圖節點(Node)可以建立各式各樣的功能;但是藍圖系統也有不足,比如常常會碰到內建的藍圖節點並沒有我們想要功能的情況。這時候就需要用UE C++來自定義藍圖節點,將用C++寫好函式暴露給藍圖系統,藍圖編輯器中各式各樣的藍圖節點也都是來源於此。藍圖編輯器中的絕大多數藍圖節點都是在編寫相應的C++程式碼時,透過UFUNCTION宏暴露出來的,這些藍圖節點的屬性(如:引腳(Pin)、名稱(Title))在藍圖編輯器中基本是不可改變的。K2Node節點是虛幻引擎另一種建立藍圖節點的機制, 與一般使用UFUNCTION宏編寫藍圖節點不同,派生於UK2Node類的藍圖節點可以在藍圖編輯器中完成各種各樣的功能,如:給藍圖節點的引腳新增下拉列表,動態修改藍圖節點引腳型別,增/減引腳個數等。引擎中具有代表性的UK2Node_GetDataTableRow類實現的GetDataTableRow藍圖節點,它不僅能夠根據DataTable引腳的值動態改變輸出引腳OutRow的型別,同時也能夠給輸入引腳RowName新增下拉列表,列舉RowName變數的可選值,以及動態改變節點名稱。曾經在我剛開始接觸虛幻引擎的那段時間,看到這個節點都感到非常的不可思議,感慨藍圖功能的強大,也為實現K2Node藍圖節點摸索良久。
GetDataTableRow藍圖節點
迴歸主題,為什麼要寫K2Node_ArraySort節點?早在去年本人曾在第1期 在藍圖實現任意型別陣列排序,介紹了使用藍圖宏方法,實現選擇排序(Select Sort)和氣泡排序(Bubble Sort)二個版本的泛型陣列排序藍圖宏節點;隨後又在第2期 UE4 C++實現任意型別陣列藍圖排序節點中介紹如何使用UFUNCTION宏建立泛型陣列排序節點,同樣是使用氣泡排序演算法。再後來對其中排序演算法進行改進,利用快速排序(Quick Sort)實現更為高效的泛型陣列排序節點,詳情見個人部落格:UE4 C++實現任意型別陣列藍圖排序節點(Quick Sort)。為了更好的使用這個藍圖節點,打算對該藍圖節點進一步的迭代,仿照GetDataTableRow節點也給這個藍圖節點新增一個下拉列表,列舉待排序陣列成員的型別為結構體和UObject派生類物件時可排序變數名稱。同時也為了深入瞭解引擎自定義K2Node節點的機制。原始碼的較長,難以面面俱到的講解每段程式碼的含義,以下內容將主要介紹其中的緣由,詳細的原始碼見文末連結。
3. Step by step
(1) 建立外掛並手動新增一個編輯器模組
一般來說,自定義K2Node節點實現的功能具有通用性,可複用,因此將K2Node節點程式碼封裝到外掛之中更為合理。首先使用空模板建立一個名為AwesomeBlueprints的外掛,此時這個外掛的Source目錄下包含了一個與外掛同名的預設模組(AwesomeBlueprints模組)。
建立AwesomeBlueprints外掛
建立名為AwesomeBlueprintsEditor的編輯器模組:複製AwesomeBlueprints外掛目錄下Source/AwesomeBlueprints資料夾(Crtl +滑鼠拖拽),修改複製後的
資料夾
、
build。cs
、 。
h檔案
、
。cpp
檔案的名稱,把所有
AwesomeBlueprints
全部替換為
AwesomeBlueprintsEditor
(文字工具:查詢->替換),修改的位置包括檔名、模組名,模組類名等。雖然這個手動建立的模組的名稱可以任取,但是一般為了方便表明二個模組之間的關聯性,直接命名對應{Runtime模組名}+Editor。
手動建立模組
隨後得到如下目錄結構:
包含二個模組的外掛AwesomeBlueprints檔案目錄
將新模組
AwesomeBlueprintsEditor
新增到AwesomeBlueprints。uplugin檔案中如下所示位置:
“Modules”
:
[
{
“Name”
:
“AwesomeBlueprints”
,
“Type”
:
“Runtime”
,
“LoadingPhase”
:
“Default”
},
{
“Name”
:
“AwesomeBlueprintsEditor”
,
“Type”
:
“UncookedOnly”
,
“LoadingPhase”
:
“Default”
}
]
重新執行Generate Visual Studio File,並編譯程式碼,在該外掛中就會有二個模組,一個Runtime模組(AwesomeBlueprints),一個Editor模組(AwesomeBlueprintsEditor);以上準備工作就以完成。特別注意:其中AwesomeBlueprintsEditor模組型別(Type)應當設定為“UncookedOnly”,否則在編譯藍圖時會遇到如下警告。
The node ‘ K2 Array Sort ’ is from an Editor Only module, but is placed in a runtime blueprint! K2 Nodes should only be defined in a Developer or UncookedOnly module。
(2)以UK2Node類為父類,建立類UK2Node_ArraySort子類
在這裡遵循引擎對K2Node派生類的命名規則
K2Node_{FunctionName}
,將其命名為K2Node_ArraySort。在進行下一步之前,先簡單分析這個K2Node_ArraySort節點程式碼的存在的模組位置。UK2Node_ArraySort類定義的是Array_Sort藍圖節點在藍圖編輯器中的行為,故而這個類的程式碼應該屬於編輯器模組(
AwesomeBlueprintsEditor
)中的程式碼。
建立K2Node_ArraySort類
由於打包之後,並不包含編輯模組的程式碼,所以同編寫普通的藍圖節點一樣,這裡我們仍需要把真正完成陣列排序功能的程式碼放到Runtime模組(
AwesomeBlueprints
)。如上一步類似,以UBlueprintFunctionLibrary基類,建立UAwesomeFunctionLibrary類,並選擇
AwesomeBlueprints
模組
4. Source Code Insight
AwesomeBlueprintsEditor。build。cs依賴的模組
PrivateDependencyModuleNames
。
AddRange
(
new
string
[]
{
“CoreUObject”
,
“Engine”
,
“Slate”
,
“SlateCore”
,
“KismetCompiler”
,
“UnrealEd”
,
“BlueprintGraph”
,
“GraphEditor”
,
“AwesomeBlueprints”
,
// 。。。 add private dependencies that you statically link with here 。。。
}
);
此處只要知道UE4模組的匯出機制,很容易理解為什麼新增這些模組,不做過多介紹。
重點介紹為藍圖引腳建立下拉列表原理:
首先簡要的闡述下,GetDataTableRow節點中RowName引腳如何產生下拉列表。如下圖呼叫堆疊所示,在藍圖編輯器中,點選GetDataTableRow節點的Datatable引腳時,首先從ContentBrowser中獲得所有DataTable資源;當我們選擇其中一個DataTable時,呼叫
UK2Node_GetDataTableRow::PinDefaultValueChanged()
函式,其中
UK2Node_GetDataTableRow::RefreshOutputPinType()
更新OutRow引腳的型別,
UK2Node_GetDataTableRow::RefreshRowNameOptions()
函式呼叫
Graph->NotifyGraphChanged()
,通知藍圖系統該藍圖圖表Graph發生改變,需重新繪製。
選擇DataTable資源後執行過程
當重新繪製藍圖圖表時,依次建立 節點(SGraphNode),引腳(SGraphPin),節點和引腳均為一個個Slate元素,如下圖所示
繪製藍圖節點執行過程
若當前正在建立的引腳為GetDataTableRow節點的RowName引腳,並且DataTable值非空,則返回
SGraphPinDataTableRowName
,其中
SGraphPinDataTableRowName
的父類之一就是SGraphPin類。
建立SGraphPinDataTableRowName的過程
而上圖中的
FBlueprintGraphPanelPinFactory
類在FBlueprintEditor啟動模組時註冊到FEdGraphUtilities類中
BlueprintEditor模組StartupModule函式
綜上分析:我們在AwesomeBlueprintsEditor模組中增加了
SGraphPinArraySortPropertyName
類和
FK2BlueprintGraphPanelPinFactory
類,並在AwesomeBlueprintsEditor模組啟動時,將
FK2BlueprintGraphPanelPinFactory
註冊到FEdGraphUtilities中。
void
FAwesomeBlueprintsEditorModule
::
StartupModule
()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the 。uplugin file per-module
TSharedPtr
<
FK2BlueprintGraphPanelPinFactory
>
BlueprintGraphPanelPinFactory
=
MakeShareable
(
new
FK2BlueprintGraphPanelPinFactory
());
FEdGraphUtilities
::
RegisterVisualPinFactory
(
BlueprintGraphPanelPinFactory
);
}
在完成
UK2Node_ArraySort
、
SGraphPinArraySortPropertyName
、
FK2BlueprintGraphPanelPinFactory
三個類的程式碼時,即可到K2Node_ArraySort藍圖節點的所有功能。
K2Node_ArraySort的PropertyName引腳下拉列表資料來源:當TargetArray引腳連線到結構體UStruct陣列或者UObject派生陣列時,迭代結構體UStruct或者UObjectClass的所有屬性,將其中可用於排序的屬性名新增到數組裡。
TArray
<
FString
>
UK2Node_ArraySort
::
GetPropertyNames
()
{
TArray
<
FString
>
PropertyNames
;
UEdGraphPin
*
TargetArrayPin
=
GetTargetArrayPin
();
if
(
TargetArrayPin
&&
TargetArrayPin
->
LinkedTo
。
Num
()
>
0
)
{
UEdGraphPin
*
LinkArrayPin
=
TargetArrayPin
->
LinkedTo
[
0
];
// The type of inner property of TargetArrayPin is structure
if
(
UScriptStruct
*
InnerStruct
=
Cast
<
UScriptStruct
>
(
LinkArrayPin
->
PinType
。
PinSubCategoryObject
。
Get
()))
{
for
(
TFieldIterator
<
const
FProperty
>
It
(
InnerStruct
);
It
;
++
It
)
{
const
FProperty
*
BaseProp
=
*
It
;
if
(
const
FEnumProperty
*
EnumProp
=
CastField
<
const
FEnumProperty
>
(
BaseProp
))
{
PropertyNames
。
Add
(
BaseProp
->
GetAuthoredName
());
}
else
if
(
const
FNumericProperty
*
NumProp
=
CastField
<
const
FNumericProperty
>
(
BaseProp
))
{
PropertyNames
。
Add
(
BaseProp
->
GetAuthoredName
());
}
}
}
// The type of inner property of TargetArrayPin is Blueprint & C++ class
else
if
(
UClass
*
InnerObjClass
=
Cast
<
UClass
>
(
LinkArrayPin
->
PinType
。
PinSubCategoryObject
。
Get
()))
{
for
(
TFieldIterator
<
const
FProperty
>
It
(
InnerObjClass
);
It
;
++
It
)
{
const
FProperty
*
BaseProp
=
*
It
;
if
(
const
FEnumProperty
*
EnumProp
=
CastField
<
const
FEnumProperty
>
(
BaseProp
))
{
PropertyNames
。
Add
(
BaseProp
->
GetAuthoredName
());
}
else
if
(
const
FNumericProperty
*
NumProp
=
CastField
<
const
FNumericProperty
>
(
BaseProp
))
{
PropertyNames
。
Add
(
BaseProp
->
GetAuthoredName
());
}
}
}
}
return
PropertyNames
;
}
5. Test & Usage
ArraySort在藍圖編輯下測試
此外外掛中還將第3期 ue4 泛型藍圖節點的實現及應用例項 的不同型別(基礎型別,FArrayProperty, FMapProperty, FSetProperty)的萬用字元引腳合併在一起,寫成二個節點
Get Property Value By Name
和
Set Property Value By Name
,使其同時Array,Map,Set的變數。
Get/Set Object Property Value By Name 泛型節點
6. Conclusion
本文以K2Node_ArraySort節點實現過程為例,著重介紹了建立模組,以及K2Node節點的引腳新增下拉列表的過程。不足之處:K2Node_ArraySort節點的PropertyName引腳下拉列表生成過程重複呼叫了
GetPropertyNames()
迭代陣列元素型別,應當新增快取機制。一家之言:將K2Node_ArraySort節點與UFUNCTION版實現的ArraySort節點對比,不難發現K2Node版節點雖然增加了一些功能,但這些功能都屬於Editor狀態下的功能,並沒有改變Runtime狀態時的程式碼執行過程,也就不會提升執行效率。但是對於
GetProperyValueByName
和
SetProperyValueByName
二個節點來說,能夠將不同型別的萬用字元引腳合併,使用K2Node方式實現該功能藍圖節點,大大減少了功能重複的節點,確有其價值。
原始碼:
7.Addition
這一期加上前幾期,分別介紹了CustomThunk泛型藍圖節點、多分支執行流非同步藍圖節點、K2Node藍圖節點的實現過程,基本上涵蓋了自定義藍圖節點的絕大數內容,估計以後不會再像之前那樣介紹藍圖節點了。距離上一次更新已經過了很久,因為這一年接手的是純UE4 C++專案,所以幾乎沒有用藍圖寫什麼程式碼。目前自己更傾向於用UE4 C++寫外掛/模組,方便程式碼管理和引擎升級,也在考慮該使用UE做些什麼樣的東西。ps。 隨緣更新。