前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity 极简UI框架

Unity 极简UI框架

作者头像
汐夜koshio
发布2020-05-08 14:58:08
1.3K0
发布2020-05-08 14:58:08
举报
文章被收录于专栏:U3DU3D

写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基类:

代码语言:javascript
复制
 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     protected HudView HudView;
11     private void Awake()
12     {
13         Canvas = GetComponent<Canvas>();
14         HudView = GetComponent<HudView>();
15     }
16 
17     private void Start()
18     {
19         InitState();
20         Enter(() => AddListeners());
21     }
22 
23     private void OnDestroy() => RemoveListeners();
24 
25     protected virtual void InitState() { }
26 
27     protected virtual void AddListeners() { }
28 
29     protected virtual void RemoveListeners() { }
30 
31     protected void Enter(UnityAction complete) => Canvas.FadeIn(Root, () => complete());
32 
33     protected void ExitTo(string sceneName) => Canvas.FadeOut(Root, () => SceneManager.LoadScene(sceneName));
34 
35     protected void UpdateView<T>(T t) where T : HudView => t.Refresh();
36 }

View基类:

代码语言:javascript
复制
1 using UnityEngine;
2 
3 public class HudView : MonoBehaviour
4 {
5     public virtual void Refresh() { }
6 }

View只有一个自带的更新视图的通用方法,数据来源则直接取游戏底层即可,能够从Ctrl中直接调用View视图的更新。

其他通用的UI方法则全部写在一个统一的地方,例如淡入淡出的函数,向外部发送事件,侦听事件等,这里统一写成了Canvas的扩展方法,便于在基类中也方便直接调用:

代码语言:javascript
复制
 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://cloud.tencent.com/developer/article/1601315

具体的用法如下:(Ctrl)

代码语言:javascript
复制
 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:

代码语言:javascript
复制
 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 }

在上面的例子中,用到了动态添加EventTrigger侦听的扩展方法:(看了下网上的很多写法都有些问题,要不就是不判断列表中有没有同类型的就直接往里塞,要不就是判断了之后发现没有同类型的实例化一个不添加侦听就放进去)

代码语言:javascript
复制
 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     }
代码语言:javascript
复制
    public static T GetOrAddComponent<T>(this GameObject obj) where T : Component => obj.GetComponent<T>() ? obj.GetComponent<T>() : obj.AddComponent<T>();

调用的时候可以进行as转换类型来使用,这样就可以取到对应子类的值了:

代码语言:javascript
复制
1     private void OnDrag(BaseEventData data)
2     {
3         var d = data as PointerEventData;
4         Debug.Log(d.dragging);
5     }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-04-30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档