寫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 6 public class HudBase : MonoBehaviour 7 { 8 public GameObject Root; 9 protected Canvas Canvas; 10 protected HudView HudView; 11 private void Awake() 12 { 13 Canvas = GetComponentInParent<Canvas>(); 14 HudView = GetComponent<HudView>(); 15 } 16 17 private void Start() => InitState(); 18 19 private void OnEnable() => Enter(() => AddListeners()); 20 21 private void OnDisable() => 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 void UpdateView<T>(T t) where T : HudView => t.Refresh(); 34 }
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 }
關於事件隊列可以詳細見之前的隨筆:
https://www.cnblogs.com/koshio0219/p/11209191.html
具體的用法如下:(Ctrl)
1 using UnityEngine; 2 using UnityEngine.EventSystems; 3 4 public class MapCanvasCtrl : HudBase 5 { 6 private MapCanvasView View; 7 public GameObject UnderPanel; 8 9 protected override void InitState() 10 { 11 //將基類的View轉化為對應子類 12 View = HudView as MapCanvasView; 13 UnderPanel.SetActive(false); 14 UpdateView(View); 15 } 16 17 protected override void AddListeners() 18 { 19 View.Back.onClick.AddListener(() => ExitTo("S_Main")); 20 21 Canvas.AddTriggerListener(View.Map, EventTriggerType.Drag, OnDrag); 22 23 Canvas.ButtonListAddListener(View.TaskPoints, () => 24 { 25 UnderPanel.SetActive(true); 26 Canvas.FadeIn(UnderPanel, () => View.HitOut.onClick.AddListener(() => ExitTo("S_DemoBattle"))); 27 }); 28 } 29 30 private void OnDrag(BaseEventData data) 31 { 32 //將基類的Data轉化為對應子類 33 var d = data as PointerEventData; 34 Debug.Log(d.dragging); 35 } 36 37 protected override void RemoveListeners() 38 { 39 View.HitOut.onClick.RemoveAllListeners(); 40 Canvas.ButtonListRemoveListener(View.TaskPoints); 41 Canvas.RemoveTriggerListener(View.Map); 42 } 43 }
只需要重寫以上三個方法即可。注意初始化時將基類的View轉為對應子類使用,使用關鍵字as。
對應的具體View:
1 using UnityEngine.UI; 2 3 public class MapCanvasView : HudView 4 { 5 public UpBoxView UpBoxView; 6 7 public Button HitOut; 8 public Button Back; 9 10 public Image Map; 11 12 public List<Button> TaskPoints = new List<Button>(); 13 14 public override void Refresh() 15 { 16 UpBoxView.Refresh(); 17 //Do something else... 18 } 19 }
當然了,大的View中也可能嵌套小的View,這樣可以更為方便的將一些零散的UI控件隨意的插入到其他View中,例如一般游戲中頂部的角色基礎信息欄等:
1 using TMPro; 2 3 public class UpBoxView : HudView 4 { 5 public TextMeshProUGUI Name; 6 public TextMeshProUGUI Resource; 7 public TextMeshProUGUI Level; 8 9 public override void Refresh() 10 { 11 var d = GameData.Instance.PlayerData; 12 Level.text = "Lv. " + d.lv.ToString(); 13 Resource.text = d.ResourcePoint.ToString(); 14 Name.text = "咕嚕靈波"; 15 } 16 }
在上面的例子中,用到了動態添加EventTrigger偵聽的擴展方法:(看了下網上的很多寫法都有些問題,要不就是不判斷列表中有沒有同類型的就直接往里塞,要不就是判斷了之后發現沒有同類型的實例化一個不添加偵聽就放進去)
1 public static void AddTriggerListener(this Canvas canvas, Component obj, EventTriggerType type, UnityAction<BaseEventData> action) 2 { 3 //先看有沒有對應組件沒有就加上 4 var trigger = obj.gameObject.GetOrAddComponent<EventTrigger>(); 5 //再看看觸發列表中有沒有事件,沒有就新建一個列表 6 if (trigger.triggers == null || trigger.triggers.Count == 0) 7 trigger.triggers = new List<EventTrigger.Entry>(); 8 //再看事件列表中是不是已經存在對應類型的值,如果存在的話簡單直接給那個事件加個偵聽就好 9 foreach (var e in trigger.triggers) 10 { 11 if (e.eventID == type) 12 { 13 e.callback.AddListener(action); 14 return; 15 } 16 } 17 //到這里就是很遺憾沒有對應類型的事件,那就實例化一個新的,注意實例化完了以后還要把對應的事件類型和回調設定進去 18 var entry = new EventTrigger.Entry(); 19 entry.eventID = type; 20 entry.callback.AddListener(action); 21 //全部設定好了再加進去,要不然沒有效果知道么 22 trigger.triggers.Add(entry); 23 }
public static T GetOrAddComponent<T>(this GameObject obj) where T : Component => obj.GetComponent<T>() ? obj.GetComponent<T>() : obj.AddComponent<T>();
調用的時候可以進行as轉換類型來使用,這樣就可以取到對應子類的值了:
1 private void OnDrag(BaseEventData data) 2 { 3 var d = data as PointerEventData; 4 Debug.Log(d.dragging); 5 }
2020年5月25日更新:
1.在刷新視圖時可傳入不定類型和個數的參數。
在實際使用的過程中發現,不傳遞參數有時刷新視圖比較困難,但不同的View又會根據不同的需要傳遞類型和個數均不同的參數,這時就想到了使用params object[] parameters作為參數進行傳遞。
1 using UnityEngine; 2 3 public class HudView : MonoBehaviour 4 { 5 public virtual void Refresh(params object[] parameters) { } 6 }
1 /// <summary> 2 /// 刷新視圖 3 /// </summary> 4 /// <typeparam name="T">視圖類型</typeparam> 5 /// <param name="t">視圖實例</param> 6 /// <param name="parameters">不定類型和個數的參數</param> 7 protected void UpdateView<T>(T t, params object[] parameters) where T : HudView => t.Refresh(parameters);
因為C#中所有的類型都繼承自object,通過裝箱和拆箱就可以傳遞任何類型的參數,這樣寫非常簡潔,但缺點是對於性能會有一點的損失;如果你實在不想損失性能,也可以考慮用多種不同類型的泛型參數作為替代。
1 protected void UpdateView<T0, T1>(T0 t0, T1 t1) where T0 : HudView => t0.Refresh(t1); 2 protected void UpdateView<T0, T1, T2>(T0 t0, T1 t1, T2 t2) where T0 : HudView => t0.Refresh(t1, t2); 3 protected void UpdateView<T0, T1, T2, T3>(T0 t0, T1 t1, T2 t2, T3 t3) where T0 : HudView => t0.Refresh(t1, t2, t3); 4 protected void UpdateView<T0, T1, T2, T3, T4>(T0 t0, T1 t1, T2 t2, T3 t3, T4 t4) where T0 : HudView => t0.Refresh(t1, t2, t3, t4);
這樣的缺點就是,寫起來會很繁瑣,需要多少參數就要加幾個不同類型的函數,但因為不需要頻繁的轉換類型,性能來講是較優解。
2.可以靈活控制切換View的函數。
有時我們不僅僅希望只用FadeIn或者FadeOut來進入或退出頁面,而是可以自定義各種切換的方式,這是就需要用到委托作為參數了。
1 /// <summary> 2 /// 切換頁面 3 /// </summary> 4 /// <param name="obj">目標</param> 5 /// <param name="way">切換方式委托</param> 6 /// <param name="complete">切換完成后委托</param> 7 protected void Shift(GameObject obj, UnityAction<GameObject, TweenCallback> way, UnityAction complete) => way(obj, () => complete());
當然了,這樣的話我們就需要規定所有的切換方式的函數參數類型和個數都需要與way保持一致。
補充——關於類型的判斷與轉換:
使用params object[] parameters在實際函數實現的過程中需要判斷傳入的參數類型是否符合當前預期,可以通過下來的方式來進行具體判斷,例如:
1 if (parameters.Length > 0 && parameters[0] is int) 2 { 3 Debug.Log((int)parameters[0]); 4 }
1 if (parameters.Length > 0 && parameters[0].GetType() == typeof(int)) 2 { 3 Debug.Log((int)parameters[0]); 4 }
當然了,如果是引用類型,除了強制類型轉換之外還可以使用關鍵字as進行裝換,上面的例子中已經有了就不再舉例了。
但如果是泛型的話是不能直接執行強制類型轉換的,但還是可以先轉換為object類型,再執行強制類型轉換:
1 private void Test<T>(T t) 2 { 3 if (t is int) 4 { 5 object temp = t; 6 Debug.Log((int)temp); 7 } 8 //or 9 if (typeof(T) == typeof(int)) 10 { 11 object temp = t; 12 Debug.Log((int)temp); 13 } 14 }