【unity进阶知识10】从零手搓一个UI管理器/UI框架,自带一个提示界面,还有自带DOTween动画效果

最终效果

文章目录

前言

unity在4.6版本之后,引入了自己的界面显示系统,全称unity graphic user interface,即我们所熟知的ugui。
毕竟是unity的亲儿子,这个系统一经推出,就与其灵活快速可视化,迅速抢占用户市场,逐渐成为unity ui的主流系统,但是它也并不是完美的,对于开发人员来说,使用这套系统往往需要面对如下困境,比如缺乏跨场景的u管理器,界面的上下层关系紊乱三,界面之间的通信手段贫乏等等,上述几个问题大大影响到我们的开发效率。

针对上述问题,我们可以选择制作一套UI管理器解决。

UI管理器

1、新增UI面板层枚举

csharp 复制代码
/// <summary>
/// UI面板层枚举
/// </summary>
public enum E_UIPanelLayer
{
    None,
    Rearmost,//最后方
    Rear,//后方
    Middle,//中间
    Front,//前方
    Forefront//最前方
}

2、初始化

2.1、用代码创建画布

csharp 复制代码
/// <summary>
/// 创建画布
/// </summary>
void CreateCanvas()
{
    //改Layer
    gameObject.layer = LayerMask.NameToLayer("UI");

    //添加并设置Canvas组件
    Canvas canvas = gameObject.AddComponent<Canvas>();
    canvas.renderMode = RenderMode.ScreenSpaceOverlay;
    canvas.sortingOrder = 30000;

    //添加并设置CanvasScaler组件
    CanvasScaler canvasScaler = gameObject.AddComponent<CanvasScaler>();
    canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
    canvasScaler.referenceResolution = new Vector2(Screen.width, Screen.height);
    //横版屏幕设置为1 竖版屏幕设置为0
    canvasScaler.matchWidthOrHeight = Screen.width > Screen.height ? 1 : 0;

    //添加Graphic Raycaster组件
    gameObject.AddComponent<GraphicRaycaster>();
}

效果

2.2、用代码创建UI面板的父物体层

csharp 复制代码
// 用于记录每个层的父物体
private Dictionary<E_UIPanelLayer, Transform> layerParents;

/// <summary>
/// 创建UI面板的父物体层
/// </summary>
void CreateLayer()
{
    //Rearmost层的父物体
    Transform rearmost = new GameObject(E_UIPanelLayer.Rearmost.ToString()).transform;
    rearmost.SetParent(transform, false);

    //Rear层的父物体
    Transform rear = new GameObject(E_UIPanelLayer.Rear.ToString()).transform;
    rear.SetParent(transform, false);

    //Middle层的父物体
    Transform middle = new GameObject(E_UIPanelLayer.Middle.ToString()).transform;
    middle.SetParent(transform, false);

    //Fornt层的父物体
    Transform front = new GameObject(E_UIPanelLayer.Front.ToString()).transform;
    front.SetParent(transform, false);

    //Frontmost层的父物体
    Transform foreFront = new GameObject(E_UIPanelLayer.ForeFront.ToString()).transform;
    foreFront.SetParent(transform, false);

    //记录每个层的父物体
    layerParents = new Dictionary<E_UIPanelLayer, Transform>
    {
        { E_UIPanelLayer.Rearmost, rearmost },
        { E_UIPanelLayer.Rear, rear },
        { E_UIPanelLayer.Middle, middle },
        { E_UIPanelLayer.Front, front },
        { E_UIPanelLayer.ForeFront, foreFront }
    };
}

效果

2.3、代码添加EventSystem物体

csharp 复制代码
/// <summary>
/// 创建EventSystem
/// </summary>
void CreateEventSystem()
{
    //如果场景中已经有一个EventSystem了,则直接返回。
    if (FindObjectOfType<EventSystem>()) return;

    GameObject eventSystem = new GameObject("EventSystem");
    DontDestroyOnLoad(eventSystem);//切换场景不销毁
    eventSystem.AddComponent<EventSystem>();
    eventSystem.AddComponent<StandaloneInputModule>();
}

