Unity 通用UI界面逻辑总结

概述

在游戏开发中,常常会遇到一些通用的界面逻辑,它不论在什么类型的游戏中都会出现。为了避免重复造轮子,本文总结并提供了一些常用UI界面的实现逻辑。希望可以帮助大家快速开发通用界面模块,也可以在次基础上进行扩展修改,以适应你项目的需求。

工程链接GitCode - 全球开发者的开源社区,开源代码托管平台

二次确认界面

cs 复制代码
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class ConfirmDialog : MonoBehaviour
{
    private Text txt_title;
    private Text txt_content;
    private Button btn_Yes;
    private Button btn_No;

    void Start()
    {
        var root = gameObject.transform;
        txt_title = root.Find("txt_Title").GetComponent<Text>();
        txt_content = root.Find("txt_Content").GetComponent<Text>();
        btn_Yes = root.Find("btn/btn_Yes").GetComponent<Button>();
        btn_No = root.Find("btn/btn_No").GetComponent<Button>();
        txt_title.text = "提示";
        
        //测试代码
        InitDialog("好好学习,天天向上!", 
            () => {Debug.Log("Yes"); },
            () => {Debug.Log("No"); });
    }

    /// <summary>
    /// 重载一:使用默认标题 "提示"
    /// </summary>
    /// <param name="content">需要确认的内容</param>
    /// <param name="yesAction">确认按钮回调</param>
    /// <param name="noAction">取消按钮回调</param>
    public void InitDialog(string content, UnityAction yesAction = null, UnityAction noAction = null)
    {
        txt_title.text = "提示";
        CoreLogic(content, yesAction, noAction);
    }

    /// <summary>
    /// 重载一:使用自定义标题
    /// </summary>
    /// <param name="title">自定义标题</param>
    /// <param name="content">需要确认的内容</param>
    /// <param name="yesAction">确认按钮回调</param>
    /// <param name="noAction">取消按钮回调</param>
    public void InitDialog(string title, string content, UnityAction yesAction = null, UnityAction noAction = null)
    {
        txt_title.text = title;
        CoreLogic(content, yesAction, noAction);
    }

    //公共逻辑提取
    private void CoreLogic(string content, UnityAction yesAction = null, UnityAction noAction = null)
    {
        txt_content.text = content;
        BindBtnLogic(btn_Yes, yesAction);
        BindBtnLogic(btn_No, noAction);
        btn_Yes.gameObject.SetActive(yesAction != null);
        btn_No.gameObject.SetActive(noAction != null);
    }

    //绑定按钮点击回调
    private void BindBtnLogic(Button btn, UnityAction action)
    {
        btn.onClick.RemoveAllListeners();
        if (action != null)
        {
            btn.onClick.AddListener(action);
        }
    }
}

切页标签

通过按钮来实现。虽然使用Toggle也可以实现,但是在实际开发中会发现使用toggle不好控制选中事件的触发和选中状态表现。通过按钮来自定义组件可以更好地控制逻辑的调用和标签的显示。

cs 复制代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

class TabNode
{ 
    public int index;
    public GameObject offBg;
    public GameObject onBg;
    public Text offTxt;
    public Text onTxt;
    public Button btn;
    public UnityAction callback;
}

public class SwitchPageTab : MonoBehaviour
{
    public Transform tabRoot;//标签组的父节点
    public GameObject tabObj;//标签页预制体模板
    private int _selectIndex;//选中的标签页索引

    private List<TabNode> _objList = new List<TabNode>();
    private Dictionary<int, UnityAction> _callbackDic = new Dictionary<int, UnityAction>();
    
    private void Start()
    {
        _selectIndex = -1;
        
        InitCount(4);
        BindSelectCallback(0, "背包", (() =>
        {
            Debug.Log("查看背包");
        }));
        BindSelectCallback(1, "英雄", (() =>
        {
            Debug.Log("查看英雄");
        }));
        BindSelectCallback(2, "商店", (() =>
        {
            Debug.Log("查看商店");
        }));
        BindSelectCallback(3, "活动", (() =>
        {
            Debug.Log("查看活动");
        }));
        
        
        OnSelectLogic(0);
    }

