Unity——基于UGUI的UI框架
阅读原文时间:2023年07月11日阅读:2

基于UGUI的UI框架



MonoSingle

继承MonoBehaviour的单例基类;做了一些特殊处理;

保证场景中必须有GameInit名称的物体,所有单例管理器脚本都挂在该物体上;

继承单例基类后,需要私有化构造;

public class MonoSingle<T> : MonoBehaviour where T :MonoSingle<T>
{
    protected static T instance;

    public static T I
    {
        get
        {
            if (instance == null)
            {
                GameObject go = GameObject.Find("GameInit");
                if (go == null)
                {
                    go = new GameObject("GameInit");
                    DontDestroyOnLoad(go);
                }

                instance = go.GetComponent<T>();

                if (instance == null)
                    instance = go.AddComponent<T>();
            }
            return instance;
        }
    }
}

UIType

所有UI的面板都需要在这个类中添加常量字段,方便比对;

public class UIType
{
    public const string UIMain = "panmain";
    public const string UIInventory = "paninventory";
    public const string UIShop = "panshop";
    public const string UIQuest = "panquest";
    public const string UIEquipment = "panequipment";
    public const string UISkill = "panskill";
}

创建UI层级枚举,根据层级设置UI面板的父节点

public enum UILayer
{
    Back,      //背景层
    Default,   //默认层
    Pop,       //弹窗
    Top        //顶层,适用悬浮等
}

UIBase

所有UI面板的基类;创建新的UI面板必须继承UIBase同时重写虚方法

public abstract class UIBase : MonoBehaviour
{
    //层级字段,根据层级设置父节点
    public UILayer uiLayer;
    //UI类型字段
    public string uiType;

    //面板进入时调用
    public virtual void OnEnter()
    {
        //设置父节点
        transform.SetParent(UIManager.I.dicLayer[uiLayer]);
    }

    //面板停止时调用(鼠标与面板的交互停止)
    public virtual void OnPause()
    {
    }

    //面板恢复使用时调用(鼠标与面板的交互恢复)
    public virtual void OnResume()
    {
    }

    //面板退出时调用
    public virtual void OnExit()
    {
    }
}

UIManager

//partial拆分类,有点像.h和.cpp的区别,但是只是把一个类分成两个写
public partial class UIManager : MonoSingle<UIManager>
{
    //所有UI面板perfab的路径key:UIType——value:Resources下的路径
    public Dictionary<string, string> dicPath;
    //根据上面路径,加载的好的具体的UI面板类
    public Dictionary<string, UIBase> dicPanel;
    //栈存储所有打开的UI面板,打开UI,push栈,关闭UI,pop栈
    private Stack<UIBase> panelStack;
    //canvas.transform 方便设置父节点
    public Transform canvasTf;
    //存储UI层级节点,方便设置父节点
    public Dictionary<UILayer, Transform> dicLayer;
    //存储UI层级对应的名称,用来加载ui层级节点时命名
    public Dictionary<UILayer, string> dicLayerName;

    private UIManager()
    {
        //初始化perfab路径和ui层级节点
        InitPath();
        InitUILayer();
    }

    public void Awake()
    {
        canvasTf = GameObject.Find("Canvas").transform;
        //加载层级节点
        LoadLayer();
    }

    private void LoadLayer()
    {
        dicLayer = new Dictionary<UILayer, Transform>();
        for (int i = 0; i < dicLayerName.Count; ++i)
        {
            GameObject layer = new GameObject(dicLayerName[(UILayer)i]);
            layer.transform.SetParent(canvasTf);
            dicLayer.Add((UILayer) i, layer.transform);
        }
    }

    //获取dicPanel中存储的基层UIBase的类,如果为空,先加载添加进去
    private UIBase GetPanel(string panelType)
    {
        if (dicPanel == null)
            dicPanel = new Dictionary<string, UIBase>();

        panelType = panelType.ToLower();
        if (dicPanel.ContainsKey(panelType))
            return dicPanel[panelType];
        else
        {
            string path = string.Empty;
            if (dicPath.ContainsKey(panelType))
                path = dicPath[panelType];
            else
                return null;

            GameObject go = Resources.Load<GameObject>(path);
            GameObject goPanel = GameObject.Instantiate(go, canvasTf, false);
            UIBase panel = goPanel.GetComponent<UIBase>();
            dicPanel.Add(panelType, panel);
            return panel;
        }
    }

    //打开UI界面
    public void PushPanel(string panelType)
    {
        if (panelStack == null)
        {
            panelStack = new Stack<UIBase>();
        }

        //停止上一个界面
        if (panelStack.Count > 0)
        {
            UIBase top = panelStack.Peek();
            top.OnPause();
        }

        UIBase panel = GetPanel(panelType);
        panelStack.Push(panel);
        panel.OnEnter();
    }

