功能描述
红点系统就做了两件事:
- 出现通知时,从叶级UI控件一直到根级出现红点;
- 玩家打开到叶级面板,点击叶级控件后,红点消失,连带上面所有级控件的红点更新;
设计
有一个独立于UI系统存在的多叉树,分叉结构和UI控件的分叉结构一样。但是UI系统要依赖于节点树。
出现通知时红点是从叶向根传的,节点需要知道它的父节点,在通知数变化时通知父节点。父节点需要知道它的子节点,以便统计自己的通知数。
节点和红点控件出现的顺序有两种情况,
- 通知先出现,相关的控件一打开就有红点;
- 控件开着,此时有了通知,出现红点;
- 针对通知先有,玩家点击按钮打开面板时,显示面板时按钮找一下自己的节点,有通知就显示红点。需要控件引用节点。
- 针对控件先有,显示按钮时先找到或创建自己的节点,添加监听。出现通知时分发事件。也是控件引用节点。
红点树节点
- 有自己的红点路径,路径用比如/分割。
- 节点应该能得到自己的所有子节点,以确定自己显不显示红点。
- 还要能得到自己的父节点,以便自己的红点变化时通知父节点。
- 有自己红点状态变化时触发的委托。不依赖任何UI,不知道UI的存在。
- 有激活红点的方法,会找到自己的父级,父级也激活红点。
- 有消除红点的方法,会找到自己的父级,通知父级遍历自己的子级确定自己要不要显示红点。
红点树管理器
- 有一个字典,键是路径,值是树节点。
- 有新建节点的方法,输入一个路径,把该节点和所有级父节点都建好。
- 有增加红点的方法,输入一个路径,如果此节点还不存在,则建立,然后节点激活红点,且让父级激活红点(每个有父级的节点都这么做)。
新建一条红点路径:优化
业务层新建一个红点时,都是叶节点,从根-枝-叶的节点都要建好。然后常规思路是把路径分割成从开头到第1个/、第2个/……直到完整路径,全部建立节点。容易产生大量临时字符串。
为了优化,我们想到叶子节点拥有最长的路径,枝节点可以复用这个最长路径,+开始、结束索引。
红点UI组件
- 有自己监听的节点的路径,从检查器填。
- 有对自己监听的节点的引用。
- 有显示隐藏红点的方法。
- 初始化时自己监听的节点如果没有则要建好。
- 点击按钮清除红点的功能可以让按钮脚本去找红点,也可以红点UI去找按钮。
调试工具
可以输入任意路径,建立红点。
红点信息的保存、序列化
现在我想到,哪些按钮有红点是否属于玩家数据的一部分?是的。正如我们看到红点,关闭游戏,再打开,仍然能看到红点,它和玩家数值、道具这些是一并存储的。这时想到,存储红点数据是否就是在一份道具的数据类里加一个bool isNew。然后怎么知道它关联的UI以及每一级父UI?
应该让按钮加载时去找它对应的节点,决定自己是否显示红点以及数量。需要在按钮上挂某个组件,或者直接继承按钮。
看了一套源代码,是树管理器用总字典存所有的节点路径。红点Mono通过检查器配置自己的路径,开始时红点Mono把自己路径的节点加入管理器的总字典,但是不会把路径里自己的父节点加入。会从根节点一级一级加入节点的子节点数组。
此时我们大概写一下红点系统的需求
- 有红点的按钮身上挂有红点组件,或者写一个继承按钮的组件;
- 开始时红点组件新建对应的节点,需要有某种方法说明它在树里的位置。可以是字符串绝对路径,或者字符串分割太费劲用字符串列表,这样管理器的字典的键就是一个字符串列表?不,用字符串列表标记节点的路径不适合总字典,此时用记录根节点的字典。
- 新通知通知到红点组件,红点组件依次通知父级。然而有通知时叶子节点一般是不存在或隐藏的,通知应该是在节点发生,不依赖红点组件的。对于根按钮,是节点通知红点组件显示红点,对于里面的按钮,是按钮显示时读取对应的节点有没有通知来决定显示红点。
- 玩家点击叶节点按钮,对应节点的通知状态消失,通知红点组件关闭通知,通知每一层父级重新计算通知数
- 树需要一个根据路径查/建一体的返回所查节点的函数。
总结:红点系统是一个存在于内存,不依赖UI存在的树,通常用字符串作节点的键。红点所在的按钮显示(OnEnable)时,去寻找(找不到就创建)它对应的节点,相互关联。红点组件可能给节点树发消息(按叶节点按钮解除通知),节点树也可能给红点组件发消息。程序内部出现通知时,也是发出一个路径,查找或创建目标节点,更新通知数量。所以树的查找/创建节点并返回节点的方法很重要。
using UnityEngine.Events; using System.Collections.Generic; using UnityEngine; public class MyTreeNode { public List<string>path= new List<string>(); MyTreeNode parent; public Dictionary<string,MyTreeNode> children= new Dictionary<string, MyTreeNode>(); UnityAction<int> updateNoti; public MyTreeNode(List<string>path,MyTreeNode parent) { this.path = path; this.parent = parent; } int notiNum; public int NotiNum { set { if (value >= 0) { notiNum = value; if(parent != null) { parent.Recalculate(); } updateNoti?.Invoke(value); } } get { return notiNum; } } public void Recalculate() { int temp = 0; foreach(KeyValuePair<string,MyTreeNode> kvp in children) { temp += kvp.Value.NotiNum; } NotiNum = temp; } public void AddListener(UnityAction<int> callback) { updateNoti += callback; } public void RemoveListener(UnityAction<int> callback) { updateNoti -= callback; } }using System.Collections; using System.Collections.Generic; using UnityEngine; public class MyTreeManager : MySingleton<MyTreeManager> { Dictionary<string, MyTreeNode> nodeDic = new Dictionary<string, MyTreeNode>(); public MyTreeNode GetOrBuildNode(List<string> path)//建好从根到此节点的节点 { Dictionary<string, MyTreeNode> dic = nodeDic; MyTreeNode parent=null; for (int i = 0; i < path.Count; i++) { if (!dic.ContainsKey(path[i])) { dic[path[i]] = new MyTreeNode(path.GetRange(0, i), parent); } parent = dic[path[i]]; dic = dic[path[i]].children; } return parent; } public void PrintRoots() { foreach(KeyValuePair<string,MyTreeNode>p in nodeDic) { Debug.Log(p.Key); } } }using UnityEngine.UI; using System.Collections.Generic; using UnityEngine; public class RedDotMono : MonoBehaviour { public List<string>path= new List<string>(); MyTreeNode node; public Image reddot; public Text textNotiNum; void Awake() { reddot.gameObject.SetActive(false); } private void OnEnable() { node=MyTreeManager.Instance.GetOrBuildNode(path); node.AddListener(ShowDot); ShowDot(node.NotiNum); } private void OnDisable() { node.RemoveListener(ShowDot); } public void ShowDot(int notiNum) { if (notiNum > 0) { reddot.gameObject.SetActive(true); textNotiNum.text = notiNum.ToString(); } else { reddot.gameObject.SetActive(false); } } public void CloseNoti() { node.NotiNum = 0; } }应用和遇到的问题
现在我想把红点系统应用于一些动态生成的按钮,它们来自同一个预制体。那么红点控件的path也不能在检查器写,需要代码生成。比如我新解锁了一关,然后关卡按钮和关卡界面那关格子显示红点,点击格子查看详情后红点消失。
关卡数据是从配表读取,在配表写死。那么就应该在关卡配表的关卡条目中加一个path。解锁新关卡时添加树节点,path来自关卡配表条目,关卡按钮显示时先得到自己的path,寻找节点,找到就显示红点。
再想象我们需要新获得装备时在装备格子显示红点。需要在玩家装备数据的装备条目里加redDotPath,一件装备是新获得的就有红点路径。玩家可能没点红点就退出,下次打开应该还能看见。装备格子的红点路径好像是不需要精确到格子本身的路径的,只要读到路径不为空,就显示自己的红点。而路径用于建好前级的节点,以便打开背包的按钮能找到自己的节点,显示红点。
例如获得的装备路径就是pack1,格子显示时不是去找节点而是看到这个路径不为空就显示红点。打开游戏时就读取玩家装备数据,根据path把节点建好。这要在显示大厅面板之前,然后大厅面板显示时能知道哪些按钮显示红点。