1. 程式人生 > 其它 >【UE4】UMG 生命週期

【UE4】UMG 生命週期

【UE4】UMG 生命週期

參考資料&原文連結

GCCONF '20:UE4で作成するUIと最適化手法

【UE4】使用UMG建立UI,瞭解內部機制及相應優化方法

貓でも分かるUMG

UMG widget構造初始化函式中獲取其內部元件

什麼是生命週期

以下摘自百度百科:

生命週期就是指一個物件的生老病死。

生命週期(Life Cycle)的概念應用很廣泛,特別是在政治經濟環境技術、社會等諸多領域經常出現,其基本涵義可以通俗地理解為“從搖籃到墳墓”(Cradle-to-Grave)的整個過程。對於某個產品而言,就是從自然中來回到自然中去的全過程,也就是既包括製造產品所需要的原材料的採集

、加工等生產過程,也包括產品貯存、運輸等流通過程,還包括產品的使用過程以及產品報廢或處置等廢棄回到自然過程,這個過程構成了一個完整的產品的生命週期。

生命週期簡單來說就是一個物件從出生到死亡的全過程。

比如:剛來到這個世界上要和世界說「Hello,World!」的嬰兒,再到後來長高一點變成了小朋友,還有能認識很多和他一樣的小朋友,這將是他們最快樂的童年時光;接下來他們會開始學習很多的知識,某一天他們會悄悄的進入青春期,變成想要探索整個世界的少年;也許會在讀中學的時候因為某一件小事而種下了一顆種子,並在心裡暗暗發誓一定朝這個方向努力;而有的人直到大學過了大半都是渾渾噩噩,不知未來不知去向;大學畢業工作後才知道生活原來如此艱難,每一個人都是勇敢的鬥士,包括自己,只有少部分人依然堅持著自己的夢想,不畏艱險;直到工作幾年後,大部分少年已經被磨平了稜角,再無年少時的意氣風發,他們只想有一個屬於自己的家;人至中年,四十而立,上有父母下有子女,頂著巨大的壓力努力的撐起一個家。

UMG也一樣,UMG也有建立、Tick、銷燬的一個過程,這個過程就是一個完整的生命週期。

為什麼我們要關注生命週期

因為我們希望正確的事情總是在正確的時機發生。

例如:我們對人類幼崽的期望就是希望他能健康平安,在他讀書的時候我們又希望他能夠好好學習,努力讀書,在他合適的時候又希望他能帶女朋友回家。

一樣的,在UMG的生命週期中我們也希望在UMG的生命週期的各個階段中能夠做一些我們期望的事情。

例如:在操作這個控制元件之前,我至少要拿到這個控制元件的物件或者引用,不然隨便一行與這個控制元件有關的程式碼都會讓程式崩潰。

UMG生命週期簡單介紹

以下內容來自:GCCONF '20:UE4で作成するUIと最適化手法

【UE4】使用UMG建立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 EngineUE4 使用者介面UE4 UMGUMG基礎