[UnrealEngine][開發日誌] CommonUI plugin 筆記與 Plugin ( HorizonFramework ) 的設計思路

posted in: UnrealEngine, 開發日誌 | 0

關於Plugin的官方介紹:

https://docs.unrealengine.com/5.0/en-US/common-ui-plugin-for-advanced-user-interfaces-in-unreal-engine/

本篇文章主要是記錄目前我在CommonUI plugin中所看到的一些東西,可能分析的不是很完整,但就是一個簡單的筆記。

版本:Unreal 5.0.3

目前plugin裡面主要有以下幾個功能:

  1. 各別平台的Input以及 Icon Mapping:例如xbox跟switch的A跟B是相反的,他有機制處理輸入跟icon的對應。
  2. Styling: 例如CommonText可以做一個Style BP給大家用,之後調整遊戲文字就只需要調該style BP就行了
  3. 從GameViewportClient上做Input Rereroute,根據目前是ECommonInputMode::Menu、Game或All決定裡面的行為。在功能面上看起來跟InputMode在機制上類似,Menu對應UIOnly、Game對應GameOnly而All對應GameAndUI,不確定之後官方會想怎麼發展。
  4. UI Manager的機制。

我自己看是覺得整套機制還不是很成熟,在使用上你會覺得缺少很多細節的實作。不過也對,他現在是放在experimental plugin下面,要使用在production上要有心理準備自己需要去補完很多缺少的實作。

關於UI Manager,他實現的機制比較特別,我自己的Plugin是用Actor,但他是拿你第一個AddToViewport的主Widget來當管理器,這時候你會需要在這個Widget上再刻一套需要的功能介面好讓其他系統呼叫。

整套機制主要分為2個元素構成:

  1. Widget Container容器:分為UCommonActivatableWidgetStack跟UCommonActivatableWidgetQueue,分別可以進行push跟pop操作,這二個容器的差別在於我們的設計是先進先出或後進先出。我們可以有多個Stack分別對應不同的需求,在Lyra中他是把Game跟Popup Modal拆成二個stack
  2. ActivatableWidget:他是一個UserWidget,我們需要繼承下來進行設計的各種UI頁面,用來push進Widget Container的元素。。

他Push跟Pop時內建transition動畫,但他動畫效果是寫死在code中,而不是去呼叫UMG上的動畫,所以不是很符合實際產品製作的需求。內建的Transition動畫效果有:FadeOnly、Horizontal、Vertical、Zoom (ECommonSwitcherTransition)。

由於我以前有解決過UI Manager相關的問題,因此就再來多來聊聊我Plugin的設計思路吧。

以下是我Plugin的連結: Horizon Framework

這個Plugin當初設計的目的是想要做一套通用的遊戲框架(GameFramework),並做為HorizonDialogue擴建的基礎。只是做到後來發現通用框架難以在一個Plugin裡面實現出來,所以目前就變成了專注在UIManager相關的功能上面。

我的UI管理器做叫HorizonSceneManager,而用來進行Push跟Pop的元素叫做HorizonScene,Scene這個名稱是借鏡於cocos2d,他們都繼承自Actor,因此都會在World上生成一份實體。HorizonScene是用來封裝UserWidget,因此我們在設計完UserWidget之後,需要另外創建一份BP繼承自HorizonScene,並把UserWidget的Class指派進Scene的參數中。如下圖:

SceneManager裡面有一份SceneStack跟SceneEventList,裡面有設計一系統的API讓我們可以針對SceneStack進行操作,幾個重要的API為:ChangeScene、PushScene、PopScene、RemoveScene。這幾個Function會建立對應的SceneEvent放進SceneEventList中,SceneManager會在每個Tick的時候逐一拿出來執行,並在適當的時機點通知SceneManager,並把Scene加入進SceneStack中、呼叫對應的delegate通知使用者實作需要的遊戲邏輯。

每個Scene主要會經過以下幾個生命週期:

  1. Enter:視是不是處於VR模式決定初始化Widget的方式,並呼叫StartTransIn。
  2. StartTransIn:呼叫TransIn動畫,會根據設定在Scene上設定的名字,在UserWidget上找到對應的Animation播放,同時關閉Widget HitTest。
  3. OnEnter:當StartTransIn動畫播放完畢後,會自動進入這個階段,會把Widget HitTest打開,讓使用者可以跟上面的按鈕互動,然後進到TickScene階段。
  4. TickScene:這個階段會持續直到有人透過SceneManager操作想要把Scene從Stack上移除。
  5. Exit:呼叫TransOut動畫,一樣會去找Scene上設定的名字,在UserWidget上找到對應的Animation播放,同時關閉Widget HitTest。若TransIn跟TransOut的名字相同,則會呼叫PlayAnimationReverse。
  6. OnExit:當TransOut動畫完畢,進入這個階段後就會做一些clean up的動作。

