Unity 适用于单机游戏的红点系统(前缀树 | 数据结构 | 设计模式 | 算法 | 含源码)
文章目录
- 功能包括
- 如何使用
功能包括
-
红点数据本地持久化
-
如果子节点有红点,父节点也要显示红点,父节点红点数为子节点红点数的和;
-
当子节点红点更新时,对应的父节点也要更新;
-
当所有子节点都没有红点时,父节点才不显示红点、
-
红点的显示方式分三种:
1.带数字的,每次经过要减1
2.不带数字只显示红点的
3.不带数字但是红点上显示感叹号的
如何使用
把这三个脚本复制到项目中
你没有这个类CryptoPrefs用PlayerPrefs代替即可
RedPointTree
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using Newtonsoft.Json; // 引入Json.NET库进行序列化和反序列化
/// <summary>
/// 节点名
/// </summary>
public enum ENodeNames
{
Shop,
Map,
User,
SongBtn,
SongBtn_Event,
VipBtn,
}
public class RedPointDataDTO
{
public string Name { get; set; }
public int PassCnt { get; set; }
public int EnCnt { get; set; }
public int RedPointCnt { get; set; }
// 如果有必要,也可以添加子节点数据,但需确保不包含Unity特有的类型
public List<RedPointDataDTO> Children { get; set; } = new List<RedPointDataDTO>();
}
public class RedPointTree : MonoSingleton<RedPointTree>
{
/// <summary>
/// 字显示为!
/// </summary>
public int MaxNum = 999;
/// <summary>
/// 字不显示
/// </summary>
public int NullNum = 998;
private RedPointNode root;
private const string RED_POINT_PREFS_KEY = "RedPointData";
/// <summary>
/// 保存红点数据到CryptoPrefs
/// </summary>
public void SaveRedPoints()
{
// 将红点树转换为可序列化的字符串
var dto = ConvertToDto(this.root);
string jsonData = JsonConvert.SerializeObject(dto, Formatting.None);
CryptoPrefs.SetString(RED_POINT_PREFS_KEY, jsonData);
CryptoPrefs.Save();
}
private RedPointDataDTO ConvertToDto(RedPointNode node)
{
var dto = new RedPointDataDTO
{
Name = node.name,
PassCnt = node.passCnt,
EnCnt = node.enCnt,
RedPointCnt = node.redpoinCnt
};
foreach (var child in node.children.Values)
{
dto.Children.Add(ConvertToDto(child));
}
return dto;
}
/// <summary>
/// 从CryptoPrefs加载红点数据
/// </summary>
public void LoadRedPoints()
{
if (CryptoPrefs.HasKey(RED_POINT_PREFS_KEY))
{
// 从CryptoPrefs加载并反序列化红点数据
string jsonData = CryptoPrefs.GetString(RED_POINT_PREFS_KEY);
var dto = JsonConvert.DeserializeObject<RedPointDataDTO>(jsonData);
// 清空当前树结构,避免数据叠加
this.root.children.Clear();
this.root = ConvertFromDto(dto);
}
}
private RedPointNode ConvertFromDto(RedPointDataDTO dto)
{
var node = new RedPointNode(dto.Name);
node.passCnt = dto.PassCnt;
node.enCnt = dto.EnCnt;
node.redpoinCnt = dto.RedPointCnt;
foreach (var childDto in dto.Children)
{
var childNode = ConvertFromDto(childDto);
node.children[childDto.Name] = childNode;
}
return node;
}
public RedPointTree()
{
root=new RedPointNode("Root");
}
/// <summary>
/// 初始化
/// </summary>
public new void Init()
{
LoadRedPoints(); // 先尝试加载已有的红点数据
if (this.root == null)
{
//创建根节点
this.root = new RedPointNode("Root");
}
// 构建前缀树
foreach (var name in Enum.GetNames(typeof(ENodeNames)))
{
this.InsterNode(name);
}
//测试塞入红点数据
// ChangeRedPointCnt(ENodeNames.SongBtn.ToString(), 20);
// ChangeRedPointCnt(ENodeNames.SongBtn_Event.ToString(), 1);
// ChangeRedPointCnt(ENodeNames.User.ToString(), 999);
// ChangeRedPointCnt(ENodeNames.Card.ToString(), 1);
// ChangeRedPointCnt(ENodeNames.Shop.ToString(), 1);
}
/// <summary>
/// 插入节点
/// </summary>
/// <param name="name"></param>
public void InsterNode(string name)
{
if (string.IsNullOrEmpty(name))
{
return;
}
if (SearchNode(name) != null)
{
//如果已经存在 则不重复插入
GameLog.Log("你已经插入了该节点" + name);
return;
}
//node从根节点出发
RedPointNode node = root;
node.passCnt += 1;
//将名字按|符合分割
string[] pathList = name.Split('_');
foreach (var path in pathList)
{
if(!node.children.ContainsKey(path))
{
node.children.Add(path, RedPointNode.New(path));
}
node = node.children[path];
node.passCnt = node.passCnt+1;
}
node.enCnt = node.enCnt + 1;
}
/// <summary>
/// 查询节点是否在树中并返回节点
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public RedPointNode SearchNode(string name)
{
if (string.IsNullOrEmpty(name))
{
return null;
}
RedPointNode node=this.root;
string[] pathList=name.Split('_');
foreach (var path in pathList)
{
if(!node.children.ContainsKey(path))
{
return null;
}
node = node.children[path];
}
if (node.enCnt > 0)
{
return node;
}
return null;
}
/// <summary>
/// 删除节点
/// </summary>
/// <param name="name"></param>
public void DeleteNode(string name)
{
if (SearchNode(name) == null)
{
return;
}
RedPointNode node= this.root;
node.passCnt = node.passCnt - 1;
string[] pathList = name.Split('_');
foreach (var path in pathList)
{
RedPointNode childNode = node.children[path];
childNode.passCnt = childNode.passCnt - 1;
if (childNode.passCnt == 0)
{
//如果该节点没有任何孩子,则直接删除
node.children.Remove(path);
return;
}
node = childNode;
}
node.enCnt=node.enCnt - 1;
}
/// <summary>
/// 修改节点的和点数
/// </summary>
/// <param name="name">红点名字</param>
/// <param name="delta">增量</param>
public void ChangeRedPointCnt(string name, int delta)
{
RedPointNode targetNode = SearchNode(name);
if (targetNode == null)
{
return;
}
//如果是减红点 并且和点数不够减了 则调整delta 使其不减为0
if (delta < 0 && targetNode.redpoinCnt + delta < 0)
{
delta = -targetNode.redpoinCnt;
}
RedPointNode node=this.root;
string[] pathList= name.Split('_');
foreach (var path in pathList)
{
RedPointNode childNode = node.children[path];
childNode.redpoinCnt = childNode.redpoinCnt + delta;
node = childNode;
//调用回调函数
foreach (var cb in node.updateCb.Values)
{
cb?.Invoke(node.redpoinCnt);
}
}
// 在更新红点计数后保存数据
SaveRedPoints();
}
/// <summary>
/// 直接设置当前红点的数值
/// </summary>
/// <param name="name"></param>
/// <param name="delta"></param>
public void SetRedPointCnt(string name, int delta)
{
RedPointNode targetNode = SearchNode(name);
if (targetNode == null)
{
return;
}
RedPointNode node=this.root;
string[] pathList= name.Split('_');
foreach (var path in pathList)
{
RedPointNode childNode = node.children[path];
childNode.redpoinCnt = delta;
node = childNode;
//调用回调函数
foreach (var cb in node.updateCb.Values)
{
cb?.Invoke(node.redpoinCnt);
}
}
// 在更新红点计数后保存数据
SaveRedPoints();
}
/// <summary>
/// 设置红点更新回调函数
/// </summary>
/// <param name="name">节点名</param>
/// <param name="key">回调key 自定义字符串</param>
/// <param name="cb">回调函数</param>
public void SetCallBack(string name, string key, Action<int> cb)
{
RedPointNode node=SearchNode(name);
if (node == null)
{
return;
}
if (!node.updateCb.ContainsKey(key))
{
node.updateCb.Add(key, cb);
}
else
{
node.updateCb[key] = cb;
}
}
/// <summary>
/// 查询节点红点数
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public int GetRedPointCnt(string name)
{
RedPointNode node=SearchNode(name);
if (node == null)
{
return 0;
}
return node.redpoinCnt;
}
}
RedPointNode
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RedPointNode
{
/// <summary>
/// 节点名
/// </summary>
public string name;
/// <summary>
/// 节点被经过的次数
/// </summary>
public int passCnt = 0;
/// <summary>
/// 节点作为尾结点的次数
/// </summary>
public int enCnt = 0;
/// <summary>
/// 红点数
/// </summary>
public int redpoinCnt = 0;
public Dictionary<string, RedPointNode> children ;
public Dictionary<string, Action<int>> updateCb ;
public RedPointNode(string name)
{
this.name = name;
this.passCnt = 0;
this.enCnt = 0;
this.redpoinCnt = 0;
this.children = new Dictionary<string, RedPointNode>();
this.updateCb = new Dictionary<string, Action<int>>();
}
public static RedPointNode New(string name)
{
return new RedPointNode(name);
}
}
RedPointMono
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RedPointMono : MonoBehaviour
{
public ENodeNames NodeName;
public Text RedPointText;
private void Awake()
{
RedPointTree.Instance.SetCallBack(NodeName.ToString()
, this.gameObject.name, (redpointCnt) => { UpdateRedPoint(redpointCnt); });
}
void OnEnable()
{
//RedPointText = transform.GetChild(0).transform.GetComponent<Text>();
UpdateRedPoint(RedPointTree.Instance.GetRedPointCnt(NodeName.ToString()));
}
private void OnDestroy()
{
//注销红点回调
RedPointTree.Instance.SetCallBack(NodeName.ToString()
, this.gameObject.name, null);
}
//更新红点
private void UpdateRedPoint(int redpointCnt)
{
//throw new NotImplementedException();
if (redpointCnt>=RedPointTree.Instance.MaxNum)
{
RedPointText.text = "!";
}
else if (redpointCnt==RedPointTree.Instance.NullNum)
{
RedPointText.text = "";
}
else
{
RedPointText.text = redpointCnt.ToString();
}
gameObject.SetActive(redpointCnt > 0);
}
}
然后红点结构是这样的
因为是基于前缀树的,父子节点关系在这里体现
SongBtn,//父节点
SongBtn_Event,//子节点
这样当SongBtn_Event有红点的时候SongBtn也会有
参考链接:https://blog.csdn.net/linxinfa/article/details/121899276