    //关闭最上层界面
    public void PopPanel()
    {
        if (panelStack == null)
        {
            panelStack = new Stack<UIBase>();
        }
        if (panelStack.Count <= 0)
        {
            return;
        }

        //退出栈顶面板
        UIBase top = panelStack.Pop();
        top.OnExit();

        //恢复上一个面板
        if (panelStack.Count > 0)
        {
            UIBase panel = panelStack.Peek();
            panel.OnResume();
        }
    }

    //获取最上层面板
    public UIBase GetTopPanel()
    {
        if (panelStack.Count > 0)
            return panelStack.Peek();
        else
            return null;
    }
}

使用partial最主要原因是想把加载和逻辑分开,加载部分类可以使用ScriptableObject自动生成或使用json外部读取;

我这里demo就手动添加了;

public partial class UIManager : MonoSingle<UIManager>
{
    private void InitPath()
    {
        dicPath = new Dictionary<string, string>();
        //自动生成代码,或使用json加载
        dicPath["panmain"] = "UIPanel/PanMain";
        dicPath["paninventory"] = "UIPanel/paninventory";
        dicPath["panskill"] = "UIPanel/panskill";
        dicPath["panquest"] = "UIPanel/panquest";
        dicPath["panequipment"] = "UIPanel/panequipment";
        dicPath["panshop"] = "UIPanel/panshop";
    }

    private void InitUILayer()
    {
        dicLayerName = new Dictionary<UILayer, string>();
        dicLayerName.Add(UILayer.Back,"Front");
        dicLayerName.Add(UILayer.Default,"Default");
        dicLayerName.Add(UILayer.Pop,"Pop");
        dicLayerName.Add(UILayer.Top,"Top");
    }
}

Pan开头类

PanMain主界面,打开后一直显示,不会隐藏;

public class PanMain : UIBase
{
    PanMain()
    {
        //初始化UIBase中层级和类型字段
        uiLayer = UILayer.Default;
        uiType = UIType.UIMain;
    }

    //打开界面按钮,公有字段,inspector界面赋值
    public Button btnInventory;
    public Button btnShop;
    public Button btnSkill;
    public Button btnQuest;
    public Button btnEquipment;

    public override void OnEnter()
    {
        base.OnEnter();
        Debug.Log("打开Mian");

        //封装的按钮添加事件方法,点击打开或关闭
        AddBtnListener(btnEquipment,UIType.UIEquipment);
        AddBtnListener(btnShop,UIType.UIShop);
        AddBtnListener(btnSkill,UIType.UISkill);
        AddBtnListener(btnQuest,UIType.UIQuest);
        AddBtnListener(btnInventory,UIType.UIInventory);
    }

    private void AddBtnListener(Button go,string type)
    {
        go.onClick.AddListener(() =>
        {
            if (UIManager.I.GetTopPanel().uiType == type)
                UIManager.I.PopPanel();
            else
                UIManager.I.PushPanel(type);
        });
    }

    public override void OnPause()
    {
        //取消下面注释,打开新界面时,主界面按钮会失效
        //ChangeBtnState(false);
    }

    public override void OnResume()
    {
        //ChangeBtnState(true);
    }

    public override void OnExit()
    {
    }

    //按钮失效代码
    public void ChangeBtnState(bool value)
    {
        btnInventory.enabled = value;
        btnShop.enabled = value;
        btnSkill.enabled = value;
        btnQuest.enabled = value;
        btnEquipment.enabled = value;
    }
}

PanInventory背包,其他shop,quest,equip,skill等逻辑相似,就只分析一个了;

public class PanInventory : UIBase
{
    PanInventory()
    {
        uiLayer = UILayer.Default;
        uiType = UIType.UIInventory;
    }

    //关闭界面按钮
    public Button btnClose;

    public void Start()
    {
        btnClose.onClick.AddListener(() => { UIManager.I.PopPanel(); });
    }

    public override void OnEnter()
    {
       base.OnEnter();
       this.gameObject.SetActive(true);
    }

    //非顶层时按钮是否失效,可自行设置;
    public override void OnPause()
    {
        //btnClose.enabled = false;
    }

    public override void OnResume()
    {
        //btnClose.enabled = true;
    }

    public override void OnExit()
    {
        this.gameObject.SetActive(false);
    }
}

GameInit

public class GameInit : MonoSingle<GameInit>
{
    private void Awake()
    {
        DontDestroyOnLoad(this.gameObject);
        //打开主界面
        UIManager.I.PushPanel(UIType.UIMain);
    }
}

运行前:

运行后:

由于目前硬件性能和内存完全够用的情况下,所有UI面板加载一次后不会被销毁,只会被隐藏;

改Demo已被我上传至Gitee,可自行下载学习;

https://gitee.com/small-perilla/uiframe-demo