在我们日常的开发中,红点几乎是一个必不可少的流程。各类游戏或多或少都会有红点提示。想要实现红点这个功能其实我们也可以通过最简单的在某个地方放上一个红点的图片,然后根据条件来判断它是否需要显示。这么一来虽然可以实现这个功能,但是如果红点的数量较多,或者样式比较多,我们就会难以进行管理。所以我们在设计之初可以将它统一进行管理。
先看下结构吧:
第一张就用来模拟我们平时游戏进入的主界面,现在主界面有邮件和任务两个按钮,点击它们可以打开对应的面板。
第二张图:通过邮件按钮打开的邮件面板了。这个红点系统采用的是一种树的结构,大致如下图示意:
这是大佬文章里面的图,我没有做修改,可以把图片里面的系统,队伍看成我图片里面的奖励列表和消息列表。应该不难理解。
再来说说一个红点应该具备什么呢?
- 父红点(比如我们邮件系统里面的奖励列表按钮上如果挂载一个红点A,那么它的父红点就是我们主界面的邮件按钮上的红点B,如果A变化那么B也要随之变化)
- 红点名称
- 数字红点的数量。如果当前红点是可以显示数字的那种,那么需要显示数量
- 子红点:有了父红点当然有子红点
- 红点类型:有正常的一个红色图片啥也不显示的那种,代码里面我就命名为NormalPoint,有数字红点,红点上可以显示数字的。还有比如上了一个新的活动需要在红点上加上一个NEW的字样。就叫他自定义红点吧
- 发生改变的回调。
- 路径Key(我这里是采用的读表然后加载红点预制体,也可以一开始就将红点放在你需要的现实的预制体上)
- 当前红点的父物体(就是该红点挂载在哪里,和父红点不是同一个东西)
- 实例化出来的红点GameObject
然后再来红点类的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 红点的类型
/// </summary>
public enum RedType
{
NUMBERPOINT = 1,//数字红点
NORMALPOINT = 2,//正常红点
CUSTOMPOINT = 3//用户自定义的红点
}
/// <summary>
/// 红点对象
/// </summary>
public class RedPointNode
{
/// <summary>
/// 当前红点的父红点(也就是受当前红点影响的上一级红点)
/// </summary>
public RedPointNode parent = null;
/// <summary>
/// 当前红点节点名称
/// </summary>
public string redPointNodeName;
/// <summary>
/// 数字红点的数量
/// </summary>
public int pointNum = 0;
/// <summary>
/// 该红点下的子红点(也就是当前红点受哪些红点的影响)
/// </summary>
public Dictionary<string, RedPointNode> dicChilds = new Dictionary<string, RedPointNode>();
/// <summary>
/// 红点类型
/// </summary>
public RedType redType;
/// <summary>
/// 红点发生改变的回调
/// </summary>
public RedPointSystem.OnPointChange pointChangeFunc;
/// <summary>
/// 红点预制体在配表中的路径Key
/// </summary>
public string pathKey;
/// <summary>
/// 当前红点的父物体(红点挂载在哪个预制体下)
/// </summary>
public Transform parentTransform = null;
public GameObject curRedNodeObj;
public void SetRedPointNum(int num)
{
//if (dicChilds.Count > 0)
//{
// Debug.LogError("Only Can Set Leaf Node");
// return;
//}
pointNum = num;
NotifyPointNumChange(num);
if (parent != null)
{
parent.ChangePredPointNum();
}
}
public void ChangePredPointNum()
{
int num = 0;
foreach (RedPointNode item in dicChilds.Values)
{
num += item.pointNum;
}
if (num != pointNum)
{
pointNum = num;
NotifyPointNumChange(pointNum);
}
}
public void NotifyPointNumChange(int num)
{
if (curRedNodeObj != null)
{
RedType type = this.redType;
this.curRedNodeObj.SetActive(num > 0);
if (this.redType == RedType.NUMBERPOINT && num > 0)
{
Text numText = this.curRedNodeObj.transform.Find("Text").GetComponent<Text>();
numText.text = num.ToString();
}
pointChangeFunc?.Invoke(this);
}
}
}
一个红点所需要具备的属性说完了,回到我们刚刚说的树结构。上一段代码:
public class RedPointConst
{
/// <summary>
/// 主界面
/// </summary>
public const string main = "Main";
/// <summary>
/// 主界面任务系统
/// </summary>
public const string task = "Main.Task";
/// <summary>
/// 主界面邮件系统
/// </summary>
public const string mail = "Main.Mail";
/// <summary>
/// 邮件奖励列表
/// </summary>
public const string mailRewardList = "Main.Mail.RewardList";
/// <summary>
/// 邮件消息列表
/// </summary>
public const string mailInfoList = "Main.Mail.InfoList";
}
public class RedPrefabPathKey
{
public const string newRed = "newRed";
public const string normalRed = "normalRed";
public const string numberRed = "numberRed";
}
应该可以很清楚的看到我们的规则,按照系统来划分树的各个节点。接下来看看定义这些字符串常量有什么作用。
RedPointNode rootNode;
List<string> redPointTreeList = new List<string>
{
RedPointConst.main,
RedPointConst.task,
RedPointConst.mail,
RedPointConst.mailRewardList,
RedPointConst.mailInfoList
};
/// <summary>
/// 初始化红点树
/// </summary>
public void InitializePointTree()
{
rootNode = new RedPointNode();
rootNode.redPointNodeName = RedPointConst.main;
foreach (string item in redPointTreeList)
{
RedPointNode pointNode = rootNode;
string[] treeNodeArr = item.Split('.');
if (treeNodeArr[0] != pointNode.redPointNodeName)
{
Debug.LogError("RedPointTree Root Node Error"+ treeNodeArr[0]);
continue;
}
if (treeNodeArr.Length > 1)
{
for (int i = 1; i < treeNodeArr.Length; i++)
{
if (!pointNode.dicChilds.ContainsKey(treeNodeArr[i]))
{
pointNode.dicChilds.Add(treeNodeArr[i],new RedPointNode());
}
pointNode.dicChilds[treeNodeArr[i]].redPointNodeName = treeNodeArr[i];
pointNode.dicChilds[treeNodeArr[i]].parent = pointNode;
pointNode = pointNode.dicChilds[treeNodeArr[i]];
}
}
}
}
这段代码也挺简单的,主要就是把刚刚定义的字符串拆分开来,这样一来每个节点都是一个RedPoint了,而且是符合树的结构。这就是红点树的初始化。我们在开发中这段代码可以在逻辑类中调用。只需要被调用一次即可。
刚刚把红点树初始化完毕了。那么接下来呢,就是要对红点的属性进行设置了。来看下方法:
public void SetRedPointNodeCallBack(string nodeName, OnPointChange callBack,RedType redType,Transform parent)
{
string[] nodeNameArr = nodeName.Split('.');
if (nodeNameArr.Length == 1)
{
if (nodeNameArr[0] != RedPointConst.main)
{
Debug.LogError("Get Wrong Node! Current Node "+ nodeNameArr[0]);
return;
}
}
RedPointNode pointNode = rootNode;
for (int i = 1; i < nodeNameArr.Length; i++)
{
if (!pointNode.dicChilds.ContainsKey(nodeNameArr[i]))
{
Debug.LogError("Dont contains child Node "+ nodeNameArr[i]);
return;
}
pointNode = pointNode.dicChilds[nodeNameArr[i]];
if (i == nodeNameArr.Length - 1)
{
pointNode.redType = redType;
string key = "";
if (redType == RedType.NORMALPOINT)
key = RedPrefabPathKey.normalRed;
else if (redType == RedType.NUMBERPOINT)
key = RedPrefabPathKey.numberRed;
else if (redType == RedType.CUSTOMPOINT)
key = RedPrefabPathKey.newRed;
pointNode.pathKey = key;
if (parent != null)
pointNode.parentTransform = parent;
string prefabPath = null;
GameObject prefab = null;
dicRedPrefabs.TryGetValue(key, out prefabPath);
if (!string.IsNullOrEmpty(prefabPath))
prefab = ResourcesMgr.GetInstance().LoadAsset(prefabPath, false);
pointNode.curRedNodeObj = prefab;
prefab.transform.SetParent(parent);
prefab.transform.localPosition = Vector3.one;
prefab.transform.localScale = Vector3.one;
prefab.transform.SetAsLastSibling();
RectTransform rectTransform = prefab.transform.GetComponent<RectTransform>();
rectTransform.anchorMin = Vector2.one;
rectTransform.anchorMax = Vector2.one;
pointNode.pointChangeFunc = callBack;
}
}
}
就是设置一些基础的属性,应该也不难理解,属性设置好了以后呢,我们需要驱动这个红点,让它做出正确的显示:
public void SetInvoke(string nodeName,int pointNum)
{
string[] nodeNameArr = nodeName.Split('.');
if (nodeNameArr.Length == 1)
{
if (nodeNameArr[0] != RedPointConst.main)
{
Debug.LogError("Get Wrong Node! Current Node " + nodeNameArr[0]);
return;
}
}
RedPointNode pointNode = rootNode;
for (int i = 1; i < nodeNameArr.Length; i++)
{
if (!pointNode.dicChilds.ContainsKey(nodeNameArr[i]))
{
Debug.LogError("Dont contains child Node " + nodeNameArr[i]);
return;
}
pointNode = pointNode.dicChilds[nodeNameArr[i]];
if (i == nodeNameArr.Length - 1)
{
pointNode.SetRedPointNum(pointNum);
}
}
}
到这里红点系统的一些基础方法都贴上来了,来看看完整的吧:
using FirstUIFrame;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RedPointSystem
{
private Dictionary<string, string> dicRedPrefabs;
public static RedPointSystem instance;
public delegate void OnPointChange(RedPointNode redPoint);
RedPointNode rootNode;
List<string> redPointTreeList = new List<string>
{
RedPointConst.main,
RedPointConst.task,
RedPointConst.mail,
RedPointConst.mailRewardList,
RedPointConst.mailInfoList
};
private RedPointSystem()
{
dicRedPrefabs = new Dictionary<string, string>();
GetUIPathByJson();
}
public static RedPointSystem GetInstance()
{
if (instance == null)
instance = new RedPointSystem();
return instance;
}
/// <summary>
/// 初始化红点树
/// </summary>
public void InitializePointTree()
{
rootNode = new RedPointNode();
rootNode.redPointNodeName = RedPointConst.main;
foreach (string item in redPointTreeList)
{
RedPointNode pointNode = rootNode;
string[] treeNodeArr = item.Split('.');
if (treeNodeArr[0] != pointNode.redPointNodeName)
{
Debug.LogError("RedPointTree Root Node Error"+ treeNodeArr[0]);
continue;
}
if (treeNodeArr.Length > 1)
{
for (int i = 1; i < treeNodeArr.Length; i++)
{
if (!pointNode.dicChilds.ContainsKey(treeNodeArr[i]))
{
pointNode.dicChilds.Add(treeNodeArr[i],new RedPointNode());
}
pointNode.dicChilds[treeNodeArr[i]].redPointNodeName = treeNodeArr[i];
pointNode.dicChilds[treeNodeArr[i]].parent = pointNode;
pointNode = pointNode.dicChilds[treeNodeArr[i]];
}
}
}
}
public void SetRedPointNodeCallBack(string nodeName, OnPointChange callBack,RedType redType,Transform parent)
{
string[] nodeNameArr = nodeName.Split('.');
if (nodeNameArr.Length == 1)
{
if (nodeNameArr[0] != RedPointConst.main)
{
Debug.LogError("Get Wrong Node! Current Node "+ nodeNameArr[0]);
return;
}
}
RedPointNode pointNode = rootNode;
for (int i = 1; i < nodeNameArr.Length; i++)
{
if (!pointNode.dicChilds.ContainsKey(nodeNameArr[i]))
{
Debug.LogError("Dont contains child Node "+ nodeNameArr[i]);
return;
}
pointNode = pointNode.dicChilds[nodeNameArr[i]];
if (i == nodeNameArr.Length - 1)
{
pointNode.redType = redType;
string key = "";
if (redType == RedType.NORMALPOINT)
key = RedPrefabPathKey.normalRed;
else if (redType == RedType.NUMBERPOINT)
key = RedPrefabPathKey.numberRed;
else if (redType == RedType.CUSTOMPOINT)
key = RedPrefabPathKey.newRed;
pointNode.pathKey = key;
if (parent != null)
pointNode.parentTransform = parent;
string prefabPath = null;
GameObject prefab = null;
dicRedPrefabs.TryGetValue(key, out prefabPath);
if (!string.IsNullOrEmpty(prefabPath))
prefab = ResourcesMgr.GetInstance().LoadAsset(prefabPath, false);
pointNode.curRedNodeObj = prefab;
prefab.transform.SetParent(parent);
prefab.transform.localPosition = Vector3.one;
prefab.transform.localScale = Vector3.one;
prefab.transform.SetAsLastSibling();
RectTransform rectTransform = prefab.transform.GetComponent<RectTransform>();
rectTransform.anchorMin = Vector2.one;
rectTransform.anchorMax = Vector2.one;
pointNode.pointChangeFunc = callBack;
}
}
}
public void SetInvoke(string nodeName,int pointNum)
{
string[] nodeNameArr = nodeName.Split('.');
if (nodeNameArr.Length == 1)
{
if (nodeNameArr[0] != RedPointConst.main)
{
Debug.LogError("Get Wrong Node! Current Node " + nodeNameArr[0]);
return;
}
}
RedPointNode pointNode = rootNode;
for (int i = 1; i < nodeNameArr.Length; i++)
{
if (!pointNode.dicChilds.ContainsKey(nodeNameArr[i]))
{
Debug.LogError("Dont contains child Node " + nodeNameArr[i]);
return;
}
pointNode = pointNode.dicChilds[nodeNameArr[i]];
if (i == nodeNameArr.Length - 1)
{
pointNode.SetRedPointNum(pointNum);
}
}
}
private void GetUIPathByJson()
{
IConfigManager configManager = new ConfigManager("UIFormsConfigInfo");
dicRedPrefabs = configManager.AppSetting;
}
}
这其中有通过读表来加载预制体,如果想试验一下的话,可以把它用其他方式代替。然后我们来看看整个流程的代码:
public class MainView : BaseUIForm
{
private Transform mail;
private Transform task;
private void Awake()
{
CurrentUIType.uiShowType = UIFormShowType.HideOther;
CurrentUIType.uiFormPositionType = UIFormPositionType.Normal;
mail = this.transform.Find("Mail");
task = this.transform.Find("Task");
}
private void Start()
{
RegisterClickEvent("Mail", (obj) => { UIManager.GetInstance().OpenUIForm("Mail"); });
RegisterClickEvent("Task", (obj) => { UIManager.GetInstance().OpenUIForm("Task"); });
RedPointSystem.GetInstance().InitializePointTree();
RedPointSystem.GetInstance().SetRedPointNodeCallBack(RedPointConst.mail, null, RedType.NUMBERPOINT, mail.transform);
RedPointSystem.GetInstance().SetInvoke(RedPointConst.mail, 4);
}
}
public class Mail : BaseUIForm
{
private Transform rewardList;
private Transform infoList;
private void Awake()
{
CurrentUIType.uiShowType = UIFormShowType.HideOther;
CurrentUIType.uiFormPositionType = UIFormPositionType.Normal;
rewardList = this.transform.Find("RewardList");
infoList = this.transform.Find("InfoList");
}
// Start is called before the first frame update
void Start()
{
RegisterClickEvent("BtnClose", (obj) =>
{ UIManager.GetInstance().CloseUIForm("Mail");
UIManager.GetInstance().OpenUIForm("MainView");
});
RegisterClickEvent("RewardList", (obj) =>
{
RedPointSystem.GetInstance().SetInvoke(RedPointConst.mailRewardList, 0);
});
RegisterClickEvent("InfoList", (obj) =>
{
RedPointSystem.GetInstance().SetInvoke(RedPointConst.mailRewardList, 0);
});
RedPointSystem.GetInstance().SetRedPointNodeCallBack(RedPointConst.mailRewardList, null, RedType.NUMBERPOINT, rewardList.transform);
RedPointSystem.GetInstance().SetRedPointNodeCallBack(RedPointConst.mailInfoList, null, RedType.NUMBERPOINT, infoList.transform);
RedPointSystem.GetInstance().SetInvoke(RedPointConst.mailRewardList, 2);
RedPointSystem.GetInstance().SetInvoke(RedPointConst.mailInfoList, 2);
}
// Update is called once per frame
void Update()
{
}
}
第一段执行后的效果是:
当我点击邮件按钮的时候会打开Mail面板执行第二段代码效果如下:
如果此时我再奖励列表的按钮,会将红点的上显示的数量设置为0(模拟真实开发的时候条件),从而达到隐藏红点的目的:
此时再点击关闭会回到主界面,此时主界面红点显示成为2
到此为止,红点系统完成,应该有些没有想到位的地方,可能存在设计不正确,欢迎大家提出。