效果

3、ShowPanel显示面板方法

csharp 复制代码
//存储加载过的界面的集合
private List<UIBase> uiList = new List<UIBase>();

/// <summary>
/// 显示面板
/// </summary>
/// <typeparam name="T">UI面板脚本,记得UI面板预制体名要和脚本名一样</typeparam>
/// <param name="layer">父级层</param>
/// <returns>UIBase</returns>
public UIBase ShowUI<T>(E_UIPanelLayer layer = E_UIPanelLayer.Middle) where T : UIBase
{
	string uiName = typeof(T).Name;//获取名称
    UIBase ui = Find(uiName);
    if (ui == null)
    {
        //记录该面板要放进哪个层中来显示
        Transform parent = layerParents[layer];

        //集合中没有 需要从Resources/UI文件夹加载
        GameObject obj = Instantiate(Resources.Load("UI/" + uiName), parent) as GameObject;

        //改名字,默认实例化会加上(clone),所以得重命名
        obj.name = uiName;

        //添加需要的脚本
        ui = obj.AddComponent<T>();

        //添加到集合进行存储
        uiList.Add(ui);
    }
    else
    {
        //显示
        ui.Show();
    }
    return ui;
}

调用

4、HidePanel隐藏面板的方法

csharp 复制代码
/// <summary>
/// 隐藏面板
/// </summary>
/// <param name="uiName">面板名</param>
public void HideUI(string uiName)
{
    UIBase ui = Find(uiName);
    if (ui != null)
    {
        ui.Hide();
    }
}

调用

5、CloseUI关闭界面的方法

csharp 复制代码
/// <summary>
/// 关闭某个界面
/// </summary>
/// <param name="uiName">面板名</param>
public void CloseUI(string uiName)
{
    UIBase ui = Find(uiName);
    if (ui != null)
    {
        uiList.Remove(ui);
        Destroy(ui.gameObject);
    }
}

6、UI界面基类

csharp 复制代码
/// <summary>
/// UI界面基类
/// </summary>
public class UIBase : MonoBehaviour
{
    //显示
    public virtual void Show()
    {
        gameObject.SetActive(true);
    }

    //隐藏
    public virtual void Hide()
    {

        gameObject.SetActive(false);
    }

    //关闭界面(销毁)
    public virtual void Close()
    {
        UIManager.Instance.CloseUI(gameObject.name);
    }
}

测试调用

欢迎面板

新增WelcomeUI.cs测试面板代码,注意记得继承UIBase基类

csharp 复制代码
public class WelcomeUI : UIBase {
    void Awake(){
        //绑定按钮事件
        transform.Find("bg/退出按钮").GetComponent<Button>().onClick.AddListener(onCloseBtn);
        transform.Find("bg/确定").GetComponent<Button>().onClick.AddListener(onConfirmBtn);
    }
    void onCloseBtn(){
        //关闭界面
        Close();
    }

    void onConfirmBtn(){
        //隐藏
        Hide();
    }
}

新增UITest ,绘制按钮显示WelcomeUI欢迎面板

csharp 复制代码
public class UITest : MonoBehaviour {
    private void OnGUI()
    {
        // 创建一个新的 GUIStyle
        GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
        // 设置字体大小
        buttonStyle.fontSize = 50; // 替换为你想要的字体大小
        buttonStyle.alignment = TextAnchor.MiddleCenter; // 可选择设置对齐方式

        if (GUI.Button(new Rect(0, 0, 500, 200), "显示欢迎面板", buttonStyle))
        {
            //显示WelcomeUI面板,创建的脚本名字记得跟预制体物体名字一致
            UIManager.Instance.ShowUI<WelcomeUI>("WelcomeUI");
        }
    }
}

效果

优化绑定按钮事件

每次绑定按钮事件都需要写这么多代码很麻烦,我们可以继续进行封装

新增UIEventTrigger事件监听代码