    /// <summary>
    /// 初始化调用
    /// </summary>
    /// <param name="count">标签的数量</param>
    public void InitCount(int count)
    {
        _objList.Clear();
        ClearAllChild(tabRoot);
        for (var i = 0; i < count; i++)
        {
            var obj = Instantiate(tabObj, tabRoot);
            obj.SetActive(true);
            var trans = obj.transform;
            var node = new TabNode
            {
                offTxt = trans.Find("btn/offBg/offTxt").GetComponent<Text>(),
                onTxt = trans.Find("btn/onBg/onTxt").GetComponent<Text>(),
                onBg = trans.Find("btn/onBg").gameObject,
                offBg = trans.Find("btn/offBg").gameObject,
                btn = trans.Find("btn").GetComponent<Button>(),
            };
            var index = i;
            BindBtnLogic(node.btn, () =>
            {
                OnSelectLogic(index);
            });
            _objList.Add(node);
        }
    }
    
    /// <summary>
    /// 绑定指定页签索引的回调函数
    /// </summary>
    /// <param name="index">页签索引</param>
    /// <param name="txt">页签问本</param>
    /// <param name="callback">选中回调</param>
    public void BindSelectCallback(int index,string txt,UnityAction callback)
    {
        if (_callbackDic.ContainsKey(index))
        {
            Debug.LogError("已经注册过了!");
            return;
        }

        if (callback == null)
        {
            Debug.LogError("回调为空!");
            return;
        }

        if (index < 0 || index > _objList.Count)
        {
            Debug.LogError("索引越界!");
            return;
        }

        var node = _objList[index];
        node.onTxt.text = txt;
        node.offTxt.text = txt;
        _callbackDic.Add(index,callback);
    }
    
    /// <summary>
    /// 调用指定索引对应的回调函数
    /// </summary>
    /// <param name="index"></param>
    private void OnSelectLogic(int index)
    {
        if (index == _selectIndex)
        {
            return;
        }
        _selectIndex = index;
        
        var isExist = _callbackDic.TryGetValue(_selectIndex, out UnityAction callback);
        if (isExist)
        {
            callback?.Invoke();
            SetSelectStatus(index);
        }
    }
    
    /// <summary>
    /// 控制指定页签的UI表现
    /// </summary>
    /// <param name="index"></param>
    private void SetSelectStatus(int index)
    {
        var count = _objList.Count;
        for (var i = 0; i < count; i++)
        {
            var isActive = index == i;
            var node = _objList[i];
            node.onBg.SetActive(isActive);
            node.offBg.SetActive(!isActive);
        }
    }
    
    //清除指定父节点下的所有子物体
    private void ClearAllChild(Transform parentRoot)
    {
        var childCount = parentRoot.childCount;
        for (var i = childCount - 1; i >= 0; i--)
        {
            var child = parentRoot.GetChild(i);
            DestroyImmediate(child.gameObject);
        }
    }

    //绑定按钮点击回调
    private void BindBtnLogic(Button btn, UnityAction action)
    {
        btn.onClick.RemoveAllListeners();
        if (action != null)
        {
            btn.onClick.AddListener(action);
        }
    }
}

飘字提示

简易版本

cs 复制代码
using UnityEngine;
using UnityEngine.Pool;
using DG.Tweening;
using UnityEngine.UI;

public class SimpleTip : MonoBehaviour
{
    //提示栏预制体
    public GameObject tipObj;
    //提示栏显示的父节点
    public Transform tipRoot;
    //对象池
    private ObjectPool<GameObject> tipPool;
    //飞行高度
    private float flyHeight = 500;
    
    void Start()
    {
        InitTipPool();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            ShowTip("货币不足!");
        }
    }

    void ShowTip(string tipStr)
    {
        var obj = tipPool.Get();
        var rectValue = obj.GetComponent<RectTransform>();
        var group = obj.GetComponent<CanvasGroup>();
        var txt = obj.transform.Find("txt").GetComponent<Text>();
        txt.text = tipStr;
        obj.SetActive(true);
        group.alpha = 1;
        ResetLocal(obj.transform);
        rectValue.DOAnchorPosY(flyHeight, 1f).OnComplete(() =>
        {
            group.DOFade(0, 0.1f).OnComplete(() =>
            {
                tipPool.Release(obj);
            });
        });
    }
    
    //初始化对象池
    void InitTipPool()
    {
        tipPool = new ObjectPool<GameObject>(() =>
        {
            //创建新对象调用
            var obj = Instantiate(tipObj, tipRoot);
            obj.SetActive(false);
            return obj;
        },
        (go) =>
        {
            //获取对象调用
            go.SetActive(true);
            ResetLocal(go.transform);
        },
        (go) =>
        {
            // 在对象放回池子时调用
            go.SetActive(false);
            ResetLocal(go.transform);
            go.transform.SetParent(tipRoot);
        },
        (go) =>
        {
            Destroy(go); 
        });
    }
    
    //重置本地信息
    void ResetLocal(Transform trans)
    {
        trans.localPosition = Vector3.zero;
        trans.localEulerAngles = Vector3.zero;
        trans.localScale = Vector3.one;
    }
}

