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基类:
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框架的更多相关文章
- Spring Boot(5)一个极简且完整的后台框架
https://blog.csdn.net/daleiwang/article/details/75007588 Spring Boot(5)一个极简且完整的后台框架 2017年07月12日 11:3 ...
- 树莓派(Raspberry Pi)使用Shell编写的极简Service
树莓派(Raspberry Pi)运行的系统是基于Debian的,不仅可以运行Shell,还支持systemd和docker,可以编写一个简单的服务,让其在启动时运行,执行一些自动化的操作.这里在Ra ...
- 极简Unity调用Android方法
简介 之前写了篇unity和Android交互的教程,由于代码里面有些公司的代码,导致很多网友看不懂,并且确实有点小复杂,这里弄一个极简的版本 步骤 废话不多说,直接来步骤吧 1.创建工程,弄大概像这 ...
- 游戏UI框架设计(二) : 最简版本设计
游戏UI框架设计(二) --最简版本设计 为降低难度决定先讲解一个最简版本,阐述UI框架的核心设计理念.这里先定义三个核心功能: 1:UI窗体的自动加载功能. 2:缓存UI窗体. 3:窗体生命周期(状 ...
- 极简实用的Asp.NetCore模块化框架决定免费开源了
背景 在开发这个框架之前,前前后后看过好几款模块化的框架,最后在一段时间内对ABP VNext痛下狠心,研究一段时间后,不得不说 ABP VNext的代码层面很规范,也都是一些最佳实践,开发出一个模块 ...
- Resty 一款极简的restful轻量级的web框架
https://github.com/Dreampie/Resty Resty 一款极简的restful轻量级的web框架 开发文档 如果你还不是很了解restful,或者认为restful只是一种规 ...
- RELabel : 一个极简的正则表达式匹配和展示框架
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
- php 极简框架ES发布(代码总和不到 400 行)
ES 框架简介 ES 是一款 极简,灵活, 高性能,扩建性强 的php 框架. 未开源之前在商业公司 经历数年,数个高并发网站 实践使用! 框架结构 整个框架核心四个文件,所有文件加起来放在一起总行数 ...
- unity简易ui框架
在unity项目开发中,ui模块的开发往往占据了很大一部分工作,部分游戏甚至绝大部分的工作都是在ui上,如何高效管理各种界面,这里分享一套高效易用的UI框架. 首先,我们定义一个PanelBase类, ...
随机推荐
- 基于Java的数字货币交易系统的架构设计与开发
前言 无论是股票交易系统,还是数字货币交易系统,都离不开撮合交易引擎,这是交易平台的心脏.同时,一个优秀的架构设计也会让交易平台的运维和持续开发更加容易.本文基于对开源项目的深入研究,总结了数字货币交 ...
- 6.Metasploit生成apk攻击Android实例
Metasploit进阶第四讲 生成Android apk文件 01 msfvenom基本参数 msfvenom介绍 承接上回,staged/unstage payload如何利用? msfven ...
- Nordic nRF52820超低功耗蓝牙5.2 SoC芯片-低端无线连接方案首选
nRF52820是功耗超低的低功耗蓝牙 (Bluetooth Low Energy /Bluetooth LE).蓝牙mesh.Thread.Zigbee和2.4 GHz专有低端无线连接解决方案.nR ...
- java中的动手动脑
1.关于构造函数的问题 为什么上面的代码不能通过编译? 因为当你没有定义构造函数时,java编译器在编译时会自动生成一个无参的构造函数,上面的代码就可以进行执行了.但是当你顶一个构造函数时,编译器将不 ...
- (js描述的)数据结构[树结构之红黑树](13)
1.二叉送搜索树的缺点: 2.红黑树难度: 3.红黑树五大规则: 4.红黑树五大规则的作用: 5.红黑树二大变换: 1)变色 2)旋转 6.红黑树的插入五种变换情况: 先声明--------插入的数据 ...
- 十年测试老鸟告诉你--自动化测试选JAVA还是选Python--写给还在迷茫中的朋友
一.前言 Python和Java哪个更适合做自动化测试?这是很多测试工程师从功能跨入自动化纠结的问题,今天测试老鸟来带大家详细分析一下!写给还在迷茫中的朋友! 首先可以确认的是提出这个问题的肯定是一个 ...
- 【图机器学习】cs224w Lecture 7 - 节点的表示
目录 Node Embedding Random Walk node2vec TransE Embedding Entire Graph Anonymous Walk Reference 转自本人:h ...
- "多行文本"组件:<multi> —— 快应用组件库H-UI
 <import name="multi" src="../Common/ui/h-ui/text/c_text_multi"></impo ...
- Python设计模式(8)-抽象工厂
# coding=utf-8 这种方式反倒把事情做复杂了 可取之处在于有了更高层次的抽象 class IEmployee: def insert_employee(self): pass class ...
- Powershell抓取网页信息
一般经常使用invoke-restmethod和invoke-webrequest这两个命令来获取网页信息,如果对象格式是json或者xml会更容易 1.invoke-restmethod 我们可以用 ...