csharp 复制代码
/// <summary>
/// 事件监听
/// </summary>
public class UIEventTrigger : MonoBehaviour, IPointerClickHandler
{
    //这是一个公共的委托,它接受两个参数,一个是被点击的游戏对象,另一个是关于点击事件的数据。
    public Action<GameObject, PointerEventData> onClick;

    //用于获取或添加 UIEventTrigger 组件
    public static UIEventTrigger Get(GameObject obj)
    {
        UIEventTrigger trigger = obj.GetComponent<UIEventTrigger>();
        if (trigger == null)
        {
            trigger = obj.AddComponent<UIEventTrigger>();
        }
        return trigger;
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        //这是 IPointerClickHandler 接口的方法,当 UI 元素被点击时,它将被调用。
        if (onClick != null) onClick(gameObject, eventData);
    }
}

修改UI界面基类UIBase

csharp 复制代码
//注册事件
public UIEventTrigger Register(string name)
{
    Transform tf = transform.Find(name);
    return UIEventTrigger.Get(tf.gameObject);
}

测试调用

csharp 复制代码
public class WelcomeUI : UIBase {
    void Awake(){
        //绑定按钮事件
        // transform.Find("bg/退出按钮").GetComponent<Button>().onClick.AddListener(onCloseBtn);
        // transform.Find("bg/确定").GetComponent<Button>().onClick.AddListener(onConfirmBtn);

        Register("bg/退出按钮").onClick = onCloseBtn;
        Register("bg/确定").onClick = onConfirmBtn;
    }

    void onCloseBtn(GameObject obj, PointerEventData pData){
        //关闭界面
        Close();
    }

    void onConfirmBtn(GameObject obj, PointerEventData pData){
        //隐藏
        Hide();
    }
}

效果,和前面一样

新增提示框

UI绘制

背景图片

配置

实现

修改UIManager

csharp 复制代码
/// <summary>
/// 提示界面
/// </summary>
/// <param name="msg">文本</param>
/// <param name="color">颜色</param>
/// <param name="callback">完成回调事件</param>
public void ShowTips(string msg, Color color, float showTime = 0.5f, UnityAction callback = null, E_UIPanelLayer layer = E_UIPanelLayer.ForeFront)
{
    UIBase ui = ShowUI<TipsUI>(layer);
    TextMeshProUGUI text = ui.transform.Find("bg/text").GetComponent<TextMeshProUGUI>();
    text.color = color;
    text.text = msg;
}

测试调用

csharp 复制代码
UIManager.Instance.ShowTips("成功!", Color.green);

效果

使用DOTween实现动画效果

参考:【推荐100个unity插件之2】DoTween动画插件的安装和使用整合(最全)

提示框动画

实现面板先经过0.4sY轴缩放从0变为1,再暂停showTime秒后,经过0.4sY轴缩放从1变回0,动画播放完成调用callback事件

csharp 复制代码
/// <summary>
/// 提示界面
/// </summary>
/// <param name="msg">文本</param>
/// <param name="color">颜色</param>
/// <param name="showTime">显示时间</param>
/// <param name="callback">完成回调事件</param>
public void ShowTips(string msg, Color color, float showTime = 0.5f, UnityAction callback = null, E_UIPanelLayer layer = E_UIPanelLayer.ForeFront)
{
	DOTween.CompleteAll(true);

    UIBase ui = ShowUI<TipsUI>("TipsUI", layer);
    TextMeshProUGUI text = ui.transform.Find("bg/text").GetComponent<TextMeshProUGUI>();
    text.color = color;
    text.text = msg;
    
    // DOTween动画 方法一
    // Sequence sequence = DOTween.Sequence();
    // sequence.Append(ui.transform.DOScaleY(1, 0.4f).From(0)) // 第一个动画,缩放到 1
    // .Append(DOVirtual.DelayedCall(showTime, () => { })) // 延迟
    // .Append(ui.transform.DOScaleY(0, 0.4f).From(1)) // 第二个动画,缩放到 0
    // .OnComplete(() =>
    // {
    //     ui.gameObject.SetActive(false); // 隐藏 UI
    //     callback?.Invoke(); // 调用回调
    // });

    // DOTween动画 方法二
    ui.transform
    .DOScaleY(1, 0.4f)
    .From(0)
    .OnComplete(() =>
    {
        // 延迟显示时间
        DOVirtual.DelayedCall(showTime, () =>
        {
            ui.transform.DOScaleY(0, 0.4f).From(1)
                .OnComplete(() =>
                {
                    ui.gameObject.SetActive(false);
                    callback?.Invoke();
                });
        });
    });
}

