游戏的UI系统往往会比较复杂,工作量比较庞大,需要多人协作完成,为了开发和维护方便,有必要对UI系统进行管理。
一.制作预制件
将UI的各个不同的功能面板制作为预制件,放入Resources目录下,方便加载预制件。
二.开发对应预制件的枚举类,使用json存储预制件名称和地址的对应关系,预制件放在Resources目录下方便加载
public enum UIPanelType { ItemMessagePanel, KnapsackPanel, MainManuPanel, ShopPanel, SkillPanel, SystemPanel, TaskPanel }
{ "infoList": [ { "panelTypeString":"ItemMessagePanel","path":"UI/ItemMessagePanel" }, { "panelTypeString":"KnapsackPanel","path":"UI/KnapsackPanel" }, { "panelTypeString":"MainManuPanel","path":"UI/MainManuPanel" }, { "panelTypeString":"ShopPanel","path":"UI/ShopPanel" }, { "panelTypeString":"SkillPanel","path":"UI/SkillPanel" }, { "panelTypeString":"SystemPanel","path":"UI/SystemPanel" }, { "panelTypeString":"TaskPanel","path":"UI/TaskPanel" } ] }
三.开发UIManager类
UIManager类只能实例化一次,使用单例模式。提供解析json的方法,在对象实例化时调用这个方法并将解析出来的预制件名称和地址使用字典存储。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class UIManager { //存储所有面板prefab路径的字典 private Dictionary<UIPanelType, string> panelPahtDict = new Dictionary<UIPanelType, string>(); private static UIManager _instance; //单例模式,提供get方法 public static UIManager Instance { get { if (_instance == null) _instance = new UIManager(); return _instance; } } //单例模式,将构造方法私有化 private UIManager() { ParseUIPannelJson(); } //解析json private void ParseUIPannelJson() { //读取json的内容 TextAsset textAsset = Resources.Load<TextAsset>("UIPanelType"); //解析json,存储为UIPanelTypeJson内部类对象,该对象包含UIPanelInfo的list,UIPanelInfo的成员变量和json中的数据名称相同 UIPanelTypeJson jsonObject = JsonUtility.FromJson<UIPanelTypeJson>(textAsset.text); //遍历list将对象中的值存储到字典中 foreach(UIPanelInfo info in jsonObject.infoList) { panelPahtDict.Add(info.panelType,info.path); } } public class UIPanelTypeJson { public List<UIPanelInfo> infoList; } }
提供用于实例化的对象模板类,成员变量和json的数据对应好。因为自定义的枚举类型序列化时会出现问题,所以不序列化枚举类型,转而提供对应的字符串变量,然后类继承ISerializationCallbackReceiver类,实现OnAfterDeserialize和OnBeforeSerialize方法,对应序列化前和反序列化后的行为,其中将字符串变量和枚举变量的值相互转化。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; [Serializable] public class UIPanelInfo : ISerializationCallbackReceiver { [NonSerialized] public UIPanelType panelType; public string panelTypeString; public string path; public void OnAfterDeserialize() { panelType = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString); } public void OnBeforeSerialize() { panelTypeString = panelType.ToString(); } }
三.管理所有面板实例化
管理面板的实例化使用多态特性
首先创建一个面板的基类BasePanel
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BasePanel : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
接下来在不同的面板上创建并挂载不同的面板脚本,但是都继承面板基类
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemMessagePanel : BasePanel { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } }
最后在UIManager类中管理面板:
使用字典保存实例化好的面板信息
//保存实例化出来的物体的BasePanel组件 private Dictionary<UIPanelType, BasePanel> panelDict = new Dictionary<UIPanelType, BasePanel>();
获取画布的transform组件,面板需要设置为画布的子物体
//画布的transform组件,用于为面板设定父子级关系等参数 private Transform cavasTransform; private Transform CavasTransform { get { if (cavasTransform == null) cavasTransform = GameObject.Find("Canvas").transform; return cavasTransform; } }
提供获取根据面板类型获取面板上BasePanel组件的方法,如果面板不存在则创建面板
/// <summary> /// 根据给定的panel类型获取物体身上的BasePanel组件 /// </summary> /// <param name="panelType"></param> /// <returns></returns> private BasePanel GetPanel(UIPanelType panelType) { BasePanel panel; if (!panelDict.ContainsKey(panelType)) { string path; panelPathDict.TryGetValue(panelType, out path); GameObject go = GameObject.Instantiate(Resources.Load(path)) as GameObject; //设置panel的父级关系同时设置坐标为局部坐标,否则默认世界坐标显示会出现问题 go.transform.SetParent(CavasTransform,false); panel = go.GetComponent<BasePanel>(); panelDict.Add(panelType, panel); } else { panelDict.TryGetValue(panelType, out panel); } return panel; }
四.面板的显示
面板的显示遵循先显示的后移除,后显示的先移除的原则,所以使用栈管理所有面板的显示
提供存储已显示面板的栈
//使用栈保存显示在cavas中的面板 private Stack<BasePanel> panelStack = new Stack<BasePanel>();
提供入栈的方法,即显示面板
/// <summary> /// 入栈,即显示面板 /// </summary> /// <param name="panelType"></param> public void PushPanel(UIPanelType panelType) { BasePanel panel = GetPanel(panelType); panelStack.Push(panel); }
在画布上挂载UIRoot脚本,用于管理面板的显示
using System.Collections; using System.Collections.Generic; using UnityEngine; public class UIRoot : MonoBehaviour { // Start is called before the first frame update void Start() { UIManager.Instance.PushPanel(UIPanelType.MainManuPanel); } }
提供ButtonRegister脚本,用于注册按钮,按下后显示界面,要显示的界面通过公开的UIPanelTypeString变量指定
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ButtonRegist : MonoBehaviour { public string UIPanelTypeString; private Button btn; private void Awake() { btn = gameObject.GetComponent<Button>(); } private void Start() { btn.onClick.AddListener(OnButtonClick); } private void OnButtonClick() { UIPanelType type = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), UIPanelTypeString); UIManager.Instance.PushPanel(type); } }
五.面板的生命周期
在BasePanel中提供虚拟方法,对应面板的生命周期,在具体面板中实现不同的生命周期行为
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BasePanel : MonoBehaviour { /// <summary> /// 面板显示 /// </summary> public virtual void OnEnter() { } /// <summary> /// 面板暂停 /// </summary> public virtual void OnPause() { } /// <summary> /// 继续面板 /// </summary> public virtual void OnResume() { } /// <summary> /// 退出面板 /// </summary> public virtual void OnExit() { } }
如在主菜单面板中,当其他面板显示时主菜单面板不能点击,使用CanvasGroup组件,将blocksRaycasts变量设置为false使面板不与鼠标交互
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MainManuPanel : BasePanel { private CanvasGroup canvasGroup; void Start() { canvasGroup = GetComponent<CanvasGroup>(); } public override void OnPause() { canvasGroup.blocksRaycasts = false; } }
在UIManager中改写入栈方法,添加暂停栈顶面板的功能
/// <summary> /// 入栈,即显示面板 /// </summary> /// <param name="panelType"></param> public void PushPanel(UIPanelType panelType) { //判断栈顶是否有面板,有的话将栈顶面板暂停 if(panelStack.Count > 0) { panelStack.Peek().OnPause(); } BasePanel panel = GetPanel(panelType); panel.OnEnter(); panelStack.Push(panel); }
六.面板的关闭
面板的关闭需要将面板出栈,所以在出栈方法中实现
/// <summary> /// 出栈,移除最上层的面板 /// </summary> public void PopPanel() { //判断栈顶是否有面板,没有的话直接返回 if (panelStack.Count == 0) return; //弹出栈顶面板,调用其生命周期函数的退出方法 panelStack.Pop().OnExit(); //判断栈内是否还有面板,有的话将其激活 if (panelStack.Count == 0) return; panelStack.Peek().OnResume(); }
修改按钮注册方法,实现面板的关闭按钮的注册,在所有面板预制件的关闭按钮上添加按钮注册脚本
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ButtonRegist : MonoBehaviour { public string UIPanelTypeString; //按钮 private Button btn; //是否是面板 public bool isPanel = false; private void Awake() { btn = GetComponent<Button>(); } private void Start() { //判断是否是面板,根据结果进行注册 if (isPanel) btn.onClick.AddListener(UIManager.Instance.PopPanel); else btn.onClick.AddListener(OnButtonClick); } private void OnButtonClick() { UIPanelType type = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), UIPanelTypeString); UIManager.Instance.PushPanel(type); } }
在具体的面板上重写面板生命周期的方法,通过调整CanvasGroup组件的alpha值实现组件的显示和隐藏,通过设置CanvasGroup组件的blocksRaycasts 的值设置组件的可点击状态,如主面板的MainManuPanel组件
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MainManuPanel : BasePanel { private CanvasGroup canvasGroup; void Start() { if(canvasGroup == null) canvasGroup = GetComponent<CanvasGroup>(); } public override void OnPause() { canvasGroup.blocksRaycasts = false; } public override void OnEnter() { if (canvasGroup == null) canvasGroup = GetComponent<CanvasGroup>(); canvasGroup.alpha = 1; canvasGroup.blocksRaycasts = true; } public override void OnExit() { canvasGroup.alpha = 0; canvasGroup.blocksRaycasts = false; } public override void OnResume() { canvasGroup.blocksRaycasts = true; } }
七.优化细节
1.重写按钮注册的方法,提供按钮注册基类实现按钮注册,具体的被注册方法为虚拟方法等待重写,不同的按钮只需要实现注册基类重写方法即可。
按钮注册基类:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ButtonRegistRoot : MonoBehaviour { //按钮 private Button btn; private void Awake() { btn = GetComponent<Button>(); } private void Start() { btn.onClick.AddListener(OnButtonClick); } public virtual void OnButtonClick() { } }
关闭按钮:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CloseButtonRegist : ButtonRegistRoot { public override void OnButtonClick() { UIManager.Instance.PopPanel(); } }
菜单按钮:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ManuButtonRegist : ButtonRegistRoot { public string UIPanelTypeString; public override void OnButtonClick() { UIPanelType type = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), UIPanelTypeString); UIManager.Instance.PushPanel(type); } }
背包中物品显示信息按钮:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ItemButtonRegist : ButtonRegistRoot { public override void OnButtonClick() { UIManager.Instance.PushPanel(UIPanelType.ItemMessagePanel); } }
2.UI动画
UI动画现在就比较简单了,使用DOTWEEN设置动画,接下来哪个面板需要动画只需要在该面板的生命周期的具体方法中设置具体的动画即可。
八.总结
整个框架中的类是比较多的,具体可以做以下分类:
UIManager:单例模式,负责整个UI框架的管理。实现了从json中读取UI的预制件的地址和根据地址读取UI的预制件,因此当预制件被移动时我们只需要到json文件中作相应的修改即可,不用动代码。实现了使用栈存储显示的面板,提供相应的入栈和出栈方法对应面板的显示和隐藏。
UIPanelType:对应所有面板预制件的枚举类。
UIPanelInfo:用于读取UI面板预制件地址的包装类。
UIRoot:用于启动UI的类,调用UIManager的方法加载主菜单面板。
BasePanel:面板基类,定义面板的生命周期方法,所有面板都挂载继承这个类的脚本,并实现具体面板的生命周期方法。
ButtonRegistRoot:按钮注册方法的基类,实现了按钮注册,定义了供按钮注册的方法,每个按钮都挂载具体的按钮注册脚本,只需要实现被注册的方法即可。