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

 using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement; public class HudBase : MonoBehaviour
{
public GameObject Root;
protected Canvas Canvas;
protected HudView HudView;
private void Awake()
{
Canvas = GetComponentInParent<Canvas>();
HudView = GetComponent<HudView>();
} private void Start() => InitState(); private void OnEnable() => Enter(() => AddListeners()); private void OnDisable() => RemoveListeners(); protected virtual void InitState() { } protected virtual void AddListeners() { } protected virtual void RemoveListeners() { } protected void Enter(UnityAction complete) => Canvas.FadeIn(Root, () => complete()); protected void ExitTo(string sceneName) => Canvas.FadeOut(Root, () => SceneManager.LoadScene(sceneName)); protected void UpdateView<T>(T t) where T : HudView => t.Refresh();
}

View基类:

 using UnityEngine;

 public class HudView : MonoBehaviour
{
public virtual void Refresh() { }
}

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

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

 using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using DG.Tweening; public static class HudHelper
{
//UI
public static void FadeIn(this Canvas canvas, GameObject target, TweenCallback action)
{
var cg = target.GetOrAddComponent<CanvasGroup>();
cg.alpha = ;
cg.DOFade(, .3f).OnComplete(action);
} public static void FadeOut(this Canvas canvas, GameObject target, TweenCallback action)
{
var cg = target.GetOrAddComponent<CanvasGroup>();
cg.DOFade(, .3f).OnComplete(action);
} public static void SendEvent<T>(this Canvas canvas, T e) where T : GameEvent => EventManager.QueueEvent(e); public static void AddListener<T>(this Canvas canvas, EventManager.EventDelegate<T> del) where T : GameEvent => EventManager.AddListener(del); public static void RemoveListener<T>(this Canvas canvas, EventManager.EventDelegate<T> del) where T : GameEvent => EventManager.RemoveListener(del); public static void ButtonListAddListener(this Canvas canvas, List<Button> buttons, UnityAction action)
{
foreach (var bt in buttons)
{
bt.onClick.AddListener(action);
}
} public static void ButtonListRemoveListener(this Canvas canvas, List<Button> buttons)
{
foreach (var bt in buttons)
{
bt.onClick.RemoveAllListeners();
}
}
}

关于事件队列可以详细见之前的随笔:

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

具体的用法如下:(Ctrl)

 using UnityEngine;
using UnityEngine.EventSystems; public class MapCanvasCtrl : HudBase
{
private MapCanvasView View;
public GameObject UnderPanel; protected override void InitState()
{
//将基类的View转化为对应子类
View = HudView as MapCanvasView;
UnderPanel.SetActive(false);
UpdateView(View);
} protected override void AddListeners()
{
View.Back.onClick.AddListener(() => ExitTo("S_Main")); Canvas.AddTriggerListener(View.Map, EventTriggerType.Drag, OnDrag); Canvas.ButtonListAddListener(View.TaskPoints, () =>
{
UnderPanel.SetActive(true);
Canvas.FadeIn(UnderPanel, () => View.HitOut.onClick.AddListener(() => ExitTo("S_DemoBattle")));
});
} private void OnDrag(BaseEventData data)
{
//将基类的Data转化为对应子类
var d = data as PointerEventData;
Debug.Log(d.dragging);
} protected override void RemoveListeners()
{
View.HitOut.onClick.RemoveAllListeners();
Canvas.ButtonListRemoveListener(View.TaskPoints);
Canvas.RemoveTriggerListener(View.Map);
}
}

只需要重写以上三个方法即可。注意初始化时将基类的View转为对应子类使用,使用关键字as。

对应的具体View:

 using UnityEngine.UI;

 public class MapCanvasView : HudView
{
public UpBoxView UpBoxView; public Button HitOut;
public Button Back; public Image Map; public List<Button> TaskPoints = new List<Button>(); public override void Refresh()
{
UpBoxView.Refresh();
//Do something else...
}
}