效果

打开UI面板动画

修改UIManager里的ShowUI方法,新增DOTween代码即可

csharp 复制代码
ui.transform.DOScale(Vector3.one, 0.5f).From(Vector3.zero);

效果

关闭UI面板动画

csharp 复制代码
//动画
CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();
Sequence closeSequence = DOTween.Sequence();
closeSequence.Append(canvasGroup.DOFade(0, 0.8f)) // 淡出
.Join(ui.transform.DOLocalMoveX(2000f, 0.8f)) // 同时移动
.OnComplete(() =>
{
    ui.gameObject.SetActive(false);
});

效果

完整代码

UIEventTrigger.cs

csharp 复制代码
/// <summary>
/// 事件监听
/// </summary>
public class UIEventTrigger : MonoBehaviour, IPointerClickHandler
{
    //这是一个公共的委托,它接受两个参数,一个是被点击的游戏对象,另一个是关于点击事件的数据。
    public Action<GameObject, PointerEventData> onClick;

    //用于获取或添加 UIEventTrigger 组件
    public static UIEventTrigger Get(GameObject obj)
    {
        UIEventTrigger trigger = obj.GetComponent<UIEventTrigger>();
        if (trigger == null)
        {
            trigger = obj.AddComponent<UIEventTrigger>();
        }
        return trigger;
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        //这是 IPointerClickHandler 接口的方法,当 UI 元素被点击时,它将被调用。
        if (onClick != null) onClick(gameObject, eventData);
    }
}

UIBase.cs

csharp 复制代码
/// <summary>
/// UI界面基类
/// </summary>
public class UIBase : MonoBehaviour
{
    //显示
    public virtual void Show()
    {
        transform.localPosition = Vector3.zero;
        gameObject.SetActive(true);
    }

    //隐藏
    public virtual void Hide()
    {
        UIManager.Instance.HideUI(gameObject.name);
    }

    //关闭界面(销毁)
    public virtual void Close()
    {
        UIManager.Instance.CloseUI(gameObject.name);
    }

    //注册事件
    public UIEventTrigger Register(string name)
    {
        Transform tf = transform.Find(name);
        return UIEventTrigger.Get(tf.gameObject);
    }
}

UIManager.cs

csharp 复制代码
/// <summary>
/// UI管理器
/// </summary>
public class UIManager : SingletonMono<UIManager>
{
    // 用于记录每个层的父物体
    private Dictionary<E_UIPanelLayer, Transform> layerParents;

    //存储加载过的界面的集合
    private List<UIBase> uiList = new List<UIBase>();

    #region 初始化
    void Awake()
    {
        //创建画布
        CreateCanvas();
        //创建UI面板的父物体层
        CreateLayer();
        //创建EventSystem
        CreateEventSystem();
    }

    /// <summary>
    /// 创建画布
    /// </summary>
    void CreateCanvas()
    {
        //改Layer
        gameObject.layer = LayerMask.NameToLayer("UI");

        //添加并设置Canvas组件
        Canvas canvas = gameObject.AddComponent<Canvas>();
        canvas.renderMode = RenderMode.ScreenSpaceOverlay;
        canvas.sortingOrder = 30000;

        //添加并设置CanvasScaler组件
        CanvasScaler canvasScaler = gameObject.AddComponent<CanvasScaler>();
        canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
        canvasScaler.referenceResolution = new Vector2(Screen.width, Screen.height);
        //横版屏幕设置为1 竖版屏幕设置为0
        canvasScaler.matchWidthOrHeight = Screen.width > Screen.height ? 1 : 0;

        //添加Graphic Raycaster组件
        gameObject.AddComponent<GraphicRaycaster>();
    }

