關於Plugin的官方介紹:
本篇文章主要是記錄目前我在CommonUI plugin中所看到的一些東西,可能分析的不是很完整,但就是一個簡單的筆記。
版本:Unreal 5.0.3
目前plugin裡面主要有以下幾個功能:
- 各別平台的Input以及 Icon Mapping:例如xbox跟switch的A跟B是相反的,他有機制處理輸入跟icon的對應。
- Styling: 例如CommonText可以做一個Style BP給大家用,之後調整遊戲文字就只需要調該style BP就行了
- 從GameViewportClient上做Input Rereroute,根據目前是ECommonInputMode::Menu、Game或All決定裡面的行為。在功能面上看起來跟InputMode在機制上類似,Menu對應UIOnly、Game對應GameOnly而All對應GameAndUI,不確定之後官方會想怎麼發展。
- UI Manager的機制。
我自己看是覺得整套機制還不是很成熟,在使用上你會覺得缺少很多細節的實作。不過也對,他現在是放在experimental plugin下面,要使用在production上要有心理準備自己需要去補完很多缺少的實作。
關於UI Manager,他實現的機制比較特別,我自己的Plugin是用Actor,但他是拿你第一個AddToViewport的主Widget來當管理器,這時候你會需要在這個Widget上再刻一套需要的功能介面好讓其他系統呼叫。
整套機制主要分為2個元素構成:
- Widget Container容器:分為UCommonActivatableWidgetStack跟UCommonActivatableWidgetQueue,分別可以進行push跟pop操作,這二個容器的差別在於我們的設計是先進先出或後進先出。我們可以有多個Stack分別對應不同的需求,在Lyra中他是把Game跟Popup Modal拆成二個stack
- 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主要會經過以下幾個生命週期:
- Enter:視是不是處於VR模式決定初始化Widget的方式,並呼叫StartTransIn。
- StartTransIn:呼叫TransIn動畫,會根據設定在Scene上設定的名字,在UserWidget上找到對應的Animation播放,同時關閉Widget HitTest。
- OnEnter:當StartTransIn動畫播放完畢後,會自動進入這個階段,會把Widget HitTest打開,讓使用者可以跟上面的按鈕互動,然後進到TickScene階段。
- TickScene:這個階段會持續直到有人透過SceneManager操作想要把Scene從Stack上移除。
- Exit:呼叫TransOut動畫,一樣會去找Scene上設定的名字,在UserWidget上找到對應的Animation播放,同時關閉Widget HitTest。若TransIn跟TransOut的名字相同,則會呼叫PlayAnimationReverse。
- OnExit:當TransOut動畫完畢,進入這個階段後就會做一些clean up的動作。
大部份遊戲都會需要在這些生命週期中間分別實作一些Gameplay的邏輯,因此每個Scene提供以下幾個Delegate供使用者綁定:
- OnTopToBack:當某個TopScene上面被疊了其他Scene的時候觸發。
- OnBackToTop:當某個Scene上面的其他Scene被移除,自己變成了TopScene的時候觸發。
- OnStartTransIn
- OnTransInFinished
- OnStartTransOut
- 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變身成浮空對話框。如下圖:
有時候我們會想要在遊戲一開始的時候或者在任務完成的時候,依條件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