升级版本

cs 复制代码
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Pool;

public class GoodTip : MonoBehaviour
{
    //提示栏显示的父节点
    public Transform root;
    //提示栏模板预制体
    public GameObject tipObj;
    //对象池节点
    public Transform objectPool;
    
    //最多显示的提示栏数量,超过就隐藏
    private int limitCount = 5;
    //提示栏之间的偏移
    private float offset = 20;
    //提示飞行高度
    private float flyHeight = 100;
    //提示栏生成数量,只用于逻辑运算
    private int tipCount = 0;
    //提示栏高度
    private float tipHeight;
    private Queue<GameObject> visualTipQueue = new Queue<GameObject>();
    //是否可继续生成提示栏,防止频繁点击造成异常
    private bool isOk = true;
    private float timer = 0f;
    private bool startTimer = false;
    private float displayTime = 0.65f;//提示停留展示时间
    private ObjectPool<GameObject> tipPool;
    
    void Start()
    {
        var rect = tipObj.GetComponent<RectTransform>();
        tipHeight = rect.rect.height;
        
        InitTipPool();
    }

    private void Update()
    {
        if (startTimer)
        {
            //定时统一清理提示消息
            timer += Time.deltaTime;
            if (timer > displayTime)
            {
                ClearAllMsg();
                timer = 0f;
                startTimer = false;
            }
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            ShowTip("货币不足!");
        }
    }
    
    public void ShowTip(string tip)
    {
        if (!isOk)
        {
            return;
        }
        startTimer = false;
        isOk = false;
        var obj = tipPool.Get();
        var rect1 = obj.GetComponent<RectTransform>();
        var group = obj.GetComponent<CanvasGroup>();
        
        var sequence = DOTween.Sequence();
        if (visualTipQueue.Count > 0)
        {
            sequence.AppendCallback(() =>
            {
                foreach (var item in visualTipQueue)
                {
                    var rectValue = item.GetComponent<RectTransform>();
                    rectValue.DOAnchorPosY(rectValue.anchoredPosition.y+tipHeight+offset, 0.2f);
                }
            });
            sequence.AppendInterval(0.2f);
        }
        sequence.AppendCallback(() =>
        {
            group.alpha = 1;
            obj.transform.SetParent(root);
            obj.transform.localScale = new Vector3(0, 0, 1);
            obj.SetActive(true);
            rect1.anchoredPosition = Vector2.zero;
            visualTipQueue.Enqueue(obj);
            
            tipCount++;
            var txt  = obj.transform.Find("txt").GetComponent<Text>();
            txt.text = tip;
            
            if (tipCount > limitCount)
            {
                var result = visualTipQueue.Dequeue();
                tipPool.Release(result);
                tipCount--;
            }
        });
        sequence.Append(obj.transform.DOScale(Vector3.one, 0.1f));
        sequence.AppendInterval(0.1f);
        sequence.OnComplete(() =>
        {
            timer = 0f;
            isOk = true;
            startTimer = true;
        });
    }
    
    //初始化对象池
    void InitTipPool()
    {
        tipPool = new ObjectPool<GameObject>(() =>
        {
            //创建新对象调用
            var obj = Instantiate(tipObj, objectPool);
            obj.SetActive(false);
            return obj;
        },
        (go) =>
        {
            //获取对象调用
            go.SetActive(true);
            ResetLocal(go.transform);
        },
        (go) =>
        {
            // 在对象放回池子时调用
            go.SetActive(false);
            ResetLocal(go.transform);
            go.transform.SetParent(objectPool);
        },
        (go) =>
        {
            Destroy(go); 
        });
    }
    
