红点系统
游戏中的红点提示系统该怎么设计
大部分的红点系统是单独的一个板块,用来给其他系统直接调用
那么这个系统要有哪些方面的考虑呢?
1.红点需要单独做成一个预制体,今后如果要改变红点的表现形式,就很好更改了
2.系统要给他人提供的接口:1.在某个节点生成红点 2.要销毁某个节点的红点
3.红点可能有比较频繁的创建销毁,因此可以用对象池来复用,或者比较简单少量的红点,可以直接放预设上,直接控制隐藏显示。
因为很多红点在UI表现上都是层层深入的,所以红点系统需要形成节点树,当某个叶子节点需要显示红点,那么它上面所有的父节点都要对应生成红点。
这个节点数的特点是:子节点的数量是无限的,子节点之间没有顺序关系
因此这个树的结构特征符合的树形结构便是 前缀树,也就是字典树,Trie
https://www.bilibili.com/video/BV17R4y1Q7aK/?spm_id_from=333.1387.upload.video_card.click
https://www.bilibili.com/video/BV13G4y1h7uX/?spm_id_from=333.1387.upload.video_card.click&vd_source=436e1882d94925de09cd95044b65f90a
红点系统的运行流程:

https://zhuanlan.zhihu.com/p/453516850
这篇文章的红点系统的设计比较有实际意义
红点系统中最复杂的感觉是这个节点树的创建
因为很多UI只有运行才会加载,没法最开始得到整个节点树,也无法仅用配置方式去拿到所有节点树的数据,因为很多节点很可能是动态生成的。
虽然说是树,但实际上用的哈希表来存放的各个节点的结构,在每个节点的数据结构里面,找到它的父节点和子节点
ds给的回答,如何在动态加载UI节点的情况下创建节点树
UI节点树的构建在动态加载场景下确实面临独特挑战,因为很多UI元素(尤其是子节点)在游戏开始时并不存在,而是在运行时根据需要(如打开界面、触发事件)才被实例化。以下是针对动态加载UI的节点树构建方法,结合工程实践和不同场景需求:
一、核心构建策略(动态加载适配版)
1. 懒加载(按需创建)
- 原理: 节点只在首次被需要时创建(通过路径查找、状态设置或UI注册)。
- 实现方式:
- 路径驱动: 当调用
RedDotManager.SetState("App/Shop/NewSkinAvailable", true)时:- 按路径逐级查找节点。
- 遇到不存在的节点(如
Shop或NewSkinAvailable),即时创建并挂载到父节点下。 - 设置状态并触发回溯更新。
- UI注册驱动: 当动态加载的UI预制体初始化时,在其
Awake/Start中:- 通过配置、推导或参数获取自身路径(如
"App/Shop/SkinTab")。 - 调用
RedDotManager.RegisterNode(path)。 - 管理器检查路径节点是否存在,不存在则创建整条路径。
- 通过配置、推导或参数获取自身路径(如
- 路径驱动: 当调用
- 优点: 内存占用最小化,只创建必要的节点。
- 缺点: 首次操作可能有轻微开销(解析路径+创建节点)。
- 适用: 绝大多数动态UI,尤其是不确定是否会被访问的深层节点。
2. 预注册框架 + 动态填充
- 原理: 预先创建主干节点(框架),动态UI加载时只填充子叶节点。
- 实现方式:
- 游戏启动时: 通过配置表或代码创建核心框架节点(如
App,App/Home,App/Shop)。 - 动态UI加载时:
- 若该UI是叶子节点(如
Shop/SkinTab):- 确保父节点存在(如
Shop已预创建)。 - 创建自身节点并挂载到父节点下。
- 确保父节点存在(如
- 若该UI是父容器(如
Shop界面包含多个标签页):- 找到对应父节点(
App/Shop)。 - 在父节点下批量创建子节点(如
WeaponTab,SkinTab,SpecialOffers)。
- 找到对应父节点(
- 若该UI是叶子节点(如
- 游戏启动时: 通过配置表或代码创建核心框架节点(如
- 优点:
- 主干路径稳定,避免路径错误。
- 父节点提前存在,状态聚合逻辑更可靠。
- 缺点: 需预先定义框架,灵活性稍低。
- 适用: 结构稳定的系统(如主界面、商店),子项动态但父容器固定。
3. 数据驱动创建
- 原理: 根据数据模型动态生成UI和红点节点,常用于列表型UI(如邮件列表、任务列表)。
- 实现方式:
- 数据层: 每个数据项携带唯一ID和红点状态(如
MailItem: {id: 1001, isUnread: true})。 - UI层: 当数据项被渲染为UI元素(如
MailEntry_1001)时:- 动态生成节点路径: 使用模板路径 + 数据ID(如
"App/Mail/Inbox/Item_1001")。 - 创建节点:
RedDotManager.GetOrCreateNode(path)。 - 绑定状态: 将节点状态与数据同步(
node.SelfState = mailItem.isUnread)。
- 动态生成节点路径: 使用模板路径 + 数据ID(如
- 状态更新: 数据变更(如邮件设为已读)时,通过路径更新对应节点状态。
- 数据层: 每个数据项携带唯一ID和红点状态(如
- 优点: 红点节点与数据生命周期一致,自动随UI创建/销毁。
- 缺点: 路径需含唯一ID,管理大量节点时需注意性能。
- 适用: 动态列表/网格(邮件、任务、背包物品、好友请求)。
二、关键问题解决方案
Q1:如何为动态UI确定路径?
- 方案1:预制体配置
在动态加载的UI预制体上挂载脚本(如UIRedDotTarget),填写:NodeName:自身节点名(如"SkinTab")。ParentPath:父节点路径(如"App/Shop")→ 完整路径 =ParentPath + "/" + NodeName。
- 方案2:运行时推导
利用Unity层级关系(需确保UI层级 ≈ 红点逻辑层级):// 在UI脚本中 void Start() { string path = RedDotUtil.CalculatePath(transform); // 工具类沿Transform向上查找带RedDotNode的父对象,拼接路径 RedDotManager.RegisterNode(path); } - 方案3:通过参数传递
动态打开UI时传入路径:UIManager.OpenWindow("SkinShopUI", new { RedDotPath = "App/Shop/Skins" });
Q2:如何避免重复创建节点?
- 统一入口: 所有节点创建通过
RedDotManager.GetOrCreateNode(path)方法。 - 内部检查: 方法内使用 字典缓存(
Dictionary<string, RedDotNode>),存在则直接返回,不存在才创建。 - 路径标准化: 统一转为小写或大写,避免大小写重复。
Q3:动态UI卸载时如何清理节点?
- 策略1:引用计数
- UI注册时:
node.RefCount++。 - UI销毁时:
node.RefCount--。 - 当
RefCount == 0且无业务逻辑引用时,安全移除节点。
- UI注册时:
- 策略2:显式注销
UI在OnDestroy中调用RedDotManager.UnregisterNode(path)。 - 策略3:延迟清理
定时扫描长时间未被访问的叶子节点(通过LastAccessTime),但需谨慎避免误删。
三、高效构建框架示例(Hybrid + 懒加载)
// 红点节点定义
public class RedDotNode {
public string FullPath; // 完整路径
public string Name; // 节点名
public RedDotNode Parent; // 父节点
public List<RedDotNode> Children = new List<RedDotNode>();
public bool SelfState; // 自身状态(叶子节点)
public bool AggregateState; // 聚合状态
public int RefCount; // 引用计数(UI监听计数)
}
// 红点管理器核心
public class RedDotManager {
private Dictionary<string, RedDotNode> _nodeDict = new(); // 路径->节点
// 核心:按路径获取节点(不存在则创建)
public RedDotNode GetOrCreateNode(string fullPath) {
if (_nodeDict.TryGetValue(fullPath, out var node)) {
return node;
}
// 1. 创建新节点
node = new RedDotNode {
FullPath = fullPath,
Name = GetNameFromPath(fullPath),
AggregateState = false
};
_nodeDict[fullPath] = node;
// 2. 挂载到父节点(非根节点)
if (!IsRootPath(fullPath)) {
string parentPath = GetParentPath(fullPath);
RedDotNode parentNode = GetOrCreateNode(parentPath); // 递归创建父节点
parentNode.Children.Add(node);
node.Parent = parentNode;
}
return node;
}
// UI注册接口(动态UI在Start中调用)
public void RegisterUI(string path) {
RedDotNode node = GetOrCreateNode(path);
node.RefCount++;
// UI初始化时查询状态 + 监听变更
}
// 设置叶子节点状态
public void SetLeafState(string path, bool state) {
RedDotNode node = GetOrCreateNode(path);
if (!node.IsLeaf) return; // 安全检查
node.SelfState = state;
MarkDirty(node); // 标记脏数据,延迟批量更新聚合状态
}
// 批量更新聚合状态(LateUpdate中调用)
private void BatchUpdateDirtyNodes() { /* 见前文优化方案 */ }
}
// 动态UI组件(挂在预制体上)
public class DynamicUIRedDot : MonoBehaviour {
public string NodeName; // 配置自身节点名
void Start() {
// 推导父路径:根据UI层级或参数传递
string parentPath = GetParentPathFromHierarchy();
string fullPath = $"{parentPath}/{NodeName}";
// 注册并监听红点
RedDotManager.Instance.RegisterUI(fullPath);
}
void OnDestroy() {
// 反注册,减少引用计数
RedDotManager.Instance.UnregisterUI(fullPath);
}
}
四、各场景构建方案推荐
| 动态UI类型 | 推荐构建方法 | 关键点 |
|---|---|---|
| 弹窗/子界面 | 懒加载 + 预制体配置路径 | 父路径通过参数传递或预制体配置 |
| 列表项(邮件/任务) | 数据驱动创建 | 路径模板 + 数据ID (如 "Mail/Item_{id}") |
| 分页标签(商店页签) | 预注册框架 + 动态填充子节点 | 父节点预创建,子节点随UI初始化生成 |
| 全局浮动按钮 | 懒加载 + 参数传递 | 由打开者指定路径 (如 "App/Global/GiftBtn") |
| 活动模块(动态入口) | 数据驱动 + 懒加载 | 根据活动配置动态注册路径 |
总结:动态UI节点树构建原则
- 懒加载为主:节点按需创建,避免预加载未知路径。
- 路径标准化:通过预制体配置、运行时推导或参数传递确定路径。
- 生命周期绑定:UI与节点同生命周期(注册/注销 + 引用计数)。
- 数据结构优化:使用 字典 (O(1)查找) + 节点指针 加速访问。
- 状态更新批量化:脏标记 + 延迟更新避免卡顿。
动态加载不是障碍,反而是红点系统灵活性的体现。正确运用懒加载与数据绑定,即使面对最复杂的动态UI,也能构建出高效、稳定的红点节点树。
浙公网安备 33010602011771号