游戏红点系统的设计 -- 红点树

347 阅读3分钟

红点提示是一种常见的用户界面元素,用于提示玩家有未读的消息或待处理的任务,那么它是怎么实现的呢?

逻辑说明

UI上的红点有上下级关系,比如游戏主界面有个通行证按钮可以有红点显示,点开的通行证界面有多个子页面,切换页面的按钮可以有红点显示

image.png

image.png 我们想一想这里红点的逻辑,打开的界面里的换页按钮红点“出现”,外面打开界面的按钮的红点就需要“出现”。 (这里说的出现是逻辑上的需要出现,实际界面没打开,看不到红点就不需要显示,就是业务开发中常用的逻辑和显示分离)

通行证界面的每个分页按钮又可能与分页内的一些领取奖励红点有上下级关系,这种多叉的关系在数据结构上我们可以用一棵树来表示。

image.png

我们的子节点需要显示红点时,该节点向上的父节点一直到根节点也需要显示。

下面在使用Unity和C# 演示代码实现

代码实现

我们需要定义一个节点类表示一个红点节点

public class ReDotNode
{
    int mId; // 标识某个节点

    public int Id
    {
       get { return mId; }
    }

    int mValue; // 当前节点的逻辑值,大于0则需要显示表现上的红点

    public int Value
    {
       get { return mValue; }
       set { mValue = value; }
    }
    
    bool mIsDirty;

    public bool IsDirty
    {
        get { return mIsDirty; }
        set { mIsDirty = value; }
    }

    public List<ReDotNode> mChild;
    
    public void AddChild(ReDotNode node)
    {
       if (mChild == null)
          mChild = new List<ReDotNode>();
       mChild.Add(node);
       node.mParent = this;
    }

    public Func<int> mCheckFun; // 更新节点值的委托
    public Action<int> mUpdateFun; // 更新界面红点的委托

    public ReDotNode(int id)
    {
       this.mId = id;
       mValue = -1;
    }

红点树逻辑,处于性能考虑,可以采用延迟更新的方式,通过设置一个脏标记字段,在Update轮询是否有脏数据。

public class RedDotTree : MonoBehaviour
{
    ReDotNode mRoot;
    Dictionary<int, ReDotNode> mNodes = new Dictionary<int, ReDotNode>();
    bool isDirty = false;

    public ReDotNode FindNode(int id)
    {
       if (mNodes.TryGetValue(id, out var node))
       {
          return node;
       }

       return null;
    }

    public void SetDirty(int id)
    {
       var node = mNodes[id];
       node.IsDirty = true;
       isDirty = true;
    }

    public ReDotNode Register(int id, int parentId)
    {
       ReDotNode node = new ReDotNode(id);
       if (mNodes.ContainsKey(id))
       {
          Debug.LogError("RedDotTree Register Error: id is already exist!");
       }
       mNodes[id] = node;
       if (mRoot == null)
       {
          mRoot = node;
       }
       else
       {
          var cur = FindNode(parentId);
          cur.AddChild(node);
       }

       return node;
    }

    int UpdateNode(ReDotNode node)
    {
       int value;

       if (node.IsDirty)
       {
          value = node.mCheckFun?.Invoke() ?? 0;
       }
       else
       {
          value = node.Value;
       }

       if (node.mChild != null)
       {
          foreach (ReDotNode _node in node.mChild)
          {
             value += UpdateNode(_node);
          }
       }

       if (value != node.Value)
       {
          node.Value = value;
          node.mUpdateFun?.Invoke(value);
       }

       return value;
    }

    public float TimerSet = 0.2f;
    float timer = 0;

    public void Update(float deltaTime)
    {
       if (!isDirty)
       {
          timer = 0;
          return;
       }

       timer += deltaTime;
       if (timer < TimerSet)
          return;
       timer = 0;

       UpdateNode(mRoot);
       isDirty = false;
    }
}

这样就可以实现一个简单的红点树逻辑

使用上,有俩个步骤

  1. 确认UI上下级关系,构建树型结构,注册各个节点
  2. 在各个节点上绑定红点UI更新的逻辑

实例如下:

public class RedDotView : MonoBehaviour // UI组件,挂载在红点组件上,绑定对应的字段
{
    public CanvasGroup cg;
    public int id;
    public int parentId;
    public int value = 0;

    void Start()
    {
       ReDotNode node = RedDotTree.Instance.FindNode(id);
       node.mUpdateFun += (newValue) =>
       {
          if (newValue >= 1)
          {
             cg.alpha = 1;
          }
          else
          {
             cg.alpha = 0;
          }
       };
    }
}

仅测试示例,实际使用需要根据自己项目进行调整。

public class TestMgr : MonoBehaviour
{
    void Start()
    {
       var redDotView = GetComponentsInChildren<RedDotView>();
       foreach (var view in redDotView)
       {
          RedDotTree.Instance.Register(view.id, view.parentId);
       }
       foreach (var view in redDotView)
       {
          ReDotNode node = RedDotTree.Instance.FindNode(view.id);
          node.mCheckFun = () => view.value;

          RedDotTree.Instance.SetDirty(view.id);
      }
       RedDotTree.Instance.SetDirty(1);
    }
}
public class TestRedDot : MonoBehaviour
{
    RedDotView redDotView;
    Button button;
    public Text ValueText;
    public Text SumText;
    public bool isInit = false;

    void Awake()
    {
       redDotView = GetComponentInChildren<RedDotView>();
       button = GetComponent<Button>();
    }

    void Init()
    {
       redDotView = GetComponentInChildren<RedDotView>();
       button = GetComponent<Button>();
       button.onClick.AddListener(() => // 测试点击修改值
       {
          if (redDotView.value == 1)
          {
             redDotView.value = 0;
          }
          else
          {
             redDotView.value = 1;
          }
          RedDotTree.Instance.SetDirty(redDotView.id);
       });
       isInit = true;
    }

    void Start()
    {
       if (!isInit)
       {
          Init();
       }
    }

    void OnEnable()
    {
       if (!isInit)
       {
          return;
       }

       button.onClick.AddListener(() =>
       {
          if (redDotView.value == 1)
          {
             redDotView.value = 0;
          }
          else
          {
             redDotView.value = 1;
          }
          RedDotTree.Instance.SetDirty(redDotView.id);
       });
    }

    void OnDisable()
    {
       button.onClick.RemoveAllListeners();
    }

    void Update()
    {
       ReDotNode node = RedDotTree.Instance.FindNode(redDotView.id);
       ValueText.text = node.mCheckFun?.Invoke().ToString();
       SumText.text = node.Value.ToString();
    }
}