Unity 極簡UI框架

寫ui的時候一般追求控制邏輯和顯示邏輯分離,經典的類似於MVC,其餘大多都是這個模式的衍生,實際上書寫的時候M是在整個遊戲的底層,我更傾向於將它稱之為D(Data)而不是M(Model),而C(Ctrl)負責接收用戶的各類UI事件,例如點擊,滑動,還有其他遊戲邏輯板塊發過來的事件或消息,處理這些消息並更新V(View)當中的各類顯示數據,這裡更新數據的方式可以抽象為兩種:

1.外部事件觸發View更新,這時不用在意底層數據更新,因為在刷新View之前這些改變的數據可以在其他邏輯版塊中直接更新完。

2.UI內部點擊,滑動等事件觸發View更新,這種情況下有可能需要更新底層數據,但最好不要直接修改和調用,而是選擇向外部發送事件和消息的方式來告知外部需要更新數據。

無論是上面兩種情況中的哪一種,都不是View直接參与外部邏輯聯繫,而是藉助中間的Ctrl來聯繫,Ctrl中處理UI與外部對接的所有邏輯,並能夠及時的更新View。

 

再來分析下Ctrl,我們發現Ctrl的控制流程是可以固定下來,抽象如下:

1.進入一個View介面之前,得到View組件,初始化View中各個元素的狀態

2.播放一段進入動畫,例如淡入

3.進入動畫播放完成後,對View中的一些元素添加事件偵聽,或對外部的一些事件添加偵聽

4.當偵聽中的事件觸發後,可以選擇是否對View更新,或向外部發送事件,消息

5.同樣的,離開時播放一段動畫,例如淡出

6.離開動畫播放完成後,移除所有事件偵聽,載入一個新的View或場景

 

定義Ctrl基類:

 1 using UnityEngine;
 2 using UnityEngine.Events;
 3 using UnityEngine.SceneManagement;
 4 
 5 [RequireComponent(typeof(Canvas))]
 6 public class HudBase : MonoBehaviour
 7 {
 8     public GameObject Root;
 9     protected Canvas Canvas;
10     private void Awake()
11     {
12         Canvas = GetComponent<Canvas>();
13     }
14 
15     private void Start()
16     {
17         InitState();
18         Enter(() => AddListeners());
19     }
20 
21     private void OnDestroy() => RemoveListeners();
22 
23     protected virtual void InitState() { }
24 
25     protected virtual void AddListeners() { }
26 
27     protected virtual void RemoveListeners() { }
28 
29     protected void Enter(UnityAction complete) => Canvas.FadeIn(Root, () => complete());
30 
31     protected void ExitTo(string sceneName) => Canvas.FadeOut(Root, () => SceneManager.LoadScene(sceneName));
32 
33     protected T GetViweComponent<T>() where T : HudView => GetComponent<T>();
34 
35     protected void UpdateView<T>(T t) where T : HudView => t.Refresh();
36 }

View基類:

1 using UnityEngine;
2 
3 public class HudView : MonoBehaviour
4 {
5     public virtual void Refresh() { }
6 }

View只有一個自帶的更新視圖的通用方法,數據來源則直接取遊戲底層即可,能夠從Ctrl中直接調用View視圖的更新。

 

其他通用的UI方法則全部寫在一個統一的地方,例如淡入淡出的函數,向外部發送事件,偵聽事件等,這裡統一寫成了Canvas的擴展方法,便於在基類中也方便直接調用:

 1 using System.Collections.Generic;
 2 using UnityEngine;
 3 using UnityEngine.UI;
 4 using UnityEngine.Events;
 5 using DG.Tweening;
 6 
 7 public static class HudHelper
 8 {
 9     //UI
10     public static void FadeIn(this Canvas canvas, GameObject target, TweenCallback action)
11     {
12         var cg = target.GetOrAddComponent<CanvasGroup>();
13         cg.alpha = 0;
14         cg.DOFade(1, .3f).OnComplete(action);
15     }
16 
17     public static void FadeOut(this Canvas canvas, GameObject target, TweenCallback action)
18     {
19         var cg = target.GetOrAddComponent<CanvasGroup>();
20         cg.DOFade(0, .3f).OnComplete(action);
21     }
22 
23     public static void SendEvent<T>(this Canvas canvas, T e) where T : GameEvent => EventManager.QueueEvent(e);
24 
25     public static void AddListener<T>(this Canvas canvas, EventManager.EventDelegate<T> del) where T : GameEvent => EventManager.AddListener(del);
26 
27     public static void RemoveListener<T>(this Canvas canvas, EventManager.EventDelegate<T> del) where T : GameEvent => EventManager.RemoveListener(del);
28 
29     public static void ButtonListAddListener(this Canvas canvas, List<Button> buttons, UnityAction action)
30     {
31         foreach (var bt in buttons)
32         {
33             bt.onClick.AddListener(action);
34         }
35     }
36 
37     public static void ButtonListRemoveListener(this Canvas canvas, List<Button> buttons)
38     {
39         foreach (var bt in buttons)
40         {
41             bt.onClick.RemoveAllListeners();
42         }
43     }
44 }

關於事件隊列可以詳細見之前的隨筆:

//www.cnblogs.com/koshio0219/p/11209191.html

具體的用法如下:(Ctrl)

 1 using UnityEngine;
 2 
 3 public class MapCanvasCtrl : HudBase
 4 {
 5     private MapCanvasView View;
 6     public GameObject UnderPanel;
 7 
 8     protected override void InitState()
 9     {
10         View = GetViweComponent<MapCanvasView>();
11         UnderPanel.SetActive(false);
12         UpdateView(View);
13     }
14 
15     protected override void AddListeners()
16     {
17         View.Back.onClick.AddListener(() => ExitTo("S_Main"));
18 
19         Canvas.ButtonListAddListener(View.TaskPoints, () =>
20         {
21             UnderPanel.SetActive(true);
22             Canvas.FadeIn(UnderPanel, () => View.HitOut.onClick.AddListener(() => ExitTo("S_DemoBattle")));
23         });
24     }
25 
26     protected override void RemoveListeners()
27     {
28         View.HitOut.onClick.RemoveAllListeners();
29         Canvas.ButtonListRemoveListener(View.TaskPoints);
30     }
31 }

只需要重寫以上三個方法即可。

(View):

 1 using System.Collections.Generic;
 2 using UnityEngine.UI;
 3 using TMPro;
 4 
 5 public class MapCanvasView : HudView
 6 {
 7     public TextMeshProUGUI Resource;
 8     public TextMeshProUGUI Difficulty;
 9     public TextMeshProUGUI Reward;
10 
11     public Button HitOut;
12     public Button Back;
13 
14     public List<Button> TaskPoints = new List<Button>();
15 
16     public override void Refresh()
17     {
18         //這裡更新視圖
19     }
20 }