    /// <summary>
    /// 创建UI面板的父物体层
    /// </summary>
    void CreateLayer()
    {
        //Rearmost层的父物体
        Transform rearmost = new GameObject(E_UIPanelLayer.Rearmost.ToString()).transform;
        rearmost.SetParent(transform, false);

        //Rear层的父物体
        Transform rear = new GameObject(E_UIPanelLayer.Rear.ToString()).transform;
        rear.SetParent(transform, false);

        //Middle层的父物体
        Transform middle = new GameObject(E_UIPanelLayer.Middle.ToString()).transform;
        middle.SetParent(transform, false);

        //Fornt层的父物体
        Transform front = new GameObject(E_UIPanelLayer.Front.ToString()).transform;
        front.SetParent(transform, false);

        //Frontmost层的父物体
        Transform foreFront = new GameObject(E_UIPanelLayer.ForeFront.ToString()).transform;
        foreFront.SetParent(transform, false);

        //记录每个层的父物体
        layerParents = new Dictionary<E_UIPanelLayer, Transform>
        {
            { E_UIPanelLayer.Rearmost, rearmost },
            { E_UIPanelLayer.Rear, rear },
            { E_UIPanelLayer.Middle, middle },
            { E_UIPanelLayer.Front, front },
            { E_UIPanelLayer.ForeFront, foreFront }
        };
    }

    /// <summary>
    /// 创建EventSystem
    /// </summary>
    void CreateEventSystem()
    {
        //如果场景中已经有一个EventSystem了,则直接返回。
        if (FindObjectOfType<EventSystem>()) return;

        GameObject eventSystem = new GameObject("EventSystem");
        DontDestroyOnLoad(eventSystem);//切换场景不销毁
        eventSystem.AddComponent<EventSystem>();
        eventSystem.AddComponent<StandaloneInputModule>();
    }
    #endregion

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">UI面板脚本,记得UI面板预制体名要和脚本名一样</typeparam>
    /// <param name="layer">父级层</param>
    /// <param name="doTween">是否使用doTween动画</param>
    /// <returns>UIBase</returns>
    public UIBase ShowUI<T>(E_UIPanelLayer layer = E_UIPanelLayer.Middle, bool doTween = true) where T : UIBase
    {
        DOTween.CompleteAll(true);
        string uiName = typeof(T).Name;//获取名称
        UIBase ui = Find(uiName);
        if (ui == null)
        {
            //记录该面板要放进哪个层中来显示
            Transform parent = layerParents[layer];

            //集合中没有 需要从Resources/UI文件夹加载
            GameObject obj = Instantiate(Resources.Load("UI/" + uiName), parent) as GameObject;

            //改名字,默认实例化会加上(clone),所以得重命名
            obj.name = uiName;

            //添加需要的脚本
            ui = obj.AddComponent<T>();

            //添加CanvasGroup组件,用于后面渐变使用
            obj.AddComponent<CanvasGroup>();

            //添加到集合进行存储
            uiList.Add(ui);
        }
        else
        {
            //显示
            ui.Show();
        }

        //透明度设置为1
        CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();
        canvasGroup.alpha = 1f;

        //动画
        if (doTween) ui.transform.DOScale(Vector3.one, 0.5f).From(Vector3.zero);

        return ui;
    }

    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <param name="uiName">面板名</param>
    public void HideUI(string uiName, bool doTween = true)
    {
        DOTween.CompleteAll(true);

        UIBase ui = Find(uiName);
        if (ui == null) return;
        if (doTween)
        {
            //动画
            CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();
            Sequence closeSequence = DOTween.Sequence();
            closeSequence.Append(canvasGroup.DOFade(0, 0.8f)) // 淡出
            .Join(ui.transform.DOLocalMoveX(2000f, 0.8f)) // 同时移动
            .OnComplete(() =>
            {
                ui.gameObject.SetActive(false);
            });
        }
        else
        {
            ui.gameObject.SetActive(false);
        }
    }
    /// <summary>
    /// 关闭某个界面
    /// </summary>
    /// <param name="uiName">面板名</param>
    public void CloseUI(string uiName, bool doTween = true)
    {
        DOTween.CompleteAll(true);

        UIBase ui = Find(uiName);
        if (ui == null) return;
        if (doTween)
        {
            //动画
            CanvasGroup canvasGroup = ui.transform.GetComponent<CanvasGroup>();
            Sequence closeSequence = DOTween.Sequence();
            closeSequence.Append(canvasGroup.DOFade(0, 0.8f)) // 淡出
             .Join(ui.transform.DOLocalMoveX(2000f, 0.8f)) // 同时移动
             .OnComplete(() =>
            {
                uiList.Remove(ui);
                Destroy(ui.gameObject);
            });
        }
        else
        {
            uiList.Remove(ui);
            Destroy(ui.gameObject);
        }
    }

