Unity红点系统笔记

设计

我们从不会开始思考。我现在只知道它使用了叫"前缀树"的结构。

然后注意到玩家一开始看见的是树根,只能从树根向树叶开始操作。引起红点的要么是网络发来的操作,要么是玩家的操作引起另一个系统的更新,比如成就系统。引起红点的都是叶节点。

如果要尽快写一个简单的测试案例,就在主界面放一个按钮,有红点,按下弹出一个面板,上面有一个按钮,有红点。至少2个红点。这是为了测试红点的传递。如果要测试任何一个叶子都会引起上级红点,最少要3个红点。

红点是从叶向根传的,节点需要知道它的父节点。父节点好像不需要知道它的子节点?

注意到引起红点的节点引起红点时,它相应的UI可能不存在,比如玩家刚进入游戏看见邮箱按钮的红点。所以这个红点类应该并不绑定在UI控件上。玩家点击按钮打开面板时,显示面板的过程中有一个决定哪些按钮显示红点的过程。就像动态加载数据一样。

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

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

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

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

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

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

相关推荐
郭逍遥3 小时前
[Godot] C#基于噪声的简单TileMap地图生成
游戏引擎·godot
作孽就得先起床9 小时前
unity UnauthorizedAccessException: 拒绝访问路径
unity·游戏引擎
tealcwu11 小时前
【Unity踩坑】Unity项目提示文件合并有冲突
elasticsearch·unity·游戏引擎
tealcwu1 天前
【Unity小技巧】如何将3D场景转换成2D场景
3d·unity·游戏引擎
全栈陈序员1 天前
用Rust和Bevy打造2D平台游戏原型
开发语言·rust·游戏引擎·游戏程序
鹿野素材屋1 天前
Unity模型中人形角色的嘴巴一直开着怎么办
unity
世洋Blog1 天前
Unity面经-List底层原理、如何基于数组、如何扩容、List存储泛型、List有关在内存中的结构
unity·面试·c#·list
神秘的土鸡2 天前
【CS创世SD NAND征文】为无人机打造可靠数据仓:工业级存储芯片CSNP32GCR01-AOW在飞控系统中的应用实践
嵌入式硬件·游戏引擎·无人机·cocos2d·雷龙
jtymyxmz2 天前
《Unity Shader》6.4.3 半兰伯特模型
unity·游戏引擎