基于Unity的UI框架 Demo展示
关键类 MonoSingle 继承MonoBehaviour的单例基类;做了一些特殊处理;
保证场景中必须有GameInit名称的物体,所有单例管理器脚本都挂在该物体上;
继承单例基类后,需要私有化构造;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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的面板都需要在这个类中添加常量字段,方便比对;
1 2 3 4 5 6 7 8 9 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面板的父节点
1 2 3 4 5 6 7 public enum UILayer { Back, //背景层 Default, //默认层 Pop, //弹窗 Top //顶层,适用悬浮等 }
UIBase 所有UI面板的基类;创建新的UI面板必须继承UIBase同时重写虚方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 //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就手动添加了;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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主界面,打开后一直显示,不会隐藏;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 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等逻辑相似,就只分析一个了;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 1 2 3 4 5 6 7 8 9 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