    //关闭所有界面
    public void CloseAllUI()
    {
        for (int i = uiList.Count - 1; i >= 0; i--)
        {
            Destroy(uiList[i].gameObject);
        }
        uiList.Clear();//清空合集
    }

    /// <summary>
    /// 从集合中找到名字对应的界面脚本
    /// </summary>
    /// <param name="uiName">面板名</param>
    /// <returns>UIBase</returns>
    public UIBase Find(string uiName)
    {
        for (int i = 0; i < uiList.Count; i++)
        {
            if (uiList[i].name == uiName) return uiList[i];
        }
        return null;
    }

    /// <summary>
    /// 提示界面
    /// </summary>
    /// <param name="msg">文本</param>
    /// <param name="color">颜色</param>
    /// <param name="showTime">显示时间</param>
    /// <param name="callback">完成回调事件</param>
    public void ShowTips(string msg, Color color, float showTime = 0.5f, UnityAction callback = null, E_UIPanelLayer layer = E_UIPanelLayer.ForeFront)
    {
        DOTween.CompleteAll(true);

        UIBase ui = ShowUI<TipsUI>(layer, false);
        TextMeshProUGUI text = ui.transform.Find("bg/text").GetComponent<TextMeshProUGUI>();
        text.color = color;
        text.text = msg;

        //动画
        Sequence sequence = DOTween.Sequence();
        sequence.Append(ui.transform.DOScaleY(1, 0.4f).From(0)) // 第一个动画,缩放到 1
        .Append(DOVirtual.DelayedCall(showTime, () => { })) // 延迟
        .Append(ui.transform.DOScaleY(0, 0.4f).From(1)) // 第二个动画,缩放到 0
        .OnComplete(() =>
        {
            ui.gameObject.SetActive(false); // 隐藏 UI
            callback?.Invoke(); // 调用回调
        });
    }
}

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

相关推荐
向宇it6 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
Heaphaestus,RC7 小时前
【Unity3D】获取 GameObject 的完整层级结构
unity·c#
芋芋qwq7 小时前
Unity UI射线检测 道具拖拽
ui·unity·游戏引擎
tealcwu8 小时前
【Unity服务】关于Unity LevelPlay的基本情况
unity·游戏引擎
鸿蒙自习室8 小时前
鸿蒙多线程开发——线程间数据通信对象02
ui·harmonyos·鸿蒙
大眼睛姑娘11 小时前
Unity3d场景童话梦幻卡通Q版城镇建筑植物山石3D模型游戏美术素材
unity·游戏美术
大霞上仙11 小时前
element ui table 每行不同状态
vue.js·ui·elementui
鹿野素材屋15 小时前
Unity Dots下的动画合批工具:GPU ECS Animation Baker
unity·游戏引擎
栈老师不回家19 小时前
Element UI 组件库详解【Vue】
前端·vue.js·ui
郭梧悠21 小时前
HarmonyOS(57) UI性能优化
ui·性能优化·harmonyos