UE4 TAttribute原理 與 Slate資料繫結
「貧乳是稀有價值!」——泉此方
最近想學習一下TAttribute的機制,上網衝浪卻幾乎找不到一篇關於它的教學文章,無奈之下只好自己讀完原始碼後來寫一篇。
先說是啥:
TAttribute這東西簡單概括一下就是個一種
儲存了值和獲取該值的委託
的結構。當委託為空的時候則近似退化為只有儲存一個值這個功能的智慧指標
然後說用來幹啥:
TAttribute基本只在寫Slate時會用到,用途為進行資料繫結——表現形式為遊戲中的任意資料發生改變時都可以實時地,動態地直接在UI中反映出來。
比如我在玩家的角色藍圖裡寫了個隨時間線性增長的浮點數,TAttribute的委託就能獲取到這個值,並實時反饋給Slate的控制元件們。如下圖所示:顏色和數值都可以隨之動態變化。
同理,像是控制元件用的圖片,筆刷,尺寸框與螢幕的間距,只要是在原始碼中以TAttribute形式宣告的屬性,都可以進行資料繫結。
這個功能我們之前在藍圖(UMG)裡其實也見過:
接下來我會開始講解怎麼在C++中對Slate也實現這種資料繫結的功能,然後講一下它背後的機制TAttribute。
(然後讀者就會發現用UMG進行資料繫結比起用C++,又快又簡單)
1.利用TAttribute進行資料繫結
寫過Slate的讀者都知道,STextBlock顯示的文字是Text,SImage顯示的圖片的顏色是ColorAndOpacity。為了便於說明,我們就拿這兩個進行資料繫結的演示。
1。首先建立一個新的Slate控制元件,在標頭檔案中定義兩個TAttribute,並寫上他們要繫結的Get函式。
class
SANDBOX_API
SLearningWidget
:
public
SCompoundWidget
{
public
:
SLATE_BEGIN_ARGS
(
SLearningWidget
)
{}
SLATE_END_ARGS
()
/** Constructs this widget with InArgs */
void
Construct
(
const
FArguments
&
InArgs
);
public
:
//過會兒要傳給控制元件的屬性。
TAttribute
<
FText
>
TestText
;
TAttribute
<
FSlateColor
>
TestColor
;
public
:
//上面兩個TAttribute要繫結的Get函式。
//【注意:函式末要加const,否則編譯不透過】
FText
GetText
()
const
;
FSlateColor
GetTestColor
()
const
;
};
2。然後先實現一下兩個Get函式,這裡也是為了圖省事,把資料放在了玩家角色物件身上。在實際應用時資料不一定來自玩家身上,可能來自任何物件中。
實際決定繫結的資料來源是誰的,就是這兩個Get函式。現在它們暫時還沒用,下一步我們把這兩個Get函式繫結給TAttribute後就能派上用場了。(實際上繫結給的是TAttribute持有的委託成員,這個我們在第2節的原理篇講到)
//我們在玩家角色的標頭檔案中定義了兩個測試資料,TestFloat,TestColorNum。
//如果覺得抽象的話可以想象成這是玩家的血量耐力資料之類的。
FSlateColor
SLearningWidget
::
GetTestColor
()
const
{
UWorld
*
ThisWord
=
GWorld
->
GetWorld
();
if
(
ThisWord
)
{
//不知道為啥,UGameplayStatics::GetPlayerCharacter這個函式在這裡用的時候會返回無效的角色物件
//所以只好出此下策,用迭代器來找到玩家物件了= =,讀者反正只要能拿到玩家物件就行。
for
(
TObjectIterator
<
ASlAiPlayerCharacter
>
It
;
It
;
++
It
)
{
if
(
It
->
TestColorNum
!=
0
)
//這個if主要是為了避免迭代器找到的是預設物件
{
float
TestColorNumber
=
It
->
TestColorNum
;
//RGB的綠色給我調到0了,因此文首的演示gif中圖示的顏色是偏紫色的。
return
FSlateColor
(
FLinearColor
(
TestColorNumber
,
0
,
TestColorNumber
));
}
}
}
return
FSlateColor
(
FLinearColor
(
1。f
,
1。f
,
1。f
));
}
//大同小異。
FText
SLearningWidget
::
GetText
()
const
{
UWorld
*
ThisWord
=
GWorld
->
GetWorld
();
if
(
ThisWord
)
{
for
(
TObjectIterator
<
ASlAiPlayerCharacter
>
It
;
It
;
++
It
)
{
if
(
It
->
TestColorNum
!=
0
)
//這個if主要是為了避免迭代器找到的是預設物件
{
LastNumber
=
It
->
TestFloat
;
//將float測試值轉為FText的型別再返回。
return
FText
::
FromString
(
FString
::
SanitizeFloat
(
It
->
TestFloat
));
}
}
}
return
FText
::
FromString
(
“DefaultString_Left”
);
}
3。在建構函式中把Get函式繫結到各自對應的TAttribute上。
void
SLearningWidget
::
Construct
(
const
FArguments
&
InArgs
)
{
TestText
。
Bind
(
this
,
&
SLearningWidget
::
GetText
);
TestColor
。
Bind
(
this
,
&
SLearningWidget
::
GetTestColor
);
//……暫時省略底下的Slate程式碼
}
4。在例項化控制元件的時候把TAttribute傳給它,資料繫結到這就完成了。
void
SLearningWidget
::
Construct
(
const
FArguments
&
InArgs
)
{
//省略上面給TAttribute的委託繫結函式的程式碼。
//控制元件的圖片和字型需要用Slate控制元件樣式來設定,由於和本篇文章內容關係不大,因此略過不表。
//如何把Slate控制元件渲染到螢幕上也按下不表。
ChildSlot
[
SNew
(
SBox
)
。
WidthOverride
(
500。f
)
。
HeightOverride
(
100。f
)
[
SNew
(
SOverlay
)
+
SOverlay
::
Slot
()
。
HAlign
(
HAlign_Left
)
。
VAlign
(
VAlign_Center
)
[
SNew
(
STextBlock
)
。
Font
(
MenuStyle
->
Font_60
)
//讀者自己整點自己喜歡的字型就行
。
Text
(
TestText
)
//將TAttribute
]
+
SOverlay
::
Slot
()
。
HAlign
(
HAlign_Center
)
。
VAlign
(
VAlign_Center
)
[
SNew
(
SImage
)
。
Image
(
&
MenuStyle
->
CheckBoxBrush
)
//讀者自己整點自己喜歡的圖片就行。
。
ColorAndOpacity
(
TestColor
)
//將TAttribute
]
]
];
}
現在完成了資料繫結,只要玩家角色例項中的TestFloat和TestColorNum改變,Slate中文字框和圖片控制元件中對應的屬性也會改變,我們來在藍圖中寫點資料變化測試一下:
讓這兩個值都2秒增加一次。
效果就像文首的gif一樣了:
接下來要說說這背後的原理所在,以及控制元件的哪些屬性可以進行資料繫結,為什麼,如何判斷。對於原理不怎麼感冒,只是想知道平時怎麼用的讀者,
請務必先點個贊+收藏+喜歡
然後再關閉該文章。
2.TAttribute與Slate之十萬個為什麼
寫過Slate的讀者也許心中都曾經有這樣的疑惑:
Slate控制元件開頭的SLATE_EVENT、SLATE_BEGIN之類的宏到底是幹啥用的?為什麼它們能用來宣告屬性和事件?
為啥用InArgs。_XXX就能拿到這個控制元件被初始化時被傳入的引數?它們是哪來的?
為什麼同樣一份屬性在SLATE_BEGIN宏底下用宏寫過一遍,還得在類的成員中再寫一遍?
以及我們上面提出的問題:一個控制元件中的哪些屬性可以資料繫結,哪些不能?如何判斷?
2.1TAttribute的結構
來簡單看看TAttribute標頭檔案的原始碼:
template
<
typename
ObjectType
>
class
TAttribute
//Attribute可以翻譯為屬性
{
public
:
/**
* Attribute的“Getter”委託
*委託繫結的函式格式必須像這樣:返回值為傳入的模板類ObjectType,函式末加const。
* ObjectType GetValue() const
*
* @返回 Attribute儲存的“值”
*/
DECLARE_DELEGATE_RetVal
(
ObjectType
,
FGetter
);
private
:
/** Attribute儲存的“值”。設定為可變的以便在使用繫結的Getter時能將該值快取到本地。*/
mutable
ObjectType
Value
;
/**這個屬性的繫結成員函式(如果沒有繫結函式,可能是NULL)。
設定後,所有讀取屬性值的嘗試都會呼叫此委託來生成該值。 */
/**我們的屬性‘getter’委託 */
FGetter
Getter
;
}
Attribute存有一個值和一個獲取這個值的委託,它們倆的具體關係,在TAttribute的Create函式處有註釋說明:(每一個用心寫註釋的人都是天使!)
/**
* 透過給Getter委託繫結任意函式來建立屬性,呼叫該函式來根據需要生成此屬性的值。
* 繫結後,屬性將不再有一個可以直接訪問的值,而是繫結到其他資料上的值
* Getter委託在被繫結後將總是被呼叫來生成“值”。
*
* @param InFuncPtr Getter委託所要繫結的成員函式。
*/
static
TAttribute
Create
(
typename
FGetter
::
FStaticDelegate
::
FFuncPtr
InFuncPtr
)
{
const
bool
bExplicitConstructor
=
true
;
return
TAttribute
(
FGetter
::
CreateStatic
(
InFuncPtr
),
bExplicitConstructor
);
}
正如我在文章開頭所說的,在沒給TAttribute的委託繫結函式前,我們可以直接訪問到TAttribute儲存的那個值;如果綁定了,則每次試圖訪問這個值時,TAttribute的委託都會被自動觸發,重新生成一次該值。
TAttribute的Get函式很好的說明了這一點:
const
ObjectType
&
Get
()
const
{
// 如果我們有一個getter委託,我們會呼叫它來生成值
if
(
Getter
。
IsBound
()
)
{
// 呼叫委託來獲取值。
// 安全呼叫(例如物件被刪除,我們可以檢測到)
// 注意:我們故意在這裡覆蓋我們的值副本,以便我們可以透過地址返回值
// 最常見的情況是,屬性根本沒有繫結委託。
Value
=
Getter
。
Execute
();
//呼叫委託
}
//如果沒繫結委託,就直接返回儲存的值。
return
Value
;
}
此外,由於Getter委託是TAttribute的私有成員,因此沒法直接對它繫結,需要TAttribute用公有方法套一層轉發給它。
隨便挑兩個簡單的函式看一眼,剩下的也都差不多,就不贅述了,感性趣的讀者可以自己再去看原始碼。
void
BindStatic
(
typename
FGetter
::
FStaticDelegate
::
FFuncPtr
InFuncPtr
)
{
bIsSet
=
true
;
Getter
。
BindStatic
(
InFuncPtr
);
}
bool
IsBound
()
const
{
return
Getter
。
IsBound
();
}
2.2透過對Slate的原始碼分析來回答十萬個為什麼
寫過Slate的讀者想必對此印象深刻——在Slate控制元件類的開頭,會有一段被
SLATE_BEGIN_ARGS
與
SLATE_END_ARGS
這兩個宏包裹起來的區域,我們可以在裡頭同樣用宏宣告一些東西,事件,屬性,變數……
拿SImage的原始碼舉個例子。
class
SLATECORE_API
SImage
:
public
SLeafWidget
{
public
:
SLATE_BEGIN_ARGS
(
SImage
)
:
_Image
(
FCoreStyle
::
Get
()。
GetDefaultBrush
()
)
,
_ColorAndOpacity
(
FLinearColor
::
White
)
,
_FlipForRightToLeftFlowDirection
(
false
)
{
}
/** Image resource */
SLATE_ATTRIBUTE
(
const
FSlateBrush
*
,
Image
)
/** Color and opacity */
SLATE_ATTRIBUTE
(
FSlateColor
,
ColorAndOpacity
)
/** Flips the image if the localization‘s flow direction is RightToLeft */
SLATE_ARGUMENT
(
bool
,
FlipForRightToLeftFlowDirection
)
/** Invoked when the mouse is pressed in the widget。 */
SLATE_EVENT
(
FPointerEventHandler
,
OnMouseButtonDown
)
SLATE_END_ARGS
()
翻看這些宏的定義,不難發現這一段都只是在建立一個類內結構體——FArguments而已。
#define SLATE_BEGIN_ARGS( WidgetType ) \
public: \
struct FArguments : public TSlateBaseNamedArgs
{ \
typedef FArguments WidgetArgsType; \
FORCENOINLINE FArguments()
把上面那段宏手動展開一下就變成了這樣:(雖然已經儘可能地精簡了,不過還是看起來有點長,是因為我在裡頭用註釋進行說明。當然,願意讀到這裡的,有勇氣與好奇心對原始碼一探究竟的讀者想必也不會因為這點長度就望而卻步www。)
public
:
struct
FArguments
:
public
TSlateBaseNamedArgs
<
SImage
>
{
//這裡有兩個模板引數,一個是WidgetType,一個是WidgetArgsType。
//WidgetType就是SLATE_BEGIN_ARGS宏傳入的引數,這裡是SImage。
//WidgetArgsType是在其基類TSlateBaseNamedArgs中定義的。限於篇幅,還望讀者去看一下原始碼。
typedef
FArguments
SImage
::
FArguments
;
FORCENOINLINE
FArguments
()
:
_Image
(
FCoreStyle
::
Get
()。
GetDefaultBrush
()
)
,
_ColorAndOpacity
(
FLinearColor
::
White
)
,
_FlipForRightToLeftFlowDirection
(
false
)
{
}
//SLATE_ATTRIBUTE宏展開
//去檢視這些宏時會發現使用了很多##——作用是進行字元的連線。
TAttribute
<
const
FSlateBrush
*
>
_Image
;
SImage
::
FArguments
&
Image
(
const
TAttribute
<
const
FSlateBrush
*
>&
InAttribute
)
{
//比如_Image就是從_##AttrName變來的。
_Image
=
InAttribute
;
return
this
->
Me
();
}
//為了詳略得當,同樣是TAttribute的顏色及透明度這裡就不展開了,和影象差不多的。
//SLATE_ARGUMENT宏展開:
bool
_FlipForRightToLeftFlowDirection
;
SImage
::
FArguments
&
FlipForRightToLeftFlowDirection
(
bool
InArg
)
{
_FlipForRightToLeftFlowDirection
=
InArg
;
//this->Me()這東西近似理解為安全版的return this即可
return
this
->
Me
();
}
//SLATE_EVENT展開長達幾百行……這裡實在沒辦法全部展開。
//概括地說,就是聲明瞭一個委託,然後為不同的函式繫結寫了不同的繫結方法。
//聲明瞭一個滑鼠點選委託。(FPointerEventHandler這個型別的委託在WidgetMouseEventsDelegate。h檔案中可以找到它的定義。)
FPointerEventHandler
_OnMouseButtonDown
;
//最簡單的初始化這個委託的方法:用一個已經做好的委託給它賦值。
SImage
::
FArguments
&
OnMouseButtonDown
(
const
FPointerEventHandler
&
InDelegate
)
{
_OnMouseButtonDown
=
InDelegate
;
return
*
this
;
}
//一個繫結1引數的SP函式的方法。
template
<
class
UserClass
,
typename
Var1Type
>
SImage
::
FArguments
&
OnMouseButtonDown
(
UserClass
*
InUserObject
,
typename
FPointerEventHandler
::
template
TSPMethodDelegate_OneVar
<
UserClass
,
Var1Type
>::
FMethodPtr
InFunc
,
Var1Type
Var1
)
{
_OnMouseButtonDown
=
FPointerEventHandler
::
CreateSP
(
InUserObject
,
InFunc
,
Var1
);
return
*
this
;
}
}
在程式碼中打下OnMouseButtonDown,可以看到提供了5個針對不同函式的繫結的方法,當然,看起來只有5個,實際上每一個都有相當多的過載版本……(const與否,引數個數,返回值等等,所以才寫了幾百行)
細心的讀者也許已經注意到了(沒注意到的可以翻上去看宏展開驗證一下):用SLATE_ATTRIBUTE和SLATE_ARGUMENTS宣告屬性時,不僅會宣告屬性本身,還會附贈一個與屬性同名的,用來初始化屬性的函式。
比如Image屬性:
TAttribute
<
const
FSlateBrush
*
>
_Image
;
SImage
::
FArguments
&
Image
(
const
TAttribute
<
const
FSlateBrush
*
>&
InAttribute
)
同樣地,宣告事件的宏也是不僅宣告委託,還附贈一堆用來初始化委託的函式。
再細心一點的讀者還會發現,這些附贈的函式都有一個共同的特點——它們返回值都是SImage::FArguments。也就是其自身。這麼做的用意是啥呢?
回憶一下我們平時初始化一個控制元件時是怎麼做的:
SNew
(
STextBlock
)
。
Font
(
MenuStyle
->
Font_60
)
。
Text
(
TestText3
)
沒錯,這裡的Font和Text實際上都是函式,它們完成了屬性初始化後返回FArguments物件,將其傳遞下去,使得下一行程式碼再用一個“ 。 ”點運算子,就能繼續訪問其他初始化函式。
現在,我們可以開始回答第2節開頭提出的問題了。
1.那一堆宏的用途?
我們已經解釋了——用於宣告屬性,事件,以及對應的初始化它們自身的方法。本來我們自己寫要稍費一番功夫的事,現在只需要用短短的一行宏就能做到。而這背後是UE乾的長達1000多行的髒活累活
(快說,謝謝UE)
最重要的是這麼做使得Slate的宣告與初始化都變得高度規範化了。也更易於理解。
2.為啥用InArgs._XXX就能拿到這個控制元件被初始化時被傳入的引數?它們是哪來的?
InArgs是哪來的?
答案是每個Slate控制元件都會附贈的Construct函式:
void
SLearningWidget
::
Construct
(
const
FArguments
&
InArgs
)
{
ChildSlot
[];
}
至於FArguments,我們上面也解釋過了,標頭檔案裡的那一堆SLATE_EVENTS之類的宏都是在宣告這個FArguments結構。
為啥InArgs有那麼多_xxx?這些不就是我宣告的屬性在前面加了個“ _ ”嗎?
同樣是看上面宏展開部分(或者直接去看SLATE_ATTRIBUTE宏),在宣告FArguments這個結構時,會用“__##AttrName”這樣的語句來宣告屬性。
FArguments的結構宣告是在標頭檔案中用那些宏完成的我知道了,那這個結構裡的屬性是啥時候被初始化的?
這個問題的回答見下面問題3的回答。
3.為什麼同樣一份屬性在SLATE_BEGIN宏底下用宏寫過一遍,還得在類的成員中再寫一遍?
首先必須要明確一下:我們通常寫的這些語句,實際上是在初始化SImage::FArguments或STextBlock::FArguments中的變數們!
SNew
(
SImage
)
。
Image
(
&
MenuStyle
->
CheckBoxBrush
)
。
ColorAndOpacity
(
TestColor
)
SNew
(
STextBlock
)
。
Font
(
MenuStyle
->
Font_60
)
。
Text
(
TestText3
)
用偽程式碼表示它們之間的關係就像這樣:
//假設現在我們有個SFather,Sson。它們沒有繼承關係。
//SFather在Construct函式中這麼寫:
void
SFather
::
Construct
(
const
FArguments
&
InArgs
){
ChildSlot
[
SNew
(
Sson
)
。
TestNumber
(
114514
)
。
TestText
(
TEXT
(
“求關注,求點贊,求收藏,求喜歡”
))
];
}
//那麼我們在Sson的Construct函式中就可以透過這種方式提取出它被初始化時接受的引數:
void
Sson
::
Construct
(
const
FArguments
&
InArgs
)
{
int
AcceptNumber
=
InArgs
。
_TestNumber
;
check
(
InArgs
。
_TestText
==
TEXT
(
“求點贊,求關注,求收藏,求喜歡”
));
//InArgs裡的引數是否用在ChildSlot中都行。
ChildSlot
[];
}
經常可以看見在宏中宣告過一次的屬性,在底下還要再宣告一遍,比如SImage:
class
SLATECORE_API
SImage
:
public
SLeafWidget
{
public
:
SLATE_BEGIN_ARGS
(
SImage
)
:
_Image
(
FCoreStyle
::
Get
()。
GetDefaultBrush
()
)
,
_ColorAndOpacity
(
FLinearColor
::
White
)
,
_FlipForRightToLeftFlowDirection
(
false
)
{
}
SLATE_ATTRIBUTE
(
const
FSlateBrush
*
,
Image
)
SLATE_ATTRIBUTE
(
FSlateColor
,
ColorAndOpacity
)
SLATE_ARGUMENT
(
bool
,
FlipForRightToLeftFlowDirection
)
SLATE_EVENT
(
FPointerEventHandler
,
OnMouseButtonDown
)
SLATE_END_ARGS
()
protected
:
//是吧,一模一樣屬於是。
FInvalidatableBrushAttribute
Image
;
TAttribute
<
FSlateColor
>
ColorAndOpacity
;
bool
bFlipForRightToLeftFlowDirection
;
FPointerEventHandler
OnMouseButtonDownHandler
;
};
那麼現在理由就很清晰了:屬性在宏中的宣告是為了在控制元件自身被初始化時接受引數,而下面在protected中再宣告一次只是為了在自身被初始化完成後,把儲存在FArguments這一結構中的引數換個地方儲存。畢竟
InArgs這東西實際上是Construct函式傳入的引數,在別的函式中用不了
。想用的話就只能宣告一些成員變數來儲存它們。
用一張圖來輔助理解這三者的關係:
4.最後一個問題:一個控制元件中的哪些屬性可以資料繫結,哪些不能?如何判斷?
這個問題的答案想必讀者現在早已瞭然於胸了——以SLATE_ATTRIBUTE形式宣告的才行。所以SImage這個控制元件的Image屬性,ColorAndOpacity屬性可以進行資料繫結,而引數FlipForRightToLeftFlowDirection不行。
結語
如果只是想要告訴讀者判斷哪些屬性可以資料繫結,我只需要直接說結論“
翻看你要建立的子控制元件的原始碼,看它的標頭檔案中開頭那塊,用SLATE_ATTRIBUTE宏宣告的屬性才可以
”就可以了,大可不必講這麼多。很明顯,為了解釋這麼簡單的一句結論,我在2。2節寫了太多超過本文的標題“TAttribute與Slate資料繫結”的內容了。那為啥要這麼做呢?
一是因為@大釗 說的那句
你只是知道了What,How,但是還擋不住別人問一句Why。而功力的提升就在於問一個個why中。
二是因為Slate這塊的原始碼確實讀起來舒服啊!特別是註釋寫的很詳盡,比以前讀的一些整個類讀完沒見到幾行註釋的原始碼強多了。因此也越讀越有興趣,感興趣的東西自然也就更有研究的動力了。
Slate這塊東西比較多,後面我看看有沒有辦法寫一篇專門講Slate結構的文章。(這裡推薦一下 @鍋約科 寫的Slate基本框架系列)
最後,都看到這裡了,如果這篇文章有幫到你或者覺得寫的還行的話,還請各位讀者朋友
點個贊,收個藏,沒關注的也別忘了點個關注
!
卑微作者線上求贊求關注
祝大家新年工作順利,平安健康!
於是我們就下次再見吧~
參考:
UE4 Slate基本框架<1> - 知乎 (zhihu。com)
Template:Slate Data Binding Part 3 - Old UE4 Wiki (nerivec。github。io)
此方:“像老爸這麼廢柴,媽媽怎麼會嫁給你呢?”
泉總次郎:“是啊,我這種廢柴……我唯一的 絕對有自信的地方 那便是——我是這個世界上最愛彼方的人。”