当然了,大的View中也可能嵌套小的View,这样可以更为方便的将一些零散的UI控件随意的插入到其他View中,例如一般游戏中顶部的角色基础信息栏等:

 using TMPro;

 public class UpBoxView : HudView
{
public TextMeshProUGUI Name;
public TextMeshProUGUI Resource;
public TextMeshProUGUI Level; public override void Refresh()
{
var d = GameData.Instance.PlayerData;
Level.text = "Lv. " + d.lv.ToString();
Resource.text = d.ResourcePoint.ToString();
Name.text = "咕噜灵波";
}
}

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

     public static void AddTriggerListener(this Canvas canvas, Component obj, EventTriggerType type, UnityAction<BaseEventData> action)
{
//先看有没有对应组件没有就加上
var trigger = obj.gameObject.GetOrAddComponent<EventTrigger>();
//再看看触发列表中有没有事件,没有就新建一个列表
if (trigger.triggers == null || trigger.triggers.Count == )
trigger.triggers = new List<EventTrigger.Entry>();
//再看事件列表中是不是已经存在对应类型的值,如果存在的话简单直接给那个事件加个侦听就好
foreach (var e in trigger.triggers)
{
if (e.eventID == type)
{
e.callback.AddListener(action);
return;
}
}
//到这里就是很遗憾没有对应类型的事件,那就实例化一个新的,注意实例化完了以后还要把对应的事件类型和回调设定进去
var entry = new EventTrigger.Entry();
entry.eventID = type;
entry.callback.AddListener(action);
//全部设定好了再加进去,要不然没有效果知道么
trigger.triggers.Add(entry);
}
    public static T GetOrAddComponent<T>(this GameObject obj) where T : Component => obj.GetComponent<T>() ? obj.GetComponent<T>() : obj.AddComponent<T>();

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

     private void OnDrag(BaseEventData data)
{
var d = data as PointerEventData;
Debug.Log(d.dragging);
}

2020年5月25日更新:

1.在刷新视图时可传入不定类型和个数的参数。

在实际使用的过程中发现,不传递参数有时刷新视图比较困难,但不同的View又会根据不同的需要传递类型和个数均不同的参数,这时就想到了使用params object[] parameters作为参数进行传递。

 using UnityEngine;

 public class HudView : MonoBehaviour
{
public virtual void Refresh(params object[] parameters) { }
}
     /// <summary>
/// 刷新视图
/// </summary>
/// <typeparam name="T">视图类型</typeparam>
/// <param name="t">视图实例</param>
/// <param name="parameters">不定类型和个数的参数</param>
protected void UpdateView<T>(T t, params object[] parameters) where T : HudView => t.Refresh(parameters);

因为C#中所有的类型都继承自object,通过装箱和拆箱就可以传递任何类型的参数,这样写非常简洁,但缺点是对于性能会有一点的损失;如果你实在不想损失性能,也可以考虑用多种不同类型的泛型参数作为替代。

     protected void UpdateView<T0, T1>(T0 t0, T1 t1) where T0 : HudView => t0.Refresh(t1);
protected void UpdateView<T0, T1, T2>(T0 t0, T1 t1, T2 t2) where T0 : HudView => t0.Refresh(t1, t2);
protected void UpdateView<T0, T1, T2, T3>(T0 t0, T1 t1, T2 t2, T3 t3) where T0 : HudView => t0.Refresh(t1, t2, t3);
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来进入或退出页面,而是可以自定义各种切换的方式,这是就需要用到委托作为参数了。

     /// <summary>
/// 切换页面
/// </summary>
/// <param name="obj">目标</param>
/// <param name="way">切换方式委托</param>
/// <param name="complete">切换完成后委托</param>
protected void Shift(GameObject obj, UnityAction<GameObject, TweenCallback> way, UnityAction complete) => way(obj, () => complete());

当然了,这样的话我们就需要规定所有的切换方式的函数参数类型和个数都需要与way保持一致。

补充——关于类型的判断与转换:

使用params object[] parameters在实际函数实现的过程中需要判断传入的参数类型是否符合当前预期,可以通过下来的方式来进行具体判断,例如:

         if (parameters.Length >  && parameters[] is int)
{
Debug.Log((int)parameters[]);
}
         if (parameters.Length >  && parameters[].GetType() == typeof(int))
{
Debug.Log((int)parameters[]);
}

当然了,如果是引用类型,除了强制类型转换之外还可以使用关键字as进行装换,上面的例子中已经有了就不再举例了。

但如果是泛型的话是不能直接执行强制类型转换的,但还是可以先转换为object类型,再执行强制类型转换:

     private void Test<T>(T t)
{
if (t is int)
{
object temp = t;
Debug.Log((int)temp);
}
//or
if (typeof(T) == typeof(int))
{
object temp = t;
Debug.Log((int)temp);
}
}