    //重置本地信息
    void ResetLocal(Transform trans)
    {
        trans.localPosition = Vector3.zero;
        trans.localEulerAngles = Vector3.zero;
        trans.localScale = Vector3.one;
    }
    
    //清空消息
    public void ClearAllMsg()
    {
        var childCount = root.childCount;
        for (var i = 0; i < childCount; i++)
        {
            var child = root.GetChild(i);
            var group = child.GetComponent<CanvasGroup>();
            var rectValue = child.GetComponent<RectTransform>();
            var sequence = DOTween.Sequence();
            sequence.AppendInterval(0.1f * i);
            sequence.Append(rectValue.DOAnchorPosY(rectValue.anchoredPosition.y + tipHeight+flyHeight, 0.2f));
            sequence.Append(group.DOFade(0, 0.1f).OnComplete(() =>
            {
                visualTipQueue.Dequeue();
                tipPool.Release(child.gameObject);
                tipCount--;
            }));
        }
    }
}

左右切换按钮组

本组件一般出现在查看英雄界面,点击左右两个按钮切换查看按钮的详细信息。在英雄列表中,第一个英雄的左按钮不显示,最后一个英雄的右按钮不显示。

cs 复制代码
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

public class SwitchCheck : MonoBehaviour
{
    public Button btn_Left;
    public Button btn_Right;
    public Text txt_Check;

    private int sumCount;
    private int curIndex;
    private UnityAction<int> callback;//外部逻辑回调
    
    void Start()
    {
        curIndex = 0;
        InitGroup(10, (index) =>
        {
            txt_Check.text = $"{index}/{sumCount}";
        });
        CheckBtnActive();
        BindBtnLogic(btn_Left, () =>
        {
            var nextIndex = curIndex - 1;
            if (nextIndex < 0)
            {
                return;
            }
            curIndex = nextIndex;
            CheckBtnActive();
        });
        
        BindBtnLogic(btn_Right, () =>
        {
            var nextIndex = curIndex + 1;
            if (nextIndex >= sumCount)
            {
                return;
            }
            curIndex = nextIndex;
            CheckBtnActive();
        });
    }
    
    public void InitGroup(int _sumCount,UnityAction<int> _callback)
    {
        sumCount = _sumCount;
        callback = _callback;
    }
    
    //按钮显隐逻辑
    private void CheckBtnActive()
    {
        if (sumCount <= 1)
        {
            btn_Left.gameObject.SetActive(false);
            btn_Right.gameObject.SetActive(false);
        }
        else
        {
            btn_Left.gameObject.SetActive(curIndex >= 1);
            btn_Right.gameObject.SetActive(curIndex <= sumCount-2);
        }

        var showIndex = curIndex + 1;
        callback?.Invoke(showIndex);
    }
    
    //绑定按钮点击回调
    private void BindBtnLogic(Button btn, UnityAction action)
    {
        btn.onClick.RemoveAllListeners();
        if (action != null)
        {
            btn.onClick.AddListener(action);
        }
    }
}

帮助说明界面

cs 复制代码
using System.Text;
using UnityEngine;
using UnityEngine.UI;

public class ComDesc : MonoBehaviour
{
    public Text txt_Title;
    public Text txt_Desc;
    public RectTransform content;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            SetDesc("帮助", "好好学习,天天向上");
        }
            
        if (Input.GetKeyDown(KeyCode.A))
        {
            var str = "好好学习,天天向上";
            StringBuilder sb = new StringBuilder();
            for (var i = 1; i < 100; i++)
            {
                sb.Append(str);
            }
            SetDesc("帮助", sb.ToString());
        }
    }

    /// <summary>
    /// 设置说明描述
    /// </summary>
    /// <param name="title">界面标题</param>
    /// <param name="desc">说明文本</param>
    public void SetDesc(string title,string desc)
    {
        txt_Title.text = title;
        txt_Desc.text = desc;
        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
    }
}

跑马灯消息提示

有消息队列缓存,等待队列中所有消息播放完后,提示才消失。

cs 复制代码
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using TMPro;
using UnityEngine.Events;
using UnityEngine.UI;

