一、UI框架设计
如何设计一个好的UI框架,主要关注三个点:1.性能点 2.功能点 3.系统点
性能点:
避免频繁的网格重建带来的大量性能消耗和垃圾回收。
避免调用SetActive
带来的性能消耗和GC垃圾。
避免频繁Instantiate
造成的卡顿和性能消耗以及GC垃圾。
避免不必要的组件属性带来的性能开销
避免不正确的组件顺序导致打断合批带来的性能开销和DrawCall
增加
功能点:
交互性:界面访问界面,外部访问界面要简单方便。
可控性:界面的生命周期函数,要完全可控。不能穿插或出现别的系统周期调用,比如Mono。
便携性:需要使用方便,简单,不可太过于复杂,可以随时随地不需要更改任何代码即可使用。
系统点:
自动化系统:整个系统重复且繁琐无用的工作自动化,合理节约时间。
透明的层级系统:整个UI系统层级要透明,可控,不能特效与界面完全乱套,或修改一个界面的层级,导致其他界面都需要临时修改。
完善的遮罩系统:遮罩系统要保证满足市面上的单遮和叠遮的需求。
简洁的堆栈系统:堆栈系统简洁明了,方便使用。
二、框架简介
Untiy高性能自动化UI管理框架ZMUIFramework
是铸梦老师以性能点、自动化点、效率点和易使用点四个大点为基础研发的一个高性能、高效率的Mono、物体分离式框架,该框架打破常规的Mono
依赖驱动,生命周期自动化管理,无需考虑与Mono
周期混杂问题。全程界面物体上无任何一个脚本,但使用起来和Mono
脚本无任何差别。对于IRuntime
可以实现无障碍移植切换到热更域或Mono
源工程。
框架优点:
不继承MonoBehaviour
但和继承MonoBehavio2ur
使用起来无任何差别。
不受MonoBehaviour
生命周期影响,生命周期完全可控,代码运行顺序掌握在自己手中。
智能化全自动化UI框架,无需手动创建脚本、声明方法、变量、拖拽物体赋值。UI脚本、方法、属性字段自动生成,UI组件自动拖拽绑定,一键搞定。
高效率,本来需要5-10分钟完成的工作,到我们这里只需要1分钟不到,即可完成工作。
高性能、不卡顿、框架内部不放过任何一点影响性能的问题,把性能问题扼杀在摇篮之中,让我们的UI弹出更加流畅,游戏体验更加舒服。(不用后期优化,减少后期工作量)
完整性框架,内置多个常用UI功能解决方案,能够满足UI相关的所有需求,比如遮罩系统、堆栈系统、层级管理系统、自动化系统等。框架能够正确的避免掉项目中常遇到的各种问题。如:遮罩错乱问题,特效UI层级混乱问题,堆栈弹窗顺序问题等等,框架直接从根部去解决掉,以绝后患。
代码整齐、美观、统一。
框架在性能方面可以避免以下的问题点:
频繁的网格重建带来的大量性能消耗和垃圾回收
频繁的SetActive
带来的性能消耗和GC
垃圾
频繁Instantiate
造成的卡顿和性能消耗以及GC垃圾
不必要的组件属性带来的性能开销
不正确的组件顺序导致打断合批带来的性能开销和DrawCall
增加
不必要的组件使用RaycastTarget
带来的性能消耗
并且铸梦老师还考虑其他的各种原因,最终以Canvas作为每个界面的基础进行制作。
把Canvas作为界面基础的主要原因为:
Canvas
本身并不占用DrawCall
Canvas
分离能最大化的减少网格重建带来的性能消耗
能够提高界面的性能,网格数据量小,绘制压力小
能够提高界面的流畅性
一般情况下,同屏同时显示的界面也很少能大于5个的,所以一般也不会产生Canvas
过多的问题
这个框架包含以下六大系统(并且包含部分性能优化工具):
UI管理系统:管理UI界面的生成、销毁、交互、以及生命周期。
遮罩系统:管理UI弹出和关闭时的遮罩的处理,单遮罩模式、叠遮模式。
堆栈系统:管理UI有序弹出、比如刚进大厅会按照优先级反复弹出多个界面。
灵活的层级系统:管理UI层级保证界面-模型-特效-界面中级不会有穿插的现象。
自动化系统:管理UI繁琐且重复的工作让其自动化生成比如脚本、方法属性声明、组件绑定等。
高效率系统:管理各种UI的创建与模板制作,让开发者以最小的动作实现最大的产出。
高性能系统:管理并解决UI元素与界面的性能问题,在框架底层彻底解决,避免后期的二次性能优化。
三、UI管理系统
UIModule(类似于唐老师UI框架的UIManager类)
UI管理器类,负责整个程序所有面板的管理,并向外部提供一些 API 调用面板,例如显示面板,隐藏面板,获取面板等。
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// UI管理器 - 单例模式
/// </summary>
public class UIModule
{
private static UIModule _instance;
public static UIModule Instance
{ get { if (_instance == null) { _instance = new UIModule(); } return _instance; } }
private Camera mUICamera; // 场景 UI 相机
private Transform mUIRoot; // UI 根物体
private Dictionary<string, WindowBase> mAllWindowDic = new Dictionary<string, WindowBase>(); // 所有窗口的Dic key-窗口类名
private List<WindowBase> mAllWindowList = new List<WindowBase>(); // 所有窗口的列表
private List<WindowBase> mVisibleWindowList = new List<WindowBase>(); // 所有可见窗口的列表
/// <summary>
/// 初始化 UIModule 管理器方法
/// </summary>
public void Initialize()
{
mUICamera = GameObject.Find("UICamera").GetComponent<Camera>();
mUIRoot = GameObject.Find("UIRoot").transform;
}
#region 窗口管理
/// <summary>
/// 弹出一个弹窗 并渲染在视窗最前面
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T PopUpWindow<T>() where T : WindowBase, new()
{
System.Type type = typeof(T);
string wndName = type.Name;
WindowBase wnd = GetWindow(wndName);
if (wnd != null)
{
return ShowWindow(wndName) as T;
}
// 如果窗口不存在 则新建一个
T t = new T();
return InitializeWindow(t, wndName) as T;
}
/// <summary>
/// 弹出一个窗口 并渲染在视窗最前面
/// </summary>
/// <param name="window"></param>
/// <returns></returns>
private WindowBase PopUpWindow(WindowBase window)
{
System.Type type = window.GetType();
string wndName = type.Name;
WindowBase wnd = GetWindow(wndName);
if (wnd != null)
{
return ShowWindow(wndName);
}
return InitializeWindow(window, wndName);
}
/// <summary>
/// 初始化窗口
/// </summary>
/// <param name="windowBase">窗口对象</param>
/// <param name="wndName">窗口名</param>
/// <returns></returns>
private WindowBase InitializeWindow(WindowBase windowBase, string wndName)
{
// 1.生成对应的窗口预制体
GameObject nWnd = TempLoadWindow(wndName);
// 2.初始化对应管理类
if (nWnd != null)
{
windowBase.gameObject = nWnd;
windowBase.transform = nWnd.transform;
windowBase.Canvas = nWnd.GetComponent<Canvas>();
windowBase.Canvas.worldCamera = mUICamera;
windowBase.transform.SetAsLastSibling();
windowBase.Name = nWnd.name;
// 调用该窗口的生命周期函数 并设置可见
windowBase.OnAwake();
windowBase.SetVisible(true);
windowBase.OnShow();
RectTransform rectTrans = nWnd.GetComponent<RectTransform>();
rectTrans.anchorMax = Vector2.one;
rectTrans.offsetMax = Vector2.zero;
rectTrans.offsetMin = Vector2.zero;
// 添加到对应列表中进行管理
mAllWindowDic.Add(wndName, windowBase);
mAllWindowList.Add(windowBase);
mVisibleWindowList.Add(windowBase);
SetWidnowMaskVisible();
return windowBase;
}
Debug.LogError("没有加载到对应的窗口 窗口名字:" + wndName);
return null;
}
/// <summary>
/// show 弹出过的窗口
/// </summary>
/// <param name="winName">窗口类名</param>
/// <returns></returns>
private WindowBase ShowWindow(string winName)
{
WindowBase window = null;
// 已经打开过
if (mAllWindowDic.ContainsKey(winName))
{
window = mAllWindowDic[winName];
// 窗口已经存在 且 是不可见的
if (window.gameObject != null && window.Visible == false)
{
// 添加到可见列表中管理
mVisibleWindowList.Add(window);
// 将窗口位置设置为最后一个 最优先渲染
window.transform.SetAsLastSibling();
window.SetVisible(true);
SetWidnowMaskVisible();
window.OnShow();
}
return window;
}
else
Debug.LogError(winName + " 窗口不存在,请调用PopUpWindow 进行弹出");
return null;
}
/// <summary>
/// 得到已经弹出过的窗口
/// </summary>
/// <param name="winName">窗口类名</param>
/// <returns></returns>
private WindowBase GetWindow(string winName)
{
if (mAllWindowDic.ContainsKey(winName))
{
return mAllWindowDic[winName];
}
return null;
}
/// <summary>
/// 获取已经弹出的弹窗
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T GetWindow<T>() where T : WindowBase
{
System.Type type = typeof(T);
// 一般都是访问已经打开的窗口 进行一些 API 调用
foreach (var item in mVisibleWindowList)
{
if (item.Name == type.Name)
{
return (T)item;
}
}
Debug.LogError("该窗口没有获取到:" + type.Name);
return null;
}
/// <summary>
/// 隐藏面板
/// </summary>
/// <param name="wndName">面板类名</param>
public void HideWindow(string wndName)
{
WindowBase window = GetWindow(wndName);
HideWindow(window);
}
/// <summary>
/// 泛型 隐藏面板
/// </summary>
/// <typeparam name="T"></typeparam>
public void HideWindow<T>() where T : WindowBase
{
HideWindow(typeof(T).Name);
}
private void HideWindow(WindowBase window)
{
// 面板不为空 且 是可见的
if (window != null && window.Visible)
{
mVisibleWindowList.Remove(window);
window.SetVisible(false); // 隐藏弹窗物体
SetWidnowMaskVisible();
window.OnHide();
}
// 在出栈的情况下,上一个界面隐藏时,自动打开栈种的下一个界面
PopNextStackWindow(window);
}
private void DestroyWindow(string wndName)
{
WindowBase window = GetWindow(wndName);
DestoryWindow(window);
}
/// <summary>
/// 泛型 销毁面板
/// </summary>
/// <typeparam name="T"></typeparam>
public void DestroyWinodw<T>() where T : WindowBase
{
DestroyWindow(typeof(T).Name);
}
private void DestoryWindow(WindowBase window)
{
if (window != null)
{
if (mAllWindowDic.ContainsKey(window.Name))
{
// 在对应容器中 移除对应的面板
mAllWindowDic.Remove(window.Name);
mAllWindowList.Remove(window);
mVisibleWindowList.Remove(window);
}
window.SetVisible(false);
SetWidnowMaskVisible();
window.OnHide();
window.OnDestroy();
GameObject.Destroy(window.gameObject);
// 在出栈的情况下 上一个界面销毁时 自动打开栈种的下一个界面
PopNextStackWindow(window);
}
}
/// <summary>
/// 销毁所有的面板都销毁掉
/// </summary>
/// <param name="filterlist"></param>
public void DestroyAllWindow(List<string> filterlist = null)
{
// 反向循环进行边循环边删除 列表的本质是数组
for (int i = mAllWindowList.Count - 1; i >= 0; i--)
{
WindowBase window = mAllWindowList[i];
if (window == null || (filterlist != null && filterlist.Contains(window.Name)))
{
continue;
}
DestroyWindow(window.Name);
}
// 注意释放资源的时机
Resources.UnloadUnusedAssets();
}
/// <summary>
/// 动态加载窗口预制件
/// </summary>
/// <param name="wndName">窗口名</param>
/// <returns></returns>
public GameObject TempLoadWindow(string wndName)
{
GameObject window = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>(mWindowConfig.GetWindowPath(wndName)), mUIRoot);
//window.transform.SetParent(mUIRoot);
window.transform.localScale = Vector3.one;
window.transform.localPosition = Vector3.zero;
window.transform.rotation = Quaternion.identity;
window.name = wndName;
return window;
}
#endregion
}
WindowBehaviour(类似于有限状态机和行为树的基类自己管理生命周期函数)
与MonoBehaviour类似是所有界面都必须继承的最继承最顶层的类,主要负责生命周期以及基础属性的声明。
using System;
using UnityEngine;
/// <summary>
/// 类似 MonoBehaviour 的类
/// </summary>
public abstract class WindowBehaviour
{
public GameObject gameObject { get; set; } // 当前窗口物体 GameObject
public Transform transform { get; set; } // 代表自己 Transform
public Canvas Canvas { get; set; } // 当前窗口的 Canvas
public string Name { get; set; } // 当前窗口的名字
public bool Visible { get; set; } // 当前窗口是否可见
/// <summary>
/// 只会在物体创建时执行一次 ,与Mono Awake调用时机和次数保持一致
/// </summary>
public virtual void OnAwake()
{ }
/// <summary>
/// 在物体显示时执行一次,与Mono OnEnable一致
/// </summary>
public virtual void OnShow()
{ }
/// <summary>
/// 更新函数 与Mono Update一致
/// </summary>
public virtual void OnUpdate()
{ }
/// <summary>
/// 在物体隐藏时执行一次,与Mono OnDisable 一致
/// </summary>
public virtual void OnHide()
{ }
/// <summary>
/// 在当前界面被销毁时调用一次
/// </summary>
public virtual void OnDestroy()
{ }
/// <summary>
/// 设置物体的可见性
/// </summary>
/// <param name="isVisble">是否可见</param>
public virtual void SetVisible(bool isVisble)
{ }
}
WindowBase(类似于唐老师UI框架的BasePanel类)
UI窗口基类,负责部分共用功能的统一化处理,例如弹出、关闭动画、以及解耦合方法的声明等其他公用接口的处理。
using DG.Tweening;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
/// <summary>
/// 面板窗口基类
/// </summary>
public class WindowBase : WindowBehaviour
{
private List<Button> mAllButtonList = new List<Button>(); // 所有 Button 列表
private List<Toggle> mToggleList = new List<Toggle>(); // 所有的 Toggle 列表
private List<InputField> mInputList = new List<InputField>(); // 所有的输入框列表
#region 重写生命周期
public override void OnAwake()
{
base.OnAwake();
}
public override void OnShow()
{
base.OnShow();
}
public override void OnUpdate()
{
base.OnUpdate();
}
public override void OnHide()
{
base.OnHide();
}
public override void OnDestroy()
{
base.OnDestroy();
RemoveAllButtonListener();
RemoveAllToggleListener();
RemoveAllInputListener();
mAllButtonList.Clear();
mToggleList.Clear();
mInputList.Clear();
}
#endregion
public void HideWindow()
{
}
public override void SetVisible(bool isVisble)
{
gameObject.SetActive(isVisble);
}
#region 组件事件管理
/// <summary>
/// 添加 Button 组件事件监听
/// </summary>
/// <param name="btn"></param>
/// <param name="action"></param>
public void AddButtonClickListener(Button btn, UnityAction action)
{
if (btn != null)
{
if (!mAllButtonList.Contains(btn))
{
// 添加 btn 组件到列表中统一管理
mAllButtonList.Add(btn);
}
// 先把 btn 所有事件移除 再添加新的事件
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(action);
}
}
/// <summary>
/// 添加 Toggle 组件事件监听
/// </summary>
/// <param name="toggle"></param>
/// <param name="action"></param>
public void AddToggleClickListener(Toggle toggle, UnityAction<bool, Toggle> action)
{
if (toggle != null)
{
if (!mToggleList.Contains(toggle))
{
// 添加 Toggle 组件到列表中统一管理
mToggleList.Add(toggle);
}
// 先把 Toggle 所有事件移除 再添加新的事件
toggle.onValueChanged.RemoveAllListeners();
toggle.onValueChanged.AddListener((isOn) =>
{
// 把 Toggle 自身和他的 isOn值一起传递出去
action?.Invoke(isOn, toggle);
});
}
}
/// <summary>
/// 添加输入框事件监听
/// </summary>
/// <param name="input"></param>
/// <param name="onChangeAction">输入修改事件</param>
/// <param name="endAction">输入完成事件</param>
public void AddInputFieldListener(InputField input, UnityAction<string> onChangeAction, UnityAction<string> endAction)
{
if (input != null)
{
if (!mInputList.Contains(input))
{
// 添加 InputField 组件到列表中统一管理
mInputList.Add(input);
}
// 先移除事件 再监听新事件
input.onValueChanged.RemoveAllListeners();
input.onEndEdit.RemoveAllListeners();
input.onValueChanged.AddListener(onChangeAction);
input.onEndEdit.AddListener(endAction);
}
}
/// <summary>
/// 移除 Button 事件监听
/// </summary>
public void RemoveAllButtonListener()
{
foreach (var item in mAllButtonList)
{
item.onClick.RemoveAllListeners();
}
}
/// <summary>
/// 移除 Toggle 事件监听
/// </summary>
public void RemoveAllToggleListener()
{
foreach (var item in mToggleList)
{
item.onValueChanged.RemoveAllListeners();
}
}
/// <summary>
/// 移除 InputField 事件监听
/// </summary>
public void RemoveAllInputListener()
{
foreach (var item in mInputList)
{
item.onValueChanged.RemoveAllListeners();
item.onEndEdit.RemoveAllListeners();
}
}
#endregion
}
四、遮罩系统
UI遮罩系统是项目中很重要的一个功能,市面上普遍存在的两种遮罩模式:一种是单遮模式,一种是叠遮模式。
单遮模式:无论打开多少界面,遮罩只有一层。(市面上常用的遮罩模式,但是更复杂,问题也更多。)
叠遮模式:即每一个界面都有一层遮罩,打开的界面越多,遮罩越黑。
UISetting
用于配置 UI 的一些设置,用单例模式,便于访问。
using UnityEngine;
[CreateAssetMenu(fileName = "UISetting", menuName = "UISetting", order = 0)]
public class UISetting : ScriptableObject
{
private static UISetting _instance;
public static UISetting Instance
{ get { if (_instance == null) { _instance = Resources.Load<UISetting>("UISetting"); } return _instance; } }
public bool SINGMASK_SYSTEM; // 是否启用单遮模式
}
创建一个面板Canvas模板,便于后续制作自定义面板
根据前面的UIModule
脚本的代码可以知道,所有面板都会放在UIRoot
这个根物体下,而每个面板的子元素都会放在一个Canvas
组件下,UIMask
则是遮罩图层(需要添加CanvasGroup
组件,因为主要通过调节CanvasGroup
上的Alpha
设置显隐,减少重绘次数),UI元素内容则放在UIContent
物体下(为了后期如果有一些特定的缩放平移等功能实现,不影响遮罩图层的变化)。
在以下脚本中新增以下代码以实现遮罩系统
UIModule
private WindowBase InitializeWindow(WindowBase windowBase, string wndName)
{
GameObject nWnd = TempLoadWindow(wndName);
if (nWnd != null)
{
windowBase.gameObject = nWnd;
windowBase.transform = nWnd.transform;
windowBase.Canvas = nWnd.GetComponent<Canvas>();
windowBase.Canvas.worldCamera = mUICamera;
windowBase.transform.SetAsLastSibling();
windowBase.Name = nWnd.name;
windowBase.OnAwake();
windowBase.SetVisible(true);
windowBase.OnShow();
RectTransform rectTrans = nWnd.GetComponent<RectTransform>();
rectTrans.anchorMax = Vector2.one;
rectTrans.offsetMax = Vector2.zero;
rectTrans.offsetMin = Vector2.zero;
mAllWindowDic.Add(wndName, windowBase);
mAllWindowList.Add(windowBase);
mVisibleWindowList.Add(windowBase);
// 新增设置窗口的遮罩层
SetWidnowMaskVisible();
return windowBase;
}
Debug.LogError("没有加载到对应的窗口 窗口名字:" + wndName);
return null;
}
private void SetWidnowMaskVisible()
{
// 单遮和叠遮模式处理
if (!UISetting.Instance.SINGMASK_SYSTEM)
{
return;
}
WindowBase maxOrderWndBase = null; // 最大渲染层级的窗口
int maxOrder = 0; // 最大渲染层级
int maxIndex = 0; // 最大排序下标 在相同父节点下的位置下标
// 1.关闭所有窗口的Mask 设置为不可见
// 2.从所有可见窗口中找到一个层级最大的窗口 把Mask设置为可见
for (int i = 0; i < mVisibleWindowList.Count; i++)
{
WindowBase window = mVisibleWindowList[i];
if (window != null && window.gameObject != null)
{
// 先把所有窗口可见性关闭
window.SetMaskVisible(false);
if (maxOrderWndBase == null)
{
maxOrderWndBase = window;
maxOrder = window.Canvas.sortingOrder;
maxIndex = window.transform.GetSiblingIndex();
}
else
{
// 找到最大渲染层级的窗口 拿到它
if (maxOrder < window.Canvas.sortingOrder)
{
maxOrderWndBase = window;
maxOrder = window.Canvas.sortingOrder;
}
// 如果两个窗口的渲染层级相同 就找到同节点下最靠下一个物体 优先渲染Mask
else if (maxOrder == window.Canvas.sortingOrder && maxIndex < window.transform.GetSiblingIndex())
{
maxOrderWndBase = window;
maxIndex = window.transform.GetSiblingIndex();
}
}
}
}
if (maxOrderWndBase != null)
{
maxOrderWndBase.SetMaskVisible(true);
}
}
private WindowBase ShowWindow(string winName)
{
WindowBase window = null;
if (mAllWindowDic.ContainsKey(winName))
{
window = mAllWindowDic[winName];
if (window.gameObject != null && window.Visible == false)
{
mVisibleWindowList.Add(window);
window.transform.SetAsLastSibling();
window.SetVisible(true);
// 新增遮罩层设置
SetWidnowMaskVisible();
window.OnShow();
}
return window;
}
else
Debug.LogError(winName + " 窗口不存在,请调用PopUpWindow 进行弹出");
return null;
}
private void HideWindow(WindowBase window)
{
if (window != null && window.Visible)
{
mVisibleWindowList.Remove(window);
window.SetVisible(false);
SetWidnowMaskVisible();
window.OnHide();
}
PopNextStackWindow(window);
}
private void DestoryWindow(WindowBase window)
{
if (window != null)
{
if (mAllWindowDic.ContainsKey(window.Name))
{
mAllWindowDic.Remove(window.Name);
mAllWindowList.Remove(window);
mVisibleWindowList.Remove(window);
}
window.SetVisible(false);
// 新增遮罩层设置
SetWidnowMaskVisible();
window.OnHide();
window.OnDestroy();
GameObject.Destroy(window.gameObject);
PopNextStackWindow(window);
}
}
WindowBase
// 拿到对应的组件
private CanvasGroup mUIMask;
private CanvasGroup mCanvasGroup;
protected Transform mUIContent;
/// <summary>
/// 初始化基类组件
/// </summary>
private void InitializeBaseComponent()
{
mCanvasGroup = transform.GetComponent<CanvasGroup>();
mUIMask = transform.Find("UIMask").GetComponent<CanvasGroup>();
mUIContent = transform.Find("UIContent").transform;
}
/// <summary>
/// 设置遮罩的显隐
/// </summary>
/// <param name="isVisble"></param>
public void SetMaskVisible(bool isVisble)
{
// 单遮和叠遮模式处理
if (!UISetting.Instance.SINGMASK_SYSTEM)
{
return;
}
mUIMask.alpha = isVisble ? 1 : 0;
}
五、灵活的层级系统
解决开发时遇到的模型与特效与界面之间的穿插问题,这个框架的设计理念就是把每个不同等级的弹窗留出100个层级去适配特效,也就是基础界面的特效永远不会穿插到二级弹窗上去,即使是相同等级的弹窗叠加情况,该设计仍然有100层级去适配叠加的情况。
制作弹窗模板
每一个模板的父物体都是一个Canvas
,因为Canvas
本身是不需要DrawCall
的,所以选择使用Canvas
作为每一个面板的基础,一级弹窗的Order in Layer
改成0,二级是100,三级是200,以此类推制作你自己的自定义模板。
六、自动化系统
自动化生成代码的本质,其实就是把一些通用的脚本比如继承WindowBase
的子类,他们共有的通用的代码直接通过字符串拼接的形式,把其中可变的一些参数变量,通过数据配置,然后拼接成字符串,再用System.IO
的文件流读写方法,生成本地C#代码文件,从而实现了代码脚本自动化生成。自动化工具这种其实没什么分析的,就是一些Editor接口调用,然后手写一个C#代码文件。
GeneratorFindComponentTool
一键生成组件查找脚本,这个脚本主要是管理UI面板中的所有组件,并向外部提供初始化方法,WindowBase类直接调用初始化方法,即可自动查找到对应的组件,并添加事件。
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using System.Text;
using Newtonsoft.Json;
/// <summary>
/// 生成查找组件脚本工具
/// </summary>
public class GeneratorFindComponentTool : Editor
{
public static Dictionary<int, string> objFindPathDic; // key-物体的insid value-代表物体的查找路径
public static List<EditorObjectData> objDataList; // 查找对象的数据
[MenuItem("GameObject/生成组件查找脚本(Shift+U) #U", false, 0)]
private static void CreateFindComponentScripts()
{
GameObject obj = Selection.objects.First() as GameObject; // 获取到当前选择的物体
if (obj == null)
{
Debug.LogError("需要选择 GameObject");
return;
}
objDataList = new List<EditorObjectData>();
objFindPathDic = new Dictionary<int, string>();
// 设置脚本生成路径
if (!Directory.Exists(GeneratorConfig.FindComponentGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.FindComponentGeneratorPath);
}
// 解析窗口组件数据
PresWindowNodeData(obj.transform, obj.name);
// 储存字段名称
string datalistJson = JsonConvert.SerializeObject(objDataList);
PlayerPrefs.SetString(GeneratorConfig.OBJDATALIST_KEY, datalistJson);
// 生成CS脚本
string csContnet = CreateCS(obj.name);
Debug.Log("CsConent:\n" + csContnet);
string cspath = GeneratorConfig.FindComponentGeneratorPath + "/" + obj.name + "UIComponent.cs";
UIWindowEditor.ShowWindow(csContnet, cspath);
}
/// <summary>
/// 解析窗口节点数据
/// </summary>
/// <param name="trans"></param>
/// <param name="WinName"></param>
public static void PresWindowNodeData(Transform trans, string WinName)
{
// 遍历孩子节点
for (int i = 0; i < trans.childCount; i++)
{
GameObject obj = trans.GetChild(i).gameObject;
string name = obj.name;
// 判断名字中是否包含 [] 中括号元素
if (name.Contains("[") && name.Contains("]"))
{
// 解析组件名 获得组件类型和名字
int index = name.IndexOf("]") + 1;
string fieldName = name.Substring(index, name.Length - index); // 获取字段昵称
string fieldType = name.Substring(1, index - 2); // 获取字段类型
// 保存到容器中
objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, insID = obj.GetInstanceID() });
// 计算该节点的查找路径
string objPath = name; // UIContent/[Button]Close
bool isFindOver = false;
Transform parent = obj.transform;
for (int k = 0; k < 20; k++)
{
for (int j = 0; j <= k; j++)
{
if (k == j)
{
parent = parent.parent;
// 如果父节点是当前窗口 说明查找已经结束
if (string.Equals(parent.name, WinName))
{
isFindOver = true;
break;
}
else
{
objPath = objPath.Insert(0, parent.name + "/");
}
}
}
if (isFindOver)
{
break;
}
}
objFindPathDic.Add(obj.GetInstanceID(), objPath);
}
// 递归直到最后路径生成
PresWindowNodeData(trans.GetChild(i), WinName);
}
}
/// <summary>
/// 生成C#脚本
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public static string CreateCS(string name)
{
StringBuilder sb = new StringBuilder();
string nameSpaceName = "ZMUIFrameWork";
sb.AppendLine("using UnityEngine.UI;");
sb.AppendLine("using UnityEngine;");
sb.AppendLine();
//生成命名空间
if (!string.IsNullOrEmpty(nameSpaceName))
{
sb.AppendLine($"namespace {nameSpaceName}");
sb.AppendLine("{");
}
sb.AppendLine($"\tpublic class {name + "UIComponent"}");
sb.AppendLine("\t{");
//根据字段数据列表 声明字段
foreach (var item in objDataList)
{
sb.AppendLine("\t\tpublic " + item.fieldType + " " + item.fieldName + item.fieldType + ";\n");
}
//声明初始化组件接口
sb.AppendLine("\t\tpublic void InitComponent(WindowBase target)");
sb.AppendLine("\t\t{");
sb.AppendLine("\t\t //组件查找");
//根据查找路径字典 和字段数据列表生成组件查找代码
foreach (var item in objFindPathDic)
{
EditorObjectData itemData = GetEditorObjectData(item.Key);
string relFieldName = itemData.fieldName + itemData.fieldType;
if (string.Equals("GameObject", itemData.fieldType))
{
sb.AppendLine($"\t\t {relFieldName} =target.transform.Find(\"{item.Value}\").gameObject;");
}
else if (string.Equals("Transform", itemData.fieldType))
{
sb.AppendLine($"\t\t {relFieldName} =target.transform.Find(\"{item.Value}\").transform;");
}
else
{
sb.AppendLine($"\t\t {relFieldName} =target.transform.Find(\"{item.Value}\").GetComponent<{itemData.fieldType}>();");
}
}
sb.AppendLine("\t");
sb.AppendLine("\t");
sb.AppendLine("\t\t //组件事件绑定");
//得到逻辑类 WindowBase => LoginWindow
sb.AppendLine($"\t\t {name} mWindow=({name})target;");
//生成UI事件绑定代码
foreach (var item in objDataList)
{
string type = item.fieldType;
string methodName = item.fieldName;
string suffix = "";
if (type.Contains("Button"))
{
suffix = "Click";
sb.AppendLine($"\t\t target.AddButtonClickListener({methodName}{type},mWindow.On{methodName}Button{suffix});");
}
if (type.Contains("InputField"))
{
sb.AppendLine($"\t\t target.AddInputFieldListener({methodName}{type},mWindow.On{methodName}InputChange,mWindow.On{methodName}InputEnd);");
}
if (type.Contains("Toggle"))
{
suffix = "Change";
sb.AppendLine($"\t\t target.AddToggleClickListener({methodName}{type},mWindow.On{methodName}Toggle{suffix});");
}
}
sb.AppendLine("\t\t}");
sb.AppendLine("\t}");
if (!string.IsNullOrEmpty(nameSpaceName))
{
sb.AppendLine("}");
}
return sb.ToString();
}
/// <summary>
/// 根据物体的 insid 获取具体的数据
/// </summary>
/// <param name="insid"></param>
/// <returns></returns>
public static EditorObjectData GetEditorObjectData(int insid)
{
foreach (var item in objDataList)
{
if (item.insID == insid)
{
return item;
}
}
return null;
}
}
/// <summary>
/// 组件数据对象
/// </summary>
public class EditorObjectData
{
public int insID;
public string fieldName;
public string fieldType;
}
GeneratorWindowTool
这个方法同上,主要用于生成继承WindowBase的子类,并根据每个面板的组件获取并序列化它自己的UICompoent类,并生成对应的UI组件事件。
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.IO;
using Newtonsoft.Json;
using System.Text;
public class GeneratorWindowTool : Editor
{
private static Dictionary<string, string> methodDic = new Dictionary<string, string>();
[MenuItem("GameObject/生成Window脚本(Shift+V) #V", false, 0)]
private static void CreateFindComponentScripts()
{
GameObject obj = Selection.objects.First() as GameObject;//获取到当前选择的物体
if (obj == null)
{
Debug.LogError("需要选择 GameObject");
return;
}
//设置脚本生成路径
if (!Directory.Exists(GeneratorConfig.WindowGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.WindowGeneratorPath);
}
//生成CS脚本
string csContnet = CreateWindoCs(obj.name);
Debug.Log("CsConent:\n" + csContnet);
string cspath = GeneratorConfig.WindowGeneratorPath + "/" + obj.name + ".cs";
UIWindowEditor.ShowWindow(csContnet, cspath, methodDic);
}
/// <summary>
/// 生成Window脚本
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public static string CreateWindoCs(string name)
{
//储存字段名称
string datalistJson = PlayerPrefs.GetString(GeneratorConfig.OBJDATALIST_KEY);
List<EditorObjectData> objDatalist = JsonConvert.DeserializeObject<List<EditorObjectData>>(datalistJson);
methodDic.Clear();
StringBuilder sb = new StringBuilder();
//添加引用
sb.AppendLine("/*---------------------------------");
sb.AppendLine(" *Title:UI表现层脚本自动化生成工具");
sb.AppendLine(" *Author:ZM 铸梦");
sb.AppendLine(" *Date:" + System.DateTime.Now);
sb.AppendLine(" *Description:UI 表现层,该层只负责界面的交互、表现相关的更新,不允许编写任何业务逻辑代码");
sb.AppendLine(" *注意:以下文件是自动生成的,再次生成不会覆盖原有的代码,会在原有的代码上进行新增,可放心使用");
sb.AppendLine("---------------------------------*/");
sb.AppendLine("using UnityEngine.UI;");
sb.AppendLine("using UnityEngine;");
sb.AppendLine("using ZMUIFrameWork;");
sb.AppendLine();
//生成类名
sb.AppendLine($"\tpublic class {name}:WindowBase");
sb.AppendLine("\t{");
sb.AppendLine("\t");
if (GeneratorConfig.GeneratorType == GeneratorType.Bind)
{
//生成字段
sb.AppendLine($"\t\t public {name}DataComponent dataCompt;");
}
else
{
//生成字段
sb.AppendLine($"\t\t public {name}UIComponent uiCompt=new {name}UIComponent();");
}
//生成生命周期函数 Awake
sb.AppendLine("\t");
sb.AppendLine($"\t\t #region 声明周期函数");
sb.AppendLine($"\t\t //调用机制与Mono Awake一致");
sb.AppendLine("\t\t public override void OnAwake()");
sb.AppendLine("\t\t {");
if (GeneratorConfig.GeneratorType == GeneratorType.Bind)
{
sb.AppendLine($"\t\t\t dataCompt=gameObject.GetComponent<{name}DataComponent>();");
sb.AppendLine($"\t\t\t dataCompt.InitComponent(this);");
}
else
sb.AppendLine($"\t\t\t uiCompt.InitComponent(this);");
sb.AppendLine("\t\t\t base.OnAwake();");
sb.AppendLine("\t\t }");
//OnShow
sb.AppendLine($"\t\t //物体显示时执行");
sb.AppendLine("\t\t public override void OnShow()");
sb.AppendLine("\t\t {");
sb.AppendLine("\t\t\t base.OnShow();");
sb.AppendLine("\t\t }");
//OnHide
sb.AppendLine($"\t\t //物体隐藏时执行");
sb.AppendLine("\t\t public override void OnHide()");
sb.AppendLine("\t\t {");
sb.AppendLine("\t\t\t base.OnHide();");
sb.AppendLine("\t\t }");
//OnDestroy
sb.AppendLine($"\t\t //物体销毁时执行");
sb.AppendLine("\t\t public override void OnDestroy()");
sb.AppendLine("\t\t {");
sb.AppendLine("\t\t\t base.OnDestroy();");
sb.AppendLine("\t\t }");
sb.AppendLine($"\t\t #endregion");
//API Function
sb.AppendLine($"\t\t #region API Function");
sb.AppendLine($"\t\t ");
sb.AppendLine($"\t\t #endregion");
//UI组件事件生成
sb.AppendLine($"\t\t #region UI组件事件");
foreach (var item in objDatalist)
{
string type = item.fieldType;
string methodName = "On" + item.fieldName;
string suffix = "";
if (type.Contains("Button"))
{
suffix = "ButtonClick";
CreateMethod(sb, ref methodDic, methodName + suffix);
}
else if (type.Contains("InputField"))
{
suffix = "InputChange";
CreateMethod(sb, ref methodDic, methodName + suffix, "string text");
suffix = "InputEnd";
CreateMethod(sb, ref methodDic, methodName + suffix, "string text");
}
else if (type.Contains("Toggle"))
{
suffix = "ToggleChange";
CreateMethod(sb, ref methodDic, methodName + suffix, "bool state,Toggle toggle");
}
}
sb.AppendLine($"\t\t #endregion");
sb.AppendLine("\t}");
return sb.ToString();
}
/// <summary>
/// 生成UI事件方法
/// </summary>
/// <param name="sb"></param>
/// <param name="methodDic"></param>
/// <param name="methodName"></param>
/// <param name="param"></param>
public static void CreateMethod(StringBuilder sb, ref Dictionary<string, string> methodDic, string methodName, string param = "")
{
//声明UI组件事件
sb.AppendLine($"\t\t public void {methodName}({param})");
sb.AppendLine("\t\t {");
sb.AppendLine("\t\t");
if (methodName == "OnCloseButtonClick")
{
sb.AppendLine("\t\t\tHideWindow();");
}
sb.AppendLine("\t\t }");
//存储UI组件事件 提供给后续新增代码使用
StringBuilder builder = new StringBuilder();
builder.AppendLine($"\t\t public void {methodName}({param})");
builder.AppendLine("\t\t {");
builder.AppendLine("\t\t");
builder.AppendLine("\t\t }");
methodDic.Add(methodName, builder.ToString());
}
}
UIWindowEditor
编写一个可以预览生成的代码脚本的窗口工具,因为Unity每次生成新的脚本,都需要编译,很麻烦,有这个工具就可以先看看生成的脚本是否正确,再去生成,这样就节约掉了排错的时候,Unity编译脚本的时间。
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text.RegularExpressions;
public class UIWindowEditor : EditorWindow
{
private string scriptContent; // 脚本内容
private string filePath; // 脚本文件路径
private Vector2 scroll = new Vector2();
/// <summary>
/// 显示代码展示窗口
/// </summary>
public static void ShowWindow(string content, string filePath, Dictionary<string, string> insterDic = null)
{
// 创建代码展示窗口
UIWindowEditor window = (UIWindowEditor)GetWindowWithRect(typeof(UIWindowEditor), new Rect(100, 50, 800, 700), true, "Window生成界面");
window.scriptContent = content;
window.filePath = filePath;
// 处理代码新增
if (File.Exists(filePath) && insterDic != null)
{
// 获取原始代码
string originScript = File.ReadAllText(filePath);
foreach (var item in insterDic)
{
// 如果老代码中没有这个代码就进行插入操作
if (!originScript.Contains(item.Key))
{
int index = window.GetInserIndex(originScript);
originScript = window.scriptContent = originScript.Insert(index, item.Value + "\t\t");
}
}
}
window.Show();
}
public void OnGUI()
{
// 绘制ScroView
scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.Height(600), GUILayout.Width(800));
EditorGUILayout.TextArea(scriptContent);
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
// 绘制脚本生成路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextArea("脚本生成路径:" + filePath);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// 绘制按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("生成脚本", GUILayout.Height(30)))
{
// 按钮事件
ButtonClick();
}
EditorGUILayout.EndHorizontal();
}
public void ButtonClick()
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
StreamWriter writer = File.CreateText(filePath);
writer.Write(scriptContent);
writer.Close();
AssetDatabase.Refresh();
if (EditorUtility.DisplayDialog("自动化生成工具", "生成脚本成功!", "确定"))
{
Close();
}
}
/// <summary>
/// 获取插入代码的下标
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public int GetInserIndex(string content)
{
// 找到UI事件组件下面的第一个public 所在的位置 进行插入
Regex regex = new Regex("UI组件事件");
Match match = regex.Match(content);
Regex regex1 = new Regex("public");
MatchCollection matchCollection = regex1.Matches(content);
for (int i = 0; i < matchCollection.Count; i++)
{
if (matchCollection[i].Index > match.Index)
{
return matchCollection[i].Index;
}
}
return -1;
}
}
七、堆栈系统
堆栈系统是任何游戏必不可少的一项功能。它用作于首次进入大厅时一些特殊或活动面板的游戏自动弹出,从而让顽疾啊能够更好的取了解到游戏内容和新增功能,以及游戏内,一些弹窗的反复多次的弹出。
这个堆栈系统使用的是队列容器实现的,因为栈是后进先出,而服务器发送消息的时间是不确定的,如果是后进先出,那么很有可能在弹出奖励1后,奖励2和3先后入栈,导致下一个弹出的窗口是奖励3,打乱了窗口弹出顺序,而队列的先进先出就很好的解决了这个问题。
在以下脚本中新增代码以实现堆栈系统
UIModule
private Queue<WindowBase> mWindowStack = new Queue<WindowBase>(); // 队列 用来管理弹窗的循环弹出
private bool mStartPopStackWndStatus = false; // 开始弹出堆栈的表只 可以用来处理多种情况 比如:正在出栈种有其他界面弹出 可以直接放到栈内进行弹出 等
private void HideWindow(WindowBase window)
{
// 面板不为空 且 是可见的
if (window != null && window.Visible)
{
mVisibleWindowList.Remove(window);
window.SetVisible(false); // 隐藏弹窗物体
SetWidnowMaskVisible();
window.OnHide();
}
// 在出栈的情况下,上一个界面隐藏时,自动打开栈种的下一个界面
PopNextStackWindow(window);
}
private void DestoryWindow(WindowBase window)
{
if (window != null)
{
if (mAllWindowDic.ContainsKey(window.Name))
{
// 在对应容器中 移除对应的面板
mAllWindowDic.Remove(window.Name);
mAllWindowList.Remove(window);
mVisibleWindowList.Remove(window);
}
window.SetVisible(false);
SetWidnowMaskVisible();
window.OnHide();
window.OnDestroy();
GameObject.Destroy(window.gameObject);
// 在出栈的情况下 上一个界面销毁时 自动打开栈种的下一个界面
PopNextStackWindow(window);
}
}
#region 堆栈系统
/// <summary>
/// 进栈一个界面
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="popCallBack"></param>
public void PushWindowToStack<T>(Action<WindowBase> popCallBack = null) where T : WindowBase, new()
{
T wndBase = new T();
wndBase.PopStackListener = popCallBack;
mWindowStack.Enqueue(wndBase);
}
/// <summary>
/// 弹出堆栈中第一个弹窗
/// </summary>
public void StartPopFirstStackWindow()
{
if (mStartPopStackWndStatus) return;
mStartPopStackWndStatus = true;//已经开始进行堆栈弹出的流程,
PopStackWindow();
}
/// <summary>
/// 压入并且弹出堆栈弹窗
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="popCallBack"></param>
public void PushAndPopStackWindow<T>(Action<WindowBase> popCallBack = null) where T : WindowBase, new()
{
PushWindowToStack<T>(popCallBack);
StartPopFirstStackWindow();
}
/// <summary>
/// 弹出堆栈中的下一个窗口
/// </summary>
/// <param name="windowBase"></param>
private void PopNextStackWindow(WindowBase windowBase)
{
if (windowBase != null && mStartPopStackWndStatus && windowBase.PopStack)
{
windowBase.PopStack = false;
PopStackWindow();
}
}
/// <summary>
/// 弹出堆栈弹窗
/// </summary>
/// <returns></returns>
public bool PopStackWindow()
{
if (mWindowStack.Count > 0)
{
WindowBase window = mWindowStack.Dequeue();
WindowBase popWindow = PopUpWindow(window);
popWindow.PopStackListener = window.PopStackListener;
popWindow.PopStack = true;
popWindow.PopStackListener?.Invoke(popWindow);
popWindow.PopStackListener = null;
return true;
}
else
{
mStartPopStackWndStatus = false;
return false;
}
}
/// <summary>
/// 清空缓存队列
/// </summary>
public void ClearStackWindows()
{
mWindowStack.Clear();
}
#endregion
WindowBehaviour
public bool PopStack { get; set; }//是否是通过堆栈系统弹出的弹窗
public Action<WindowBase> PopStackListener { get; set; }
八、弹出和关闭窗口动画实现
主要使用到了DoTween
插件实现这个动态效果。前面制作的时候就把所有的面板元素放在了UIContent
下,所以现在只需要缩放UIContent
即可,并不会影响到其他元素的缩放。
在以下脚本中新增代码以实现弹出动画
WindowBase
protected bool mDisableAnim = false; // 禁用动画
#region 动画管理
public void ShowAnimation()
{
//基础弹窗不需要动画
if (Canvas.sortingOrder > 90 && mDisableAnim == false)
{
//Mask动画
mUIMask.alpha = 0;
mUIMask.DOFade(1, 0.2f);
//缩放动画
mUIContent.localScale = Vector3.one * 0.8f;
mUIContent.DOScale(Vector3.one, 0.3f).SetEase(Ease.OutBack);
}
}
public void HideAnimation()
{
if (Canvas.sortingOrder > 90 && mDisableAnim == false)
{
mUIContent.DOScale(Vector3.one * 1.1f, 0.2f).SetEase(Ease.OutBack).OnComplete(() =>
{
UIModule.Instance.HideWindow(Name);
});
}
else
{
UIModule.Instance.HideWindow(Name);
}
}
#endregion
九、高性能系统
高性能系统是为了解决UI性能问题,增加游戏流畅度而设计的一系列性能解决方案,主要针对渲染、重绘、顶点、UI组件等多个方面进行性能的优化处理。主要有以下几个功能点:
1.一键优化合批。自动根据图集图片和相邻组件的特征进行重写排序。
2.避免使用SetActive引起的UI重绘和GC垃圾。 用CanvasGroup和Scale进行替代。
3.使用UI对象池。避免频繁的克隆物体导致的卡顿和GC。
4.智能化禁用不必要的组件属性。从而避免一些不必要的性能开销。
5.界面预加载。针对复杂一些的界面我们可以使用预加载进行提前加载物体,来确保在真正使用界面时,能够流畅加载出界面。
6.高性能文字描边。Unity描边组件时拷贝4份相同的文本顶点数占用量巨大。一个字母的Text加上Unity的描边一共占用30个顶点。而这个框架的Text同样是一个字母加上描边能做到只占用6个顶点,性能是Unity组件的5倍。
7.组件自动序列化。避免使用Find接口查找组件带来的性能开销,而使用自动序列化的方式取得组件,将性能消耗极可能降到最低。
预加载实现:UIModule
/// <summary>
/// 只加载物体,不调用生命周期
/// </summary>
/// <typeparam name="T"></typeparam>
public void PreLoadWindow<T>() where T : WindowBase, new()
{
System.Type type = typeof(T);
string wndName = type.Name;
T windowBase = new T();
//克隆界面,初始化界面信息
//1.生成对应的窗口预制体
GameObject nWnd = TempLoadWindow(wndName);
//2.初始出对应管理类
if (nWnd != null)
{
windowBase.gameObject = nWnd;
windowBase.transform = nWnd.transform;
windowBase.Canvas = nWnd.GetComponent<Canvas>();
windowBase.Canvas.worldCamera = mUICamera;
windowBase.Name = nWnd.name;
windowBase.OnAwake();
windowBase.SetVisible(false);
RectTransform rectTrans = nWnd.GetComponent<RectTransform>();
rectTrans.anchorMax = Vector2.one;
rectTrans.offsetMax = Vector2.zero;
rectTrans.offsetMin = Vector2.zero;
mAllWindowDic.Add(wndName, windowBase);
mAllWindowList.Add(windowBase);
}
Debug.LogError("预加载窗口 窗口名字:" + wndName);
}
避免使用SetActive引起的UI重绘和GC垃圾
通过CanvasGroup
这个组件设置面板的显示和隐藏,从而解决面板每次显示隐藏时候的网格重绘。因为CanvasGroup
是不会引起网格重绘的,所以在设置一些UI元素的显示和隐藏,也尽量可以使用这种方法,给每个组件添加一个拓展方法。
private CanvasGroup mCanvasGroup;
public override void SetVisible(bool isVisble)
{
mCanvasGroup.alpha = isVisble ? 1 : 0;
mCanvasGroup.blocksRaycasts = isVisble;
Visible = isVisble;
}
UGUIAgent
每个UI组件添加一个设置CanvasGroup
的拓展方法,便于使用。
using UnityEngine;
using UnityEngine.UI;
public static class UGUIAgent
{
public static void SetVisible(this GameObject obj, bool visible)
{
obj.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this Transform trans, bool visible)
{
trans.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this Button button, bool visible)
{
button.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this Text text, bool visible)
{
text.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this Slider slider, bool visible)
{
slider.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this Toggle toggle, bool visible)
{
toggle.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this InputField input, bool visible)
{
input.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this RawImage image, bool visible)
{
image.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
public static void SetVisible(this ScrollRect scroll, bool visible)
{
scroll.transform.localScale = visible ? Vector3.one : Vector3.zero;
}
}
智能化禁用不必要的组件属性,并自动赋值Canvas上面的Camera。
禁用UI组件上一些不必要的属性,避免性能开销。比如:Text组件的Raycast Target属性。
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
public class SyStemUiEditor : Editor
{
[InitializeOnLoadMethod]
private static void InitEditor()
{
//监听hierarchy发生改变的委托
EditorApplication.hierarchyChanged += HanderTextOrImageRaycast;
EditorApplication.hierarchyChanged += LoadWindowCamera;
}
private static void HanderTextOrImageRaycast()
{
GameObject obj = Selection.activeGameObject;
if (obj != null)
{
if (obj.name.Contains("Text"))
{
Text text = obj.GetComponent<Text>();
if (text != null)
{
text.raycastTarget = false;
}
}
else if (obj.name.Contains("Image"))
{
Image image = obj.GetComponent<Image>();
if (image != null)
{
image.raycastTarget = false;
}
else
{
RawImage rawImage = obj.GetComponent<RawImage>();
if (rawImage != null)
{
rawImage.raycastTarget = false;
}
}
}
}
}
private static void LoadWindowCamera()
{
if (Selection.activeGameObject != null)
{
GameObject uiCameraObj = GameObject.Find("UICamera");
if (uiCameraObj != null)
{
Camera camera = uiCameraObj.GetComponent<Camera>();
if (Selection.activeGameObject.name.Contains("Window"))
{
Canvas canvas = Selection.activeGameObject.GetComponent<Canvas>();
if (canvas != null)
{
canvas.worldCamera = camera;
}
}
}
}
}
}
一键优化DrawCall
借用第三方开源库UGUI-Editor
,可以在GitHub
下载导入Unity。
未对UI组件进行DrawCall优化。
通过UGUI-Editor对UI组件进行DrawCall优化。
可以看到这个插件可以帮助我们一键优化UI组件的合批处理,减少DrawCall次数,简单方便快捷省时省力。