红点系统

游戏中的红点提示系统该怎么设计

大部分的红点系统是单独的一个板块,用来给其他系统直接调用
那么这个系统要有哪些方面的考虑呢?

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) 时:
      1. 按路径逐级查找节点。
      2. 遇到不存在的节点(如 ShopNewSkinAvailable),即时创建并挂载到父节点下。
      3. 设置状态并触发回溯更新。
    • UI注册驱动: 当动态加载的UI预制体初始化时,在其 Awake/Start 中:
      1. 通过配置、推导或参数获取自身路径(如 "App/Shop/SkinTab")。
      2. 调用 RedDotManager.RegisterNode(path)
      3. 管理器检查路径节点是否存在,不存在则创建整条路径
  • 优点: 内存占用最小化,只创建必要的节点。
  • 缺点: 首次操作可能有轻微开销(解析路径+创建节点)。
  • 适用: 绝大多数动态UI,尤其是不确定是否会被访问的深层节点。

2. 预注册框架 + 动态填充

  • 原理: 预先创建主干节点(框架),动态UI加载时只填充子叶节点
  • 实现方式:
    • 游戏启动时: 通过配置表或代码创建核心框架节点(如 App, App/Home, App/Shop)。
    • 动态UI加载时:
      • 若该UI是叶子节点(如 Shop/SkinTab):
        1. 确保父节点存在(如 Shop 已预创建)。
        2. 创建自身节点并挂载到父节点下。
      • 若该UI是父容器(如 Shop 界面包含多个标签页):
        1. 找到对应父节点(App/Shop)。
        2. 在父节点下批量创建子节点(如 WeaponTab, SkinTab, SpecialOffers)。
  • 优点:
    • 主干路径稳定,避免路径错误。
    • 父节点提前存在,状态聚合逻辑更可靠。
  • 缺点: 需预先定义框架,灵活性稍低。
  • 适用: 结构稳定的系统(如主界面、商店),子项动态但父容器固定。

3. 数据驱动创建

  • 原理: 根据数据模型动态生成UI和红点节点,常用于列表型UI(如邮件列表、任务列表)。
  • 实现方式:
    • 数据层: 每个数据项携带唯一ID和红点状态(如 MailItem: {id: 1001, isUnread: true})。
    • UI层: 当数据项被渲染为UI元素(如 MailEntry_1001)时:
      1. 动态生成节点路径: 使用模板路径 + 数据ID(如 "App/Mail/Inbox/Item_1001")。
      2. 创建节点: RedDotManager.GetOrCreateNode(path)
      3. 绑定状态: 将节点状态与数据同步(node.SelfState = mailItem.isUnread)。
    • 状态更新: 数据变更(如邮件设为已读)时,通过路径更新对应节点状态。
  • 优点: 红点节点与数据生命周期一致,自动随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 且无业务逻辑引用时,安全移除节点。
  • 策略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节点树构建原则

  1. 懒加载为主:节点按需创建,避免预加载未知路径。
  2. 路径标准化:通过预制体配置、运行时推导或参数传递确定路径。
  3. 生命周期绑定:UI与节点同生命周期(注册/注销 + 引用计数)。
  4. 数据结构优化:使用 字典 (O(1)查找) + 节点指针 加速访问。
  5. 状态更新批量化:脏标记 + 延迟更新避免卡顿。

动态加载不是障碍,反而是红点系统灵活性的体现。正确运用懒加载与数据绑定,即使面对最复杂的动态UI,也能构建出高效、稳定的红点节点树。

posted @ 2025-06-20 17:06  木土无心  阅读(51)  评论(0)    收藏  举报