概述
在游戏开发中,常常会遇到一些通用的界面逻辑,它不论在什么类型的游戏中都会出现。为了避免重复造轮子,本文总结并提供了一些常用UI界面的实现逻辑。希望可以帮助大家快速开发通用界面模块,也可以在次基础上进行扩展修改,以适应你项目的需求。
工程链接:GitCode - 全球开发者的开源社区,开源代码托管平台
二次确认界面
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不好控制选中事件的触发和选中状态表现。通过按钮来自定义组件可以更好地控制逻辑的调用和标签的显示。
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);
}
}
}
飘字提示
简易版本
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;
}
}
升级版本
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--;
}));
}
}
}
左右切换按钮组
本组件一般出现在查看英雄界面,点击左右两个按钮切换查看按钮的详细信息。在英雄列表中,第一个英雄的左按钮不显示,最后一个英雄的右按钮不显示。
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);
}
}
}
帮助说明界面
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);
}
}
跑马灯消息提示
有消息队列缓存,等待队列中所有消息播放完后,提示才消失。
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;
}
}