大部份遊戲都會需要在這些生命週期中間分別實作一些Gameplay的邏輯,因此每個Scene提供以下幾個Delegate供使用者綁定:

  1. OnTopToBack:當某個TopScene上面被疊了其他Scene的時候觸發。
  2. OnBackToTop:當某個Scene上面的其他Scene被移除,自己變成了TopScene的時候觸發。
  3. OnStartTransIn
  4. OnTransInFinished
  5. OnStartTransOut
  6. OnTransOutFinished

我自己最常用的是OnTopToBack跟OnBackToTop這二個,他可以很方便的讓我把某個Scene需要的邏輯集中在同一個地方而不是分散在各處。例如當某片Setting UI開起來的時候想播放一些動畫效果,並在有其他UI蓋上來的時候停止動畫以節省效能,這時候我們可以這樣做:

   auto pSceneManager = UHorizonSceneManagerLibrary::GetDefaultSceneManager(this);
int32 playerIndex = UGameplayStatics::GetPlayerControllerID(GetOwningPlayer());
auto pSceneEvent = pSceneManager->PushSceneByClass(SettingSceneClass, false, playerIndex);
   auto pSettingScene = pSceneEvent->GetTransInScene();
    pSettingScene->OnBackToTopNative.AddWeakLambda(this, [&]() 
    {
         // TODO: Start SettingScene’s Animation
    });
   pSettingScene->OnTopToBackNative.AddWeakLambda(this,[&]() 
   {
       // TODO: Stop SettingScene’s Animation
   });

為什麼我要選擇用Actor對UserWidget進行封裝?

原因有很多,但最大的理由是當初我有在涉略一些VR開發,因此在設計的時候就在思考,該怎麼樣把設計好的一份UserWidget,在不更改任何程式碼與手動調整的前提下,同時能夠在VR跟非VR的世界中使用。使用Actor的話,我就可以掛上WidgetComponent,然後在runtime判斷VR模式有沒有啟用,有啟用的話就把Widget放進WidgetComponent中顯示,沒有的話就根據ControllerID呼叫AddToPlayerScreen或AddToViewport。

到現在,HorizonFramework作為HorizonDialogue的基礎框架,目前已在marketplace上架多年。對於對話場景的處理,我的HorizonDialogueScene是繼承自HorizonScene,因此他內建就支援SceneStack相關的事件操作。我們可以將每一段的對話演出封裝到不同的DialogueScene中,由於Scene上面有WidgetComponent,因此我們的對話就可以同時支援2D跟3D的形式,其2D形式會在VR模式下,利用WidgetComponent變身成浮空對話框。如下圖:

2D對話
3D對話
VR模式的浮空對話框

有時候我們會想要在遊戲一開始的時候或者在任務完成的時候,依條件Push多個DialogueScene,這時候我有設計HorizonDialogueQueueComponent,呼叫裡面的EnqueueDialogue(YourDialogueScene),可以讓系統能夠依序消化而不是一次同時執行所有的對話場景。

最後,CommonUI對於多個Stack的處理是在設計階段手動加Widget,在這裡我是利用生成多個SceneManager的概念來支援,GetSceneManagerWithName會在找不到該名稱的SceneManager時自動創建一個:

UHorizonSceneManagerLibrary::GetSceneManagerWithName(this, “Game”); UHorizonSceneManagerLibrary::GetSceneManagerWithName(this, “Popup”);

結論

UI Manager是個常常被遺忘的議題,尤其是當你的遊戲系統越來越多的時候,怎麼好好管理你的UI的生命週期與移轉的狀態絕對不是個簡單的議題。特別是手遊這種UI特別多的應用。

我們可以看到官方在CommonUI中有試圖加入這方面的功能,我覺得是好事,雖然他跟我自己的Plugin功能有了直接的競爭關係。不過由於官方的版本還不是很成熟,而且官方設計的架構並不符合自己的使用習慣,因此在可見的未來我的這套Plugin應該還是會繼續開發跟維護。

想要著手使用CommonUI Plugin提供的機制開發UI Manager的朋友,或許可以參考我這篇的設計思路,把相關的功能補足後應該就可以更好的應用在自己的遊戲上。

Leave a Reply

Your email address will not be published. Required fields are marked *