【UE4】UMG 生命週期
【UE4】UMG 生命週期
參考資料&原文連結
什麼是生命週期
以下摘自百度百科:
生命週期就是指一個物件的生老病死。
生命週期(Life Cycle)的概念應用很廣泛,特別是在政治、經濟、環境、技術、社會等諸多領域經常出現,其基本涵義可以通俗地理解為“從搖籃到墳墓”(Cradle-to-Grave)的整個過程。對於某個產品而言,就是從自然中來回到自然中去的全過程,也就是既包括製造產品所需要的原材料的採集
、加工等生產過程,也包括產品貯存、運輸等流通過程,還包括產品的使用過程以及產品報廢或處置等廢棄回到自然過程,這個過程構成了一個完整的產品的生命週期。
生命週期簡單來說就是一個物件從出生到死亡的全過程。
比如:剛來到這個世界上要和世界說「Hello,World!」的嬰兒,再到後來長高一點變成了小朋友,還有能認識很多和他一樣的小朋友,這將是他們最快樂的童年時光;接下來他們會開始學習很多的知識,某一天他們會悄悄的進入青春期,變成想要探索整個世界的少年;也許會在讀中學的時候因為某一件小事而種下了一顆種子,並在心裡暗暗發誓一定朝這個方向努力;而有的人直到大學過了大半都是渾渾噩噩,不知未來不知去向;大學畢業工作後才知道生活原來如此艱難,每一個人都是勇敢的鬥士,包括自己,只有少部分人依然堅持著自己的夢想,不畏艱險;直到工作幾年後,大部分少年已經被磨平了稜角,再無年少時的意氣風發,他們只想有一個屬於自己的家;人至中年,四十而立,上有父母下有子女,頂著巨大的壓力努力的撐起一個家。
UMG也一樣,UMG也有建立、Tick、銷燬的一個過程,這個過程就是一個完整的生命週期。
為什麼我們要關注生命週期
因為我們希望正確的事情總是在正確的時機發生。
例如:我們對人類幼崽的期望就是希望他能健康平安,在他讀書的時候我們又希望他能夠好好學習,努力讀書,在他合適的時候又希望他能帶女朋友回家。
一樣的,在UMG的生命週期中我們也希望在UMG的生命週期的各個階段中能夠做一些我們期望的事情。
例如:在操作這個控制元件之前,我至少要拿到這個控制元件的物件或者引用,不然隨便一行與這個控制元件有關的程式碼都會讓程式崩潰。
UMG生命週期簡單介紹
以下內容來自:GCCONF '20:UE4で作成するUIと最適化手法
先從藍圖開始,藍圖稍微簡單一些:
Widget生命週期的五個步驟:
UMG生命週期流程圖解
Create
Create -> 開始生成過程 -> 與UObject相關的UObject的生成 -> Initialize只調用一次的初始化。
注意:
- 為什麼這裡要強調「Initialize只調用一次的初始化」呢,因為在這之前的有些過程還會反覆呼叫,比如說在PIE裡面在儲存/編譯/開啟Widget時就會反覆呼叫,在遊戲裡面只會呼叫一次。
- 這裡只是建立,並沒有新增到視口,新增到視口以後才會真正的顯示到螢幕上面。
AddToViewport
Tick
Remove
Destroy
UMG生命週期方法測試
新建測試檔案
和前面一樣,我整了個測試用的UI,名為「UWC_TestUI」,繼承於「UUserWidget」。
public:
//一個控制元件,測試BindWidget是多久在哪個生命週期呼叫賦值的
UPROPERTY(Meta = (BindWidget))
UTextBlock * Txt_Main = nullptr;
public:
//所關注的生命週期的方法
virtual bool Initialize() override;
virtual void NativeOnInitialized() override;
virtual void AddToScreen(ULocalPlayer* LocalPlayer, int32 ZOrder) override;
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
virtual void RemoveFromParent() override;
virtual void NativeDestruct() override;
virtual void BeginDestroy() override;
virtual void FinishDestroy() override;
裡面簡單列印了一下:
bool UWC_TestUI::Initialize()
{
UE_LOG(LogTemp,Warning,TEXT("UWC_TestUI:Initialize Txt_Main is nullptr : %d"),Txt_Main == nullptr ? true : false);
return Super::Initialize();
}
void UWC_TestUI::NativeOnInitialized()
{
UE_LOG(LogTemp,Warning,TEXT("UWC_TestUI:NativeOnInitialized Txt_Main is nullptr : %d"),Txt_Main == nullptr ? true : false);
Super::NativeOnInitialized();
}
//other function...
為什麼我會選擇這些方法?
這幾乎是UMG生命週期有關的所有方法了,藍圖裡面的某些Event事件實際上是在C++程式碼裡面呼叫的。
例如:藍圖中的EventPreConstruct
它實際上是由NativePreConstruct()
呼叫的:
void UUserWidget::NativePreConstruct()
{
PreConstruct(IsDesignTime());
}
而關注藍圖或者關注C++都是一樣的,只不過我們關注的是C++程式碼比較多一點。這個會在下文分析。
為什麼會有一個Txt_Main
來佔坑?
因為也想順便探尋一下BindWidget這個方法是在生命週期的哪個實際繫結的。
藍圖裡面就放了一個Text,提示一下:
接下來測試:
我就弄簡單一點,在關卡藍圖裡面寫了,按下E的時候建立並新增到視口,按下C的時候從視口移除。
Game中的生命週期
可以看到,在Game時,UMG宣告週期走的流程是:
bool Initialize()
-> void NativeOnInitialized()
-> void AddToScreen(ULocalPlayer* LocalPlayer, int32 ZOrder)
-> void NativePreConstruct()
-> void NativeConstruct()
-> void NativeTick()
-> void RemoveFromParent()
-> void NativeDestruct()
還可以觀察到:Txt_Main
這個TextBlock的繫結是在bool Initialize()
或void NativeOnInitialized()
方法繫結的,盲猜是在前者。
PIE中的生命週期
PIE又分為很多情況,在UMG開啟時、新增/拖拽/刪除/Widget時、編譯Widget時、儲存Widget時。這些都會走不同的生命週期方法。
PIE-UMG開啟時
在PIE中,UMG開啟時依次呼叫的是:bool Initialize()
-> void NativePreConstruct()
。
PIE-UMG新增控制元件時
在PIE中,UMG新增控制元件時依次呼叫的是:bool Initialize()
-> void NativePreConstruct()
。
PIE-UMG拖拽控制元件時
在PIE中,UMG拖拽控制元件時依次呼叫的是:bool Initialize()
-> void NativePreConstruct()
。
PIE-UMG刪除控制元件時
在PIE中,UMG刪除時依次呼叫的是:bool Initialize()
-> void NativePreConstruct()
。
PIE-UMG修改控制元件內容時
在修改Widget內容時,居然什麼都沒呼叫 - -。直到點選編譯才有了呼叫。
PIE-UMG編譯時
在PIE中,UMG編譯時依次呼叫的是:void BeginDestroy()
-> void RemoveFromParent()
-> void FinishDestroy()
-> bool Initialize()
-> void NativePreConstruct()
。
其中,前面四個會反覆呼叫。
PIE-UMG儲存時
在PIE中,UMG儲存時依次呼叫的是:bool Initialize()
。
UUserWidget有關原始碼淺析
Initialize
bool UUserWidget::Initialize()
{
// If it's not initialized initialize it, as long as it's not the CDO, we never initialize the CDO.
if (!bInitialized && !HasAnyFlags(RF_ClassDefaultObject))
{
bInitialized = true;
// If this is a sub-widget of another UserWidget, default designer flags and player context to match those of the owning widget
if (UUserWidget* OwningUserWidget = GetTypedOuter<UUserWidget>())
{
#if WITH_EDITOR
SetDesignerFlags(OwningUserWidget->GetDesignerFlags());
#endif
SetPlayerContext(OwningUserWidget->GetPlayerContext());
}
UWidgetBlueprintGeneratedClass* BGClass = Cast<UWidgetBlueprintGeneratedClass>(GetClass());
if (BGClass)
{
BGClass = GetWidgetTreeOwningClass();
}
// Only do this if this widget is of a blueprint class
if (BGClass)
{
BGClass->InitializeWidget(this);
}
else
{
InitializeNativeClassData();
}
if ( WidgetTree == nullptr )
{
WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
}
else
{
WidgetTree->SetFlags(RF_Transient);
const bool bReparentToWidgetTree = false;
InitializeNamedSlots(bReparentToWidgetTree);
}
if (!IsDesignTime() && PlayerContext.IsValid())
{
NativeOnInitialized();
}
return true;
}
return false;
}
- 注意
bInitialized
這個值在構造的時候都是false
,只有在bool Initialize()
在回改變這個值,標識此Widget已經初始化,防止二次呼叫。
- 繼續看,下面這幾行程式碼說了如果當前例項化的UI不是widget藍圖,而是一個C++ class,在觸發
Initialize()
之後,還會繼續觸發InitializeNativeClassData()
,否則只觸發Initialize()
UWidgetBlueprintGeneratedClass* BGClass = Cast<UWidgetBlueprintGeneratedClass>(GetClass());
if (BGClass)
{
BGClass = GetWidgetTreeOwningClass();
}
// Only do this if this widget is of a blueprint class
if (BGClass)
{
BGClass->InitializeWidget(this);
}
else
{
InitializeNativeClassData();
}
InitializeNativeClassData()
宣告在UUserWidget
中是這樣的:
protected:
/** The function is implemented only in nativized widgets (automatically converted from BP to c++) */
virtual void InitializeNativeClassData() {}
- 最後觸發了
NativeOnInitialized();
。 - 在這個函式中你可以獲得藍圖中的控制元件,像這樣寫:
bool UWC_TestUI::Initialize()
{
//防止出現空指標異常
if (!Super::Initialize())
{
return false;
}
if (UWidgetSwitcher* WS_TabContentTemp = Cast<UWidgetSwitcher>(GetWidgetFromName(TEXT("WS_TabContent"))))
{
WS_TabContent = WS_TabContentTemp;
}
return true;
}
其中,WS_TabContent
的宣告是:
class UWidgetSwitcher* WS_TabContent;
或者在NativeOnInitialized()
/NativeConstruct()
中寫。
除了在這裡獲得控制元件,你也可以在void NativeConstruct()
中獲得控制元件,方式是一樣的。
或者你可以直接用BindWidget
這個巨集,然後什麼都不用做了,更方便:
UPROPERTY(Meta = (BindWidget))
class UWidgetSwitcher* WS_TabContent;
參考這裡:虛幻官方文件-UMWidget::UPROPERTY 巨集的有效元資料關鍵字。
NativeOnInitialized
// Native handling for SObjectWidget
void UUserWidget::NativeOnInitialized()
{
OnInitialized();
}
這裡可以看到,只是呼叫了一下給藍圖提供的函式OnInitialized();
,它的宣告是這樣的:
/**
* Called once only at game time on non-template instances.
* While Construct/Destruct pertain to the underlying Slate, this is called only once for the UUserWidget.
* If you have one-time things to establish up-front (like binding callbacks to events on BindWidget properties), do so here.
*/
UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic, Category="User Interface")
void OnInitialized();
這裡註釋說的很清楚了,繫結事件可以在這裡做,比如Button的OnClick、OnHovered之類的。
AddToScreen
檢視宣告,發現需要一個ULocalPlayer*
和一個層級。
/** Adds the widget to the screen, either to the viewport or to the player's screen depending on if the LocalPlayer is null. */
virtual void AddToScreen(ULocalPlayer* LocalPlayer, int32 ZOrder);
其實AddToViewport、AddToPlayerScreen的本質都是呼叫的AddToScreen:
void UUserWidget::AddToViewport(int32 ZOrder)
{
AddToScreen(nullptr, ZOrder);
}
bool UUserWidget::AddToPlayerScreen(int32 ZOrder)
{
if ( ULocalPlayer* LocalPlayer = GetOwningLocalPlayer() )
{
AddToScreen(LocalPlayer, ZOrder);
return true;
}
FMessageLog("PIE").Error(LOCTEXT("AddToPlayerScreen_NoPlayer", "AddToPlayerScreen Failed. No Owning Player!"));
return false;
}
它們的區別是:
AddToScreen需要手動傳遞一個ULocalPlayer*和一個ZOrder進去,這指定了新增到哪個Player上和Widget的層級。
AddToPlayerScreen只需要一個ZOrder,是因為它會在方法裡面使用GetOwningLocalPlayer()嘗試獲取ULocalPlayer*,獲取成功就呼叫AddToScreen以新增,否則就報錯。
AddToViewport只需要一個ZOrder,ULocalPlayer*傳的是一個nullptr。
NativePreConstruct
這個裡面也是呼叫了給藍圖提供的事件:
void UUserWidget::NativePreConstruct()
{
PreConstruct(IsDesignTime());
}
PreConstruct的宣告是:
/**
* Called by both the game and the editor. Allows users to run initial setup for their widgets to better preview
* the setup in the designer and since generally that same setup code is required at runtime, it's called there
* as well.
*
* **WARNING**
* This is intended purely for cosmetic updates using locally owned data, you can not safely access any game related
* state, if you call something that doesn't expect to be run at editor time, you may crash the editor.
*
* In the event you save the asset with blueprint code that causes a crash on evaluation. You can turn off
* PreConstruct evaluation in the Widget Designer settings in the Editor Preferences.
*/vs
UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic, Category="User Interface")
void PreConstruct(bool IsDesignTime);
說的很清楚了。
- 第一,在Game和Editor中都會呼叫,以預覽在設計器中的設定。
- 如果你呼叫了一些不期望在編輯器執行的東西,你可能會崩潰編輯器。
如果你想了解更多的話,請點選這裡:TODO
void NativeConstruct
void UUserWidget::NativeConstruct()
{
Construct();
UpdateCanTick();
}
這個方法就很簡單了,也是調一下給藍圖的事件,然後更新一下是否能Tick,能Tick的話就開始Tick,否則就不Tick。
再看一眼Construct()
;
/**
* Called after the underlying slate widget is constructed. Depending on how the slate object is used
* this event may be called multiple times due to adding and removing from the hierarchy.
* If you need a true called-once-when-created event, use OnInitialized.
*/
UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic, Category="User Interface", meta=( Keywords="Begin Play" ))
void Construct();
這是提供給藍圖的節點,因為會呼叫多次,所以如果有需要真正的建立時呼叫一次的事件,請使用OnInitialized()
。
void NativeTick
這個沒什麼好說的,就是每幀呼叫。不過要注意的是這一行:
SObjectWidget and UUserWidget have mismatching tick states or UUserWidget::NativeTick was called manually (Never do this)
RemoveFromParent
/**
* Removes the widget from its parent widget. If this widget was added to the player's screen or the viewport
* it will also be removed from those containers.
*/
virtual void RemoveFromParent() override;
從其父小部件中刪除小部件。如果這個小部件被新增到玩家的螢幕或視口,它也將從這些容器中刪除。
NativeDestruct
void UUserWidget::NativeDestruct()
{
StopListeningForAllInputActions();
Destruct();
}
這裡面停止了輸入監聽,比如監聽鍵盤和滑鼠的輸入,可以在這裡面搜尋InputComponent
以檢視相關程式碼。
/**
* Called when a widget is no longer referenced causing the slate resource to destroyed. Just like
* Construct this event can be called multiple times.
*/
UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic, Category="User Interface", meta=( Keywords="End Play, Destroy" ))
void Destruct();
這個Widget不再被引用時呼叫。一樣的,會被呼叫多次。
總結
Widget生命週期的五個步驟
在Game中
bool Initialize()
-> void NativeOnInitialized()
-> void AddToScreen(ULocalPlayer* LocalPlayer, int32 ZOrder)
-> void NativePreConstruct()
-> void NativeConstruct()
-> void NativeTick()
-> void RemoveFromParent()
-> void NativeDestruct()
在PIE中
UMG開啟:bool Initialize()
-> void NativePreConstruct()
。
UMG新增控制元件:bool Initialize()
-> void NativePreConstruct()
。
UMG拖拽控制元件:bool Initialize()
-> void NativePreConstruct()
。
UMG刪除控制元件:bool Initialize()
-> void NativePreConstruct()
。
修改Widget內容,什麼都沒呼叫,點選編譯才有呼叫。
UMG編譯:void BeginDestroy()
-> void RemoveFromParent()
-> void FinishDestroy()
-> bool Initialize()
-> void NativePreConstruct()
。
其中,前面四個會反覆呼叫。
UMG儲存:bool Initialize()
。
獲得控制元件和繫結事件
BindWidget
這個巨集是在Initialize()
中完成繫結的。
繫結控制元件也可以手動在Initialize()
或NativeOnInitialized()
或NativeConstruct()
中做。
繫結事件在NativeOnInitialized()
中做。不過要注意需要先得到控制元件再繫結事件,並且繫結的那個函式要加上UFUNCTION()
巨集。
在PIE中實時預覽自定義UMG
重寫Initialize()
:初始化並獲得控制元件,別忘了避免空指標異常。
重寫NativePreConstruct()
:這裡寫預覽相關的程式碼,例如設定顏色、字型等。
詳情請點這裡:TODO
本文標籤
遊戲開發
、遊戲開發基礎
、Unreal Engine
、UE4 使用者介面
、UE4 UMG
、UMG基礎
。