【Unity】游戏UI框架(基于UGUI)

前几天练习写了这一套Unity的UI框架,对于游戏开发很实用,写篇笔记梳理一下大致思路
工程文件见 github

实际效果

游戏运行加载主菜单页面
在这里插入图片描述

点击对应按钮可以打开具体页面,当有具体页面的时候主菜单按钮无法点击,点击右上角的关闭按钮可以关闭该页面,关闭页面后主菜单按钮继续生效
在这里插入图片描述

UIFramework基本架构

基本架构如下图所示
UIFramework

UIPanel部分

这一部分是用于描述UI面板属性的一些类,分别是UIPanelType、UIPanel、BasePanel

UIPanelType

这是一个枚举类,用于描述UIPanel的种类。在这里我写了四种,分别是主菜单面板MainMenuPanel、商店面板StorePanel、暂停面板PausePanel和系统设置面板SystemSettingPanel(注意:面板的prefab名称需要与枚举类的类型名称相同)

public enum UIPanelType
{
    MainMenuPanel,
    SystemSettingPanel,
    PausePanel,
    StorePanel
}

UIPanel

这个类用于储存面板本身文件的信息,有两个字段,一个字段用来描述该面板UIPanelType的类型,一个字段储存该面板prefab文件的路径,以便在游戏运行时动态实例化面板

public class UIPanel
{   
    public UIPanelType UIPanelType;
    public string UIPanelPath;
}

BasePanel

这是所有面板上挂载的Script的基类,所以需要继承自MonoBehaviour。BasePanel里的方法用来描述所有面板共同的一些基本行为,例如打开与关闭,而面板的四个状态:进入场景OnEnter、暂停OnPause、继续OnResume(解除暂停)、推出场景OnExit也是属于这个类的四个方法。
方法的具体实现以及其他字段方法待后期需要再添加。

using UnityEngine;
using UnityEngine.UI;

public class BasePanel : MonoBehaviour
{
    public void OnEnter()
    {
    }
    public void OnPause()
    {
    }
    public void OnResume()
    {
    }
    public void OnExit()
    {
    }     
}

Manager部分

UIManager

UIManager类的该UI框架的核心,它负责的工作如下:
1.自动更新Json文件记载的指定的面板prefab文件夹下的prefab的UIPanelType与路径
2.使用列表储存所有UIPanel
3.使用字典储存所有UIPanel上挂载的继承自BasePanel的组件/script
4.使用栈储存场景中正在显示的UIPanel

单例模式

把UIManager做成单例模式

private static UIManager _instance;
public static UIManager Instance
{
        get
        {
            if (_instance == null)
                _instance = new UIManager();
            return _instance;
        }
}

1.自动更新Json

首先引入LitJson库,然后引入命名空间using LitJson
指定面板prefab所存放的文件夹,这里我放在Resources下的UIPanelPrefab文件夹下,方便Unity打包与加载

readonly string panelPrefabPath = Application.dataPath + @"/Resources/UIPanelPrefab";

指定Json文件所存放的文件夹,因为Resources文件夹在游戏打包后是只读的,需要写入操作的Json文件就需要单独另开一个文件夹,这里我放在Json文件夹下,Json文件命名为UIJson

readonly string jsonPath = Application.dataPath + @"/Json/UIJson.json";

使用List列表储存所有的UIPanel

private List<UIPanel> panelList;

写一个传入Json路径,就可以读取Json文件并转换为List类型的列表返回的方法

public List<UIPanel> ReadJsonFile(string jsonPath)
{
	//如果找不到UIJson文件,则新建一个Json文件并写入“[]”
	//如果仅新建一个空Json文件,Json转换会返回一个null,也就是后面的list等于null,之后使用list的操作就会报一个空指针错误。
	if (!File.Exists(jsonPath))
            File.WriteAllText(jsonPath, "[]");
    List<UIPanel> list = JsonMapper.ToObject<List<UIPanel>>(File.ReadAllText(jsonPath));

    return list;
}

再写一个传入Json路径和List类型的UIPanel的列表,可以把该列表内容写入该路径的Json文件的方法

public void WriteJsonFile(string jsonPath, List<UIPanel> list)
{
    string json = JsonMapper.ToJson(list);
    File.WriteAllText(jsonPath, json);
}

最后写自动更新Json文件的方法UIPanelInfoSaveInJson()