Unity 极简UI框架的更多相关文章

  1. Spring Boot(5)一个极简且完整的后台框架

    https://blog.csdn.net/daleiwang/article/details/75007588 Spring Boot(5)一个极简且完整的后台框架 2017年07月12日 11:3 ...

  2. 树莓派(Raspberry Pi)使用Shell编写的极简Service

    树莓派(Raspberry Pi)运行的系统是基于Debian的,不仅可以运行Shell,还支持systemd和docker,可以编写一个简单的服务,让其在启动时运行,执行一些自动化的操作.这里在Ra ...

  3. 极简Unity调用Android方法

    简介 之前写了篇unity和Android交互的教程,由于代码里面有些公司的代码,导致很多网友看不懂,并且确实有点小复杂,这里弄一个极简的版本 步骤 废话不多说,直接来步骤吧 1.创建工程,弄大概像这 ...

  4. 游戏UI框架设计(二) : 最简版本设计

    游戏UI框架设计(二) --最简版本设计 为降低难度决定先讲解一个最简版本,阐述UI框架的核心设计理念.这里先定义三个核心功能: 1:UI窗体的自动加载功能. 2:缓存UI窗体. 3:窗体生命周期(状 ...

  5. 极简实用的Asp.NetCore模块化框架决定免费开源了

    背景 在开发这个框架之前,前前后后看过好几款模块化的框架,最后在一段时间内对ABP VNext痛下狠心,研究一段时间后,不得不说 ABP VNext的代码层面很规范,也都是一些最佳实践,开发出一个模块 ...

  6. Resty 一款极简的restful轻量级的web框架

    https://github.com/Dreampie/Resty Resty 一款极简的restful轻量级的web框架 开发文档 如果你还不是很了解restful,或者认为restful只是一种规 ...

  7. RELabel : 一个极简的正则表达式匹配和展示框架

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  8. php 极简框架ES发布(代码总和不到 400 行)

    ES 框架简介 ES 是一款 极简,灵活, 高性能,扩建性强 的php 框架. 未开源之前在商业公司 经历数年,数个高并发网站 实践使用! 框架结构 整个框架核心四个文件,所有文件加起来放在一起总行数 ...

  9. unity简易ui框架

    在unity项目开发中,ui模块的开发往往占据了很大一部分工作,部分游戏甚至绝大部分的工作都是在ui上,如何高效管理各种界面,这里分享一套高效易用的UI框架. 首先,我们定义一个PanelBase类, ...

随机推荐

  1. vue中的生命周期事件和钩子函数

    vue实例有一个完整的生命周期,也就是从开始创建.初始化数据.编译模板.挂载Dom.渲染->更新->渲染.卸载等一系列过程,我们称这是vue的生命周期.通俗的将就是vue实例从创建到销毁的 ...

  2. LeetCode 题解 | 242. 有效的字母异位词

    给定两个字符串 s 和t,编写一个函数来判断 t 是否是 s 的字母异位词. 示例 1: 输入: s = "anagram", t = "nagaram" 输出 ...

  3. find的基本查询命令《二》

    Linux find命令详解 由于find具有强大的功能,所以它的选项也很多,其中大部分选项都值得我们花时间来了解一下.即使系统中含有网络文件系统( NFS),find命令在该文件系统中同样有效,只你 ...

  4. Django -->admin后台(后台管理可以直接往数据库添加数据)

    一.使用pymysql时,必须加这两行(#如果使用mysql的数据库,请进行伪装 pymysql伪装为MySQLdb) import pymysqlpymysql.install_as_MySQLdb ...

  5. for循环in遍历

    <script> //对象本身没有length,所以不能用for循环遍历 //要用for...in...循环 var aaa = {"name":"拴住&qu ...

  6. CopyOnWriteArrayList线程安全的集合

    CopyOnWriteArrayList是线程安全的集合.本身就是安全的,同时只能被一个进程所访问. 属于JUC并发编程里面的内容. public static void main(String[] ...

  7. 1.Git 安装

    Git的安装 阿里云镜像去下载 阿里云的镜像地址 卸载Git 1.首先在系统环境变量->path->里面去清理掉git相关的环境变量 2.然后控制面板卸载 安装Git 一直下一步即可 使用 ...

  8. stand up meeting for beta release plan 12/16/2015

    今天我们开会讨论一下beta版需要的feature,其中待定的feature是可选做的,如果有时间.其他都是必须实现的. 因为做插件的计划失败了,所以我们现在是pdf阅读器和取词查词加入生词本这两部分 ...

  9. H - Hamiltonian Hypercube Gym - 101170H

    规律题 首先我们要知道他的顺序是怎么来的,首先当n等于1时,是0,1 当n=2时,先按照与按顺序在他们前面分别加0,即00,01,在逆序加1,即11,10 构成的顺序为00,01,11,10:往后同理 ...

  10. Laravel joinSub 子查询的写法

    $subQuery = $model::query() ->from('table1 as a') ->getQuery(); $query = $model::query() -> ...