unity中UI管理器的详解及其优化
目录概述对比下有无ui管理器的效果对比UI管理器实现的基本思路基类实现面板基类的实现一定要实现的变量还有就是每个面板都要实现的函数最重要的一个函数UI管理器的实现完整代码对于UI管理器的优化优化代码思路如下概述这套UI管理器是一个基于Unity引擎的轻量级面板管理系统核心功能包括UI面板的动态加载、生命周期管理、淡入淡出动画以及自动资源回收。整体设计采用单例模式和泛型方法。基本作用就是面板基类里面有show与hide方法。通过ui管理器来对ui面板统一的管理最后面会给完整代码对比下有无ui管理器的效果对比// ❌ 无管理器 GameObject prefab Resources.LoadGameObject(UI/LoginPanel); GameObject obj Instantiate(prefab); obj.transform.SetParent(GameObject.Find(Canvas).transform, false);每次对ui面板的创建都要这3行的代码可能还有些但是这3行是必要的。// ✅ 有管理器 UImanager.Instance.hidemeLoginPanel();对于无管理器来说代码重复性高对于find方法是非常消耗性能的下面会提到解决方法UI管理器实现的基本思路调用链如下ui管理器来创建面板面板是继承基类的可以调用基类的显示与隐藏函数基类实现面板基类的实现基类是所有面板的父类 及继承概念。所以对于基类的实现要思考每个面板都又什么一样的必须实现的方法以及变量一定要实现的变量这下面三个参数都是实现淡入淡出效果的。一个是组件一个是控制淡入淡出速度还有标志不详细展开这里只是为了探讨实现ui管理类的思路与方法private CanvasGroup canvasGroup; [SerializeField] private float alphaSpeed 5; public bool isshow;CanvasGroup 自动管理每个面板实例化出来后都加载这个组件控制淡入淡出protected void Awake() { canvasGroup this.GetComponentCanvasGroup(); if(canvasGroupnull) canvasGroup this.gameObject.AddComponentCanvasGroup(); }还有就是每个面板都要实现的函数那就是面板显示与隐藏函数public virtual void showme() { canvasGroup.alpha 0; isshowtrue; } private UnityAction hidecallback null; public virtual void hideme(UnityAction callback) { canvasGroup.alpha 1; isshowfalse; hidecallback callback; //这里的委托函数很关键写了委托函数才可以先在updtae里面淡出的效果结束以后在调用callback函数。这里回调函数一般是销毁obj如果不写回调 //就不会等待淡出而是俩边异步一起执行了 }这里可能就有人要问你 canvasGroup.alpha 还有hideme函数的委托函数做参数是干嘛的我这里只实现简单的淡入淡出效果不会立马关闭UI面板具体实现不细写这里函数你也可以实现你的自己的效果下面也是为了实现淡入淡出实现的。如果你只是写个简单的ui管理器仅仅就是创建以及显示没必要写void Update() { if (isshowcanvasGroup.alpha!1) { canvasGroup.alpha alphaSpeed * Time.deltaTime; if(canvasGroup.alpha1) canvasGroup.alpha 1; } else if(!isshow canvasGroup.alpha!0) { canvasGroup.alpha- alphaSpeed * Time.deltaTime; if (canvasGroup.alpha 0) { canvasGroup.alpha 0; if (hidecallback ! null) { hidecallback.Invoke(); } } } }最重要的一个函数抽象的初始化函数并且要在生命周期start中调用因为每个面板都要初始化但是实现不一样比如每个面板都有按钮每次初始化的时候都要对按钮注册监听事件。每个面板的按钮都不一样。这就是多态所以要写成抽象函数init()start成写虚函数是为了方便拓展好好解耦public abstract void Init(); protected virtual void Start() { Init(); }UI管理器的实现首先是单例模式不了解可以自己去了解下单例模式private static UImanager instance new UImanager(); public static UImanager Instance instance;不用ui管理器的时候要经常调用find去找canvas。下面用ui管理器解决方法第一次加载管理器的时候在构造函数里面实现画布的实例化以及记录画布这样就不需要find了private Transform canvastran; private UImanager() { GameObject cnavas GameObject.Instantiate(Resources.LoadGameObject(UI/Canvas)); canvastran cnavas.transform; GameObject.DontDestroyOnLoad(cnavas); }下面就是ui管理器的show函数基本实例是实例化面板调用面板的父类函数这里的函数一定是要泛型。根据面板类名来实例化面板。所以我们要做到面板名与脚本名一致因为我们是根据传入的面板类型去找面板预制体位置的然后就是字典类的作用防止重复实例化UI,不小心连续调用俩次会实例化出俩个ui界面出来。private Dictionarystring, Basepanel panelDic new Dictionarystring, Basepanel(); public T ShowpanelT() where T : Basepanel { string panelname typeof(T).Name; if (panelDic.ContainsKey(panelname)) { return panelDic[panelname] as T; } else { GameObject obj GameObject.Instantiate(Resources.LoadGameObject(UI/panelname)); obj.transform.SetParent(canvastran,false);//挂载到canvas下面 T panel obj.GetComponentT(); panelDic.Add(panelname, panel); panel.showme(); return panel; } }下面就是隐藏面板的实现隐藏后把面板从字典移除//隐藏面板 public void hidemeT( ) where T : Basepanel { string panelname typeof(T).Name; if (panelDic.ContainsKey(panelname)) { //回调很关键会使这边等待那边基类淡出实现完成后再销毁obj panelDic[panelname].hideme(() { GameObject.Destroy(panelDic[panelname].gameObject); panelDic.Remove(panelname); }); } }完整代码using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; public abstract class Basepanel : MonoBehaviour { private CanvasGroup canvasGroup; [SerializeField] private float alphaSpeed 5; public bool isshow; // Start is called before the first frame update public abstract void Init(); protected void Awake() { canvasGroup this.GetComponentCanvasGroup(); if(canvasGroupnull) canvasGroup this.gameObject.AddComponentCanvasGroup(); } protected virtual void Start() { Init(); } public virtual void showme() { canvasGroup.alpha 0; isshowtrue; } private UnityAction hidecallback null; public virtual void hideme(UnityAction callback) { canvasGroup.alpha 1; isshowfalse; hidecallback callback; //这里的委托函数很关键写了委托函数才可以先在updtae里面淡出的效果结束以后在调用callback函数。这里回调函数一般是销毁obj如果不写回调 //就不会等待淡出而是俩边异步一起执行了 } // Update is called once per frame protected virtual void Update() { if (isshowcanvasGroup.alpha!1) { canvasGroup.alpha alphaSpeed * Time.deltaTime; if(canvasGroup.alpha1) canvasGroup.alpha 1; } else if(!isshow canvasGroup.alpha!0) { canvasGroup.alpha- alphaSpeed * Time.deltaTime; if (canvasGroup.alpha 0) { canvasGroup.alpha 0; if (hidecallback ! null) { hidecallback.Invoke(); } } } } }using System.Collections; using System.Collections.Generic; using Unity.VisualScripting.FullSerializer; using UnityEngine; public class UImanager { private static UImanager instance new UImanager(); public static UImanager Instance instance; private Dictionarystring, Basepanel panelDic new Dictionarystring, Basepanel(); private Transform canvastran; //showpanel private UImanager() { GameObject cnavas GameObject.Instantiate(Resources.LoadGameObject(UI/Canvas)); canvastran cnavas.transform; GameObject.DontDestroyOnLoad(cnavas); } public T ShowpanelT() where T : Basepanel { string panelname typeof(T).Name; if (panelDic.ContainsKey(panelname)) { return panelDic[panelname] as T; } else { GameObject obj GameObject.Instantiate(Resources.LoadGameObject(UI/panelname)); obj.transform.SetParent(canvastran,false); T panel obj.GetComponentT(); panelDic.Add(panelname, panel); panel.showme(); return panel; } } //隐藏面板 public void hidemeT( ) where T : Basepanel { string panelname typeof(T).Name; if (panelDic.ContainsKey(panelname)) { //回调很关键会使这边等待那边基类淡出实现完成后再销毁obj panelDic[panelname].hideme(() { GameObject.Destroy(panelDic[panelname].gameObject); panelDic.Remove(panelname); }); } } //得到面板 public T GetpanelT( ) where T : Basepanel { string panelname typeof(T).Name; if(panelDic.ContainsKey(panelname)) return panelDic[panelname] as T ; return null; } }对于UI管理器的优化我们目前的ui管理器在面板隐藏后会销毁面板然后从字典中移除。但是对于频繁调出的ui比如王者荣耀里面的商城界面玩家要频繁的调出商城界面买装备我们可以不销毁ui而是控制面板的失活激活优化代码思路如下创建一个新函数只控制面板的失活激活不移除字典。在show函数中如果发现字典中已经存在了面板直接激活public void closeT() where T : Basepanel { string panelname typeof(T).Name; if (panelDic.ContainsKey(panelname)) { panelDic[panelname].gameObject.SetActive(false); } }在show函数里面也要加一行代码public T ShowpanelT() where T : Basepanel { string panelname typeof(T).Name; if (panelDic.ContainsKey(panelname)) { panelDic[panelname].gameObject.SetActive(true);//优化1代码 return panelDic[panelname] as T; } else { GameObject obj GameObject.Instantiate(Resources.LoadGameObject(UI/panelname)); obj.transform.SetParent(canvastran,false); T panel obj.GetComponentT(); panelDic.Add(panelname, panel); panel.showme(); return panel; } }还有就是每个函数里都要用到string panelname typeof(T).Name;把这个成员变量变成全局变量5.2月更新内容---------------------------------------------------------------------------------------------------------------------------------上面的优化还有点遐思对于只控制面板的失活激活而不销毁的面板的思路是对的。但是有个更快的方法。我是用canvas group的阿尔法来控制面板的淡入淡出。那我可以直接可以使阿尔法0来达到上述效果方法对比控制失活激活来使面板消失会触发整个的生命周期enable没有控制阿尔法快。优化代码部分控制阿尔法后要改动一部分内容。在basepanel中这里隐藏面板后要做的事情有1.取消交互不然会导致隐藏后可以交互2.还有关闭射线阻挡不然可能会导致后面的面板无法交互3.设置enablefalse。这样就不会导致面板隐藏后有update的空转对比setactveenable只会禁用当前脚本setactive会把面板所有的组件与子物体失活例如按钮等下面是ai给的对比性能量化示例假设一个中等复杂度的 UI 面板包含 20 个子物体操作this.enabled falseSetActive(false)隐藏耗时≈ 0.2 μs仅脚本禁用≈ 50~150 μs递归禁用所有组件再次显示耗时≈ 0.2 μs仅脚本启用≈ 200~500 μs递归激活 布局重建隐藏后每帧空转开销假设面板不可见但未销毁子物体若有 Update 脚本仍会执行但通常没有0所有脚本停止public virtual void showme() { this.enabled true; // ✅ 新增恢复 Update canvasGroup.alpha 0; canvasGroup.interactable true; // ✅ 新增恢复交互 canvasGroup.blocksRaycasts true; // ✅ 新增恢复射线阻挡 isshow true; }public virtual void hideme(UnityAction callback) { canvasGroup.interactable false; // ✅ 新增关闭交互 canvasGroup.blocksRaycasts false; // ✅ 新增穿透射线 isshow false; hideCallback callback; // 不要设置 this.enabled false留给 Update 完成淡出后做 }protected virtual void Update() { if (isshow canvasGroup.alpha ! 1f) { canvasGroup.alpha alphaSpeed * Time.deltaTime; if (canvasGroup.alpha 1f) canvasGroup.alpha 1f; } else if (!isshow canvasGroup.alpha ! 0f) { canvasGroup.alpha - alphaSpeed * Time.deltaTime; if (canvasGroup.alpha 0f) { canvasGroup.alpha 0f; this.enabled false; // ✅ 新增淡出完成停止 Update 空转 if (hideCallback ! null) { hideCallback.Invoke(); hideCallback null; } } } }切记这个close函数是频繁调用UI的情况才使用王者荣耀游戏内商店界面。这样能做到对面板打开和关闭的性能。最后总结一下。对频繁调用UI用阿尔法来控制隐藏这个优化还是有必要的但是对于担心隐藏后面板update的空转而调用this.enable.实在没有必要因为对于隐藏后的面板的转其实显示后阿尔法1的时候也有空转。但是代价太小了它比一次Debug.Log轻几万倍AI说的。基本没有影响。除非你有好几个面板在激活状态。或者你有代码洁癖根本就忍受不了这个无意义的调用。