public void UIPanelInfoSaveInJson()
    {    	
        panelList = ReadJsonFile(jsonPath);//读取现有json里的UIPanelList

	//读取储存面板prefab的文件夹的信息
        DirectoryInfo folder = new DirectoryInfo(panelPrefabPath);

	//遍历储存面板prefab的文件夹里每一个prefab的名字,并把名字转换为对应的UIPanelType
	//再检查UIPanelType是否存在于List里,若存在List里则更新path,若不存在List里则加上
        foreach (FileInfo file in folder.GetFiles("*.prefab"))
        {
            UIPanelType type = (UIPanelType)Enum.Parse(typeof(UIPanelType), file.Name.Replace(".prefab", ""));
            //这里的path是相对路径,因为加载时使用的是Resources.load
            string path = @"UIPanelPrefab/" + file.Name.Replace(".prefab", "");

            bool UIPanelExistInList = false;//默认该UIPanel不在现有UIPanelList中
            
		//在List里查找type相同的UIPanel
		//SearchPanelForType是一个List的扩展方法,具体见Extension部分
            UIPanel uIPanel = panelList.SearchPanelForType(type);

            if (uIPanel != null)//UIPanel在该List中,更新path值
            {
                uIPanel.UIPanelPath = path;
                UIPanelExistInList = true;
            }

            if (UIPanelExistInList == false)//UIPanel不在List中,加上该UIPanel
            {
                UIPanel panel = new UIPanel
                {
                    UIPanelType = type,
                    UIPanelPath = path
                };
                panelList.Add(panel);
            }
        }

        WriteJsonFile(jsonPath, panelList);//把更新后的UIPanelList写入Json

        AssetDatabase.Refresh();//刷新资源
    }

UIPanelInfoSaveInJson()需要在UIManager的构造方法里调用,因为需要在游戏运行开始就更新Json文件,以免后面加载的仍然是旧Json文件里记录的prefab路径

 private UIManager()//【单例模式】构造方法为私有,UIManager的实例仅能在类内构造
    {
        //自动解析Json文件并赋值给panelList
        //并使用prefab文件夹下的信息对panelList进行更新,再写入json文件
        UIPanelInfoSaveInJson();
    }

使用字典储存 面板类型UIPanelType :实例面板BasePanel 键值对

private Dictionary<UIPanelType, BasePanel> panelDict;

开发一个给出面板类型,就实例化该面板,并返回面板上挂载的BasePanel组件的方法

先获得场景中的Canvas,以便实例化的面板设为Canvas的子物体

private Transform canvasTransform;
    public Transform CanvasTransform
    {
        get
        {
            if (canvasTransform == null)
            //通过名称获取Canvas上的Transform,所以不要有同名Canvas
                canvasTransform = GameObject.Find("Canvas").transform;
            return canvasTransform;
        }
    }

GetPanel方法

public BasePanel GetPanel(UIPanelType type)
    {
        if (panelDict == null)
            panelDict = new Dictionary<UIPanelType, BasePanel>();
		//【扩展发放】通过type查找字典里对应的BasePanel,若找不到则返回null,具体见Extension部分
        BasePanel panel = panelDict.TryGetValue(type);

        //在现有字典里没有找到
        //只能去json里找type对应的prefab的路径并加载
        //再加进字典里以便下次在字典中查找
        if (panel == null)
        {
        	//【扩展方法】通过Type查找列表里对应的UIPanel,若找不到则返回null,具体见Extension部分
            string path = panelList.SearchPanelForType(type).UIPanelPath;
            if (path == null)
                throw new Exception("找不到该UIPanelType的Prefab");

            if (Resources.Load(path) == null)
                throw new Exception("找不到该Path的Prefab");
			//实例化prefab
            GameObject instPanel = GameObject.Instantiate(Resources.Load(path)) as GameObject;
            //把面板设为Canvas的子物体,false表示不保持worldPosition,而是根据Canvas的坐标设定localPosition
            instPanel.transform.SetParent(CanvasTransform, false);

            panelDict.Add(type, instPanel.GetComponent<BasePanel>());

            return instPanel.GetComponent<BasePanel>();
        }

        return panel;
    }

接下来就可以进行UIManager的最后一项工作——开发一个栈来储存当前场景中正在显示的Panel
因为面板是后打开的面板先关闭,栈是最适合储存的数据结构

