设计
我们从不会开始思考。我现在只知道它使用了叫"前缀树"的结构。
然后注意到玩家一开始看见的是树根,只能从树根向树叶开始操作。引起红点的要么是网络发来的操作,要么是玩家的操作引起另一个系统的更新,比如成就系统。引起红点的都是叶节点。
如果要尽快写一个简单的测试案例,就在主界面放一个按钮,有红点,按下弹出一个面板,上面有一个按钮,有红点。至少2个红点。这是为了测试红点的传递。如果要测试任何一个叶子都会引起上级红点,最少要3个红点。
红点是从叶向根传的,节点需要知道它的父节点。父节点好像不需要知道它的子节点?
注意到引起红点的节点引起红点时,它相应的UI可能不存在,比如玩家刚进入游戏看见邮箱按钮的红点。所以这个红点类应该并不绑定在UI控件上。玩家点击按钮打开面板时,显示面板的过程中有一个决定哪些按钮显示红点的过程。就像动态加载数据一样。
现在我想到,哪些按钮有红点是否属于玩家数据的一部分?是的。正如我们看到红点,关闭游戏,再打开,仍然能看到红点,它和玩家数值、道具这些是一并存储的。这时想到,存储红点数据是否就是在一份道具的数据类里加一个bool isNew。然后怎么知道它关联的UI以及每一级父UI?
应该让按钮加载时去找它对应的节点,决定自己是否显示红点以及数量。需要在按钮上挂某个组件,或者直接继承按钮。
看了一套源代码,是树管理器用总字典存所有的节点路径。红点Mono通过检查器配置自己的路径,开始时红点Mono把自己路径的节点加入管理器的总字典,但是不会把路径里自己的父节点加入。会从根节点一级一级加入节点的子节点数组。
此时我们大概写一下红点系统的需求
- 有红点的按钮身上挂有红点组件,或者写一个继承按钮的组件;
- 开始时红点组件新建对应的节点,需要有某种方法说明它在树里的位置。可以是字符串绝对路径,或者字符串分割太费劲用字符串列表,这样管理器的字典的键就是一个字符串列表?不,用字符串列表标记节点的路径不适合总字典,此时用记录根节点的字典。
- 新通知通知到红点组件,红点组件依次通知父级。然而有通知时叶子节点一般是不存在或隐藏的,通知应该是在节点发生,不依赖红点组件的。对于根按钮,是节点通知红点组件显示红点,对于里面的按钮,是按钮显示时读取对应的节点有没有通知来决定显示红点。
- 玩家点击叶节点按钮,对应节点的通知状态消失,通知红点组件关闭通知,通知每一层父级重新计算通知数
- 树需要一个根据路径查/建一体的返回所查节点的函数。
总结:红点系统是一个存在于内存,不依赖UI存在的树,通常用字符串作节点的键。红点所在的按钮显示(OnEnable)时,去寻找(找不到就创建)它对应的节点,相互关联。红点组件可能给节点树发消息(按叶节点按钮解除通知),节点树也可能给红点组件发消息。程序内部出现通知时,也是发出一个路径,查找或创建目标节点,更新通知数量。所以树的查找/创建节点并返回节点的方法很重要。
cs
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;
}
}
cs
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);
}
}
}
cs
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把节点建好。这要在显示大厅面板之前,然后大厅面板显示时能知道哪些按钮显示红点。