public class Marquee : MonoBehaviour
{
    public TMP_Text tmpTxt;
    public RectTransform maskNode;
    public CanvasGroup canvasGroup;
    private float maskWidth;
    private float unitTime = 0.2f;//计算动画时间自定义标准
    private Queue<MsgNode> marqueeMsg = new Queue<MsgNode>();
    private List<int> idList = new List<int>();
    private bool isPlay;//是否正在播放消息
    
    private class MsgNode
    {
        public int id;
        public string msg;
        public int loopCount;
    }
    
    void Start()
    {
        maskWidth = maskNode.rect.width;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            var id = Random.Range(1,100);
            var str = $"id:{id}好好学习,天天向上>>";
            AddMarqueeMsg(id,str,1);
        }
        
        if(marqueeMsg.Count > 0) {
            if (!isPlay)
            {
                isPlay = true;
                tmpTxt.rectTransform.anchoredPosition = Vector2.zero;
                var data = marqueeMsg.Peek();
                idList.Remove(data.id);
                DisplayMarqueeMsg(data.msg,data.loopCount, () =>
                {
                    marqueeMsg.Dequeue();
                    if (marqueeMsg.Count == 0)
                    {
                        canvasGroup.alpha = 0;
                    }
                    isPlay = false;
                });
            }
        }
    }

    /// <summary>
    /// 在跑马灯消息队列中添加消息
    /// </summary>
    /// <param name="msgId">消息记录的唯一id</param>
    /// <param name="msg">消息内容</param>
    /// <param name="loopCount">循环播放时间</param>
    public void AddMarqueeMsg(int msgId, string msg, int loopCount)
    {
        if (idList.Contains(msgId))
        {
            Debug.LogError("消息已在预播队列");
            return;
        }
        
        if (canvasGroup.alpha < 0.95f)
        {
            canvasGroup.alpha = 1;
        }
        
        idList.Add(msgId);
        marqueeMsg.Enqueue(new MsgNode
        {
            id = msgId,
            msg = msg,
            loopCount = loopCount
        });
    }
    
    /// <summary>
    /// 跑马灯消息播放
    /// </summary>
    /// <param name="msgId">消息记录的唯一id</param>
    /// <param name="msg">消息内容</param>
    /// <param name="loopCount">循环播放时间</param>
    public void DisplayMarqueeMsg(string msg,int loopCount,UnityAction callback)
    {
        tmpTxt.text = msg;
        LayoutRebuilder.ForceRebuildLayoutImmediate(tmpTxt.rectTransform);
        var width = tmpTxt.rectTransform.rect.width+maskWidth;
        var duration = GetDuration(width);
        tmpTxt.rectTransform.DOAnchorPosX(-width, duration)
            .SetEase(Ease.Linear)
            .SetLoops(loopCount, LoopType.Restart)
            .OnComplete(() =>
            {
                callback?.Invoke();
            });
    }

    //根据消息长度计算动画匀速运行时间
    private float GetDuration(float width)
    {
        var offset1 = (int)width / 100;
        var offset2 = width % 100 == 0 ?0:1;
        var offset = offset1 + offset2;
        return offset * unitTime;
    }
}
相关推荐
太妃糖耶31 分钟前
Shader中着色器的编译目标级别
unity·shader·着色器
TWO8572 小时前
Unity背景音乐管理器实现方法
unity·游戏引擎
咩咩觉主14 小时前
C# &Unity 唐老狮 No.8 模拟面试题
开发语言·unity·c#
DanmF--16 小时前
实现客户端的网络不影响主线程且随时与服务器通信
网络·网络协议·tcp/ip·unity
梦起丶20 小时前
「 DelegateUI 」Ant-d 风格的 Qt Qml UI 套件
qt·ui·qml·ant-d·ui-kit
c#上位机1 天前
winform中chart控件解决显示大量曲线数据卡顿方法——删旧添新法
ui·c#·winform
咩咩觉主1 天前
[Unity] 封装一个依赖于MonoBehaviour的计时器(上)
unity·c#·游戏引擎
表面矿工1 天前
unity相机缩放
数码相机·unity·游戏引擎
轻口味1 天前
【每日学点HarmonyOS Next知识】顶部状态栏、文本最大行数后缀、弹窗背景、状态栏颜色、导航
ui·华为·harmonyos·harmonyosnext