private Stack<BasePanel> panelStack;

开发把指定类型的Panel入栈的方法PushPanel()

//把指定类型的panel入栈,并显示在场景中
    public void PushPanel(UIPanelType type)
    {
        if (panelStack == null)
            panelStack = new Stack<BasePanel>();

        //判断栈里是否有其他panel,若有,则把原栈顶panel设定其状态为暂停(OnPause)
        if (panelStack.Count > 0)
        {
            BasePanel topPanel = panelStack.Peek();
            topPanel.OnPause();
        }

        BasePanel panel = GetPanel(type);

        //把指定类型的panel入栈并设定其状态为进入场景(OnEnter)
        panelStack.Push(panel);
        panel.OnEnter();
    }

开发把栈顶的Panel出栈的方法PopPanel()

//把栈顶panel出栈,并从场景中消失
    public void PopPanel()
    {
        if (panelStack == null)
            panelStack = new Stack<BasePanel>();
		
		//检查栈是否为空,若为空则直接退出方法
        if (panelStack.Count <= 0) return;

		//把栈顶panel出栈,并把该panel状态设为退出场景(OnExit)
        BasePanel topPanel = panelStack.Pop();
        topPanel.OnExit();

		//再次检查出栈栈顶Panel后栈是否为空
		//若为空,直接退出方法
		//若不为空,则把新的栈顶Panel状态设为继续(OnResume)
        if (panelStack.Count <= 0) return;

        BasePanel topPanel2 = panelStack.Peek();
        topPanel2.OnResume();
    }

至此,UIManager开发完毕。

GameRoot

这个类负责在游戏开始的时候加载所有需要的游戏配置,在该UI框架里,GameRoot负责在游戏刚开始运行时加载主菜单面板MainMenuPanel。
由于这个类需要挂在场景中的物体上,所以需要继承自MonoBehaviour

using UnityEngine;

public class GameRoot : MonoBehaviour
{
    void Start ()
    {
        //在游戏开始时加载MainMenuPanel
        UIManager.Instance.PushPanel(UIPanelType.MainMenuPanel);
    }
}

Extension部分

该部分是对基类库的一些类的扩展,在这里扩展了List列表类和Dictionary字典类

List扩展

using System.Collections.Generic;

public static class ListExtension
{
    /// <summary>
    /// 扩展List类
    /// 查找字段是指定UIPanelType的UIPanel,返回UIPanel的引用
    /// </summary>
    /// <param name="list">UIPanel的List</param>
    /// <param name="type"></param>
    /// <returns></returns>
    /// 
    public static UIPanel SearchPanelForType(this List<UIPanel> list,UIPanelType type)
    {
        foreach (var item in list)
        {
            if (item.UIPanelType == type)
                return item;
        }

        return null;
    }
}

Dictionary扩展

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class DictionaryExtension
{
    /// <summary>
    /// 扩展字典类中的TryGetValue方法
    /// 可以直接通过给出key返回value,而不是像原方法一样返回bool值
    /// </summary>
    /// <typeparam name="TKey"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <param name="dict"></param>
    /// <param name="key"></param>
    /// <returns></returns>

    public static TValue TryGetValue<TKey,TValue>(this Dictionary<TKey,TValue> dict,TKey key)
    {
        TValue value;
        dict.TryGetValue(key, out value);

        return value;
    }
}

最后完善BasePanel类

首先实现BasePanel里的四个状态的方法
我通过CanvasGroup组件来控制Panel的可否点击与出现消失
canvasGroup.alpha属性等于1时面板出现(OnEnter),等于0时面板消失(OnExit)
canvasGroup.blocksRaycasts属性等于true时面板可以点击(OnResume),等于false时面板不可点击(OnPause)

首先获得该组件,如果Panel上没有CanvasGroup组件,则添加该组件

public void Awake()
    {        
        canvasGroup = GetComponent<CanvasGroup>();
        if(canvasGroup == null)
        {
            canvasGroup = gameObject.AddComponent<CanvasGroup>();
        }
    }

在进入状态(OnEnter)中,Panel是出现且可点击的

public void OnEnter()
    {
        canvasGroup.alpha = 1;
        canvasGroup.blocksRaycasts = true;
    }

在暂停状态(OnPause)中,Panel是出现且不可点击的

public void OnPause()
    {
        canvasGroup.alpha = 1;
        canvasGroup.blocksRaycasts = false;
    }

