功能描述

红点系统就做了两件事:

  1. 出现通知时,从叶级UI控件一直到根级出现红点;
  2. 玩家打开到叶级面板,点击叶级控件后,红点消失,连带上面所有级控件的红点更新;

设计

有一个独立于UI系统存在的多叉树,分叉结构和UI控件的分叉结构一样。但是UI系统要依赖于节点树。

出现通知时红点是从叶向根传的,节点需要知道它的父节点,在通知数变化时通知父节点。父节点需要知道它的子节点,以便统计自己的通知数。

节点和红点控件出现的顺序有两种情况,

  1. 通知先出现,相关的控件一打开就有红点;
  2. 控件开着,此时有了通知,出现红点;

  1. 针对通知先有,玩家点击按钮打开面板时,显示面板时按钮找一下自己的节点,有通知就显示红点。需要控件引用节点。
  2. 针对控件先有,显示按钮时先找到或创建自己的节点,添加监听。出现通知时分发事件。也是控件引用节点。

类设计

红点树节点

  1. 有自己的红点路径,路径用比如/分割。
  2. 节点应该能得到自己的所有子节点,以确定自己显不显示红点。
  3. 还要能得到自己的父节点,以便自己的红点变化时通知父节点。
  4. 有自己红点状态变化时触发的委托。不依赖任何UI,不知道UI的存在。
  5. 有激活红点的方法,会找到自己的父级,父级也激活红点。
  6. 有消除红点的方法,会找到自己的父级,通知父级遍历自己的子级确定自己要不要显示红点。

红点树管理器

  1. 有一个字典,键是路径,值是树节点。
  2. 有新建节点的方法,输入一个路径,把该节点和所有级父节点都建好。
  3. 有增加红点的方法,输入一个路径,如果此节点还不存在,则建立,然后节点激活红点,且让父级激活红点(每个有父级的节点都这么做)。

红点UI组件

  1. 有自己监听的节点的路径,从检查器填。
  2. 有对自己监听的节点的引用。
  3. 有显示隐藏红点的方法。
  4. 初始化时自己监听的节点如果没有则要建好。
  5. 点击按钮清除红点的功能可以让按钮脚本去找红点,也可以红点UI去找按钮。

调试工具

可以输入任意路径,建立红点。

红点信息的保存、序列化

现在我想到,哪些按钮有红点是否属于玩家数据的一部分?是的。正如我们看到红点,关闭游戏,再打开,仍然能看到红点,它和玩家数值、道具这些是一并存储的。这时想到,存储红点数据是否就是在一份道具的数据类里加一个bool isNew。然后怎么知道它关联的UI以及每一级父UI?

应该让按钮加载时去找它对应的节点,决定自己是否显示红点以及数量。需要在按钮上挂某个组件,或者直接继承按钮。

看了一套源代码,是树管理器用总字典存所有的节点路径。红点Mono通过检查器配置自己的路径,开始时红点Mono把自己路径的节点加入管理器的总字典,但是不会把路径里自己的父节点加入。会从根节点一级一级加入节点的子节点数组。

此时我们大概写一下红点系统的需求

  1. 有红点的按钮身上挂有红点组件,或者写一个继承按钮的组件;
  2. 开始时红点组件新建对应的节点,需要有某种方法说明它在树里的位置。可以是字符串绝对路径,或者字符串分割太费劲用字符串列表,这样管理器的字典的键就是一个字符串列表?不,用字符串列表标记节点的路径不适合总字典,此时用记录根节点的字典。
  3. 新通知通知到红点组件,红点组件依次通知父级。然而有通知时叶子节点一般是不存在或隐藏的,通知应该是在节点发生,不依赖红点组件的。对于根按钮,是节点通知红点组件显示红点,对于里面的按钮,是按钮显示时读取对应的节点有没有通知来决定显示红点。
  4. 玩家点击叶节点按钮,对应节点的通知状态消失,通知红点组件关闭通知,通知每一层父级重新计算通知数
  5. 树需要一个根据路径查/建一体的返回所查节点的函数。

总结:红点系统是一个存在于内存,不依赖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把节点建好。这要在显示大厅面板之前,然后大厅面板显示时能知道哪些按钮显示红点。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