【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

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

相关推荐
Alger_Hamlet9 小时前
Photoshop 2025 Mac中文 Ps图像编辑软件
macos·ui·photoshop
牙膏上的小苏打233310 小时前
Unity Surround开关后导致获取主显示器分辨率错误
unity·主屏幕
Unity大海12 小时前
诠视科技Unity SDK开发环境配置、项目设置、apk打包。
科技·unity·游戏引擎
浅陌sss17 小时前
Unity中 粒子系统使用整理(一)
unity·游戏引擎
维度攻城狮1 天前
实现在Unity3D中仿真汽车,而且还能使用ros2控制
python·unity·docker·汽车·ros2·rviz2
为你写首诗ge1 天前
【Unity网络编程知识】FTP学习
网络·unity
神码编程1 天前
【Unity】 HTFramework框架(六十四)SaveDataRuntime运行时保存组件参数、预制体
unity·编辑器·游戏引擎
菲fay1 天前
Unity 单例模式写法
unity·单例模式
火一线1 天前
【Framework-Client系列】UIGenerate介绍
游戏·unity
ZKY_241 天前
【工具】Json在线解析工具
unity·json