在继续状态(OnResume),Panel是出现且可点击的

public void OnResume()
    {
        canvasGroup.alpha = 1;
        canvasGroup.blocksRaycasts = true;
    }

在退出状态(OnExit),Panel是消失且不可点击的

public void OnExit()
    {
        canvasGroup.alpha = 0;
        canvasGroup.blocksRaycasts = false;
    }    

如果有Panel的四个状态的逻辑与BasePanel中不同,或者希望加一些进入消失动画,可以在对应的派生类中对这四个方法进行override

大多数面板都有关闭按钮,所以在BasePanel里也可以写一个检测子物体中是否有一个叫做CloseButton的按钮,并绑定监听事件

//给出子物体名字,并尝试在子物体中搜索到以该名字命名的子物体的button组件
//搜索到了返回button组件,未搜索到则返回null
private Button FindCloseButton(string childName)
    {
        Button closeButton = null;
        foreach (var item in GetComponentsInChildren<Button>())
        {
            if (item.name == childName)
                closeButton = item.GetComponent<Button>();
        }
        return closeButton;
    }

然后在Awake里绑定按钮点击监听事件,如果点击了名为CloseButton的物体上的Button,就让panelStack栈顶元素出栈(关闭当前页面最上方的页面)

public void Awake()
    {
        btn = FindCloseButton("CloseButton");
        canvasGroup = GetComponent<CanvasGroup>();

        if (btn != null)
        {
            btn.onClick.AddListener(UIManager.Instance.PopPanel);
        }

        if(canvasGroup == null)
        {
            canvasGroup = gameObject.AddComponent<CanvasGroup>();
        }
    }

所以如果要为Panel设置关闭按钮,就要把关闭按钮命名为CloseButton,并作为Panel的子物体。
如果找不到CloseButton,这个页面就是无法关闭的,例如主菜单页面MainMenuPanel

写主菜单页面上三个按钮的点击事件并绑定到按钮的监听事件上

public class MainMenuPanel : BasePanel
{
    public void OnClickSystemSettingButton()
    {
        UIManager.Instance.PushPanel(UIPanelType.SystemSettingPanel);
    }

    public void OnClickStoreButton()
    {
        UIManager.Instance.PushPanel(UIPanelType.StorePanel);
    }

    public void OnClickPauseButton()
    {
        UIManager.Instance.PushPanel(UIPanelType.PausePanel);
    }
}

在这里插入图片描述

总结

UI框架搭建完毕,之后需要添加Panel的时候,只需要做好Prefab,然后在UIPanelType里添加其对应的枚举类型,并以此枚举类型为Prefab命名,然后把Prefab放到指定的文件夹里即可。

  • 40
    点赞
  • 198
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
Unity UI框架是一种用于创建用户界面(UI)的工具集和组件集合。它提供了丰富的UI元素和功能,可以帮助开发者轻松地构建交互性和可视化的界面。 Unity UI框架可以通过Unity Asset Store进行下载。打开Unity编辑器,点击"Window"菜单,选择"Asset Store"选项,会打开Asset Store窗口。在搜索栏中输入"Unity UI框架",可以找到很多可用的UI框架。 在选择和下载UI框架之前,开发者应该了解项目的需求并进行评估。可以根据项目的规模、复杂度和用户需求来选择合适的UI框架。一些常用的UI框架包括:"TextMeshPro"、"DOTween" 和 "UGUI"等。开发者可以根据自己的需要选择适合的框架。 下载UI框架后,可以将其导入到Unity项目中。在Asset Store或者Unity Package Manager中选择合适的UI框架,点击"Download" 或者 "Import"按钮进行安装。安装完成后,可以在Unity编辑器的"Assets"文件夹中找到导入的UI框架。 使用UI框架时,可以在Unity编辑器中创建UI元素,如按钮、文本、滑动条等,并对其进行布局和样式设置。可以通过脚本代码来实现交互性功能,如按钮的点击事件、输入框的文本处理等。UI框架还提供了很多可定制的功能和效果,如动画、过渡和绘制等,可以增强用户界面的视觉效果和交互体验。 总之,Unity UI框架是一个方便使用的工具集,可以帮助开发者快速构建用户界面。通过下载合适的UI框架,开发者可以轻松地创建、布局和管理UI元素,提供良好的用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值