红点框架系统设计

红点框架系统设计文档

本文是对红点系统核心代码梳理,抽出部分关键代码,仅供参考,介绍一种"事件驱动 + 脏节点队列(DirtyQueue) + 主动推送"的高性能红点系统实现。

核心文件:

  • RedPointLogicNode.cs
  • RedPointLogicMgr.cs
  • RedPointShowNode.cs
  • RedPointShowMgr.cs
  • RedPointSystemModule.cs

一、思维导图

红点系统整体分为 逻辑层表现层 两条主线,通过 事件 + 脏队列 解耦。

复制代码
                      ┌─────────────────────────────────────────────┐
                      │              红点框架系统                     │
                      └─────────────────────────────────────────────┘
                                          │
                ┌─────────────────────────┴─────────────────────────┐
                │                                                   │
        ┌───────▼────────┐                                  ┌───────▼────────┐
        │   逻辑层        │                                  │   表现层        │
        │ (Logic Layer)  │                                  │  (View Layer)  │
        └───────┬────────┘                                  └───────┬────────┘
                │                                                   │
   ┌────────────┼────────────┐                          ┌───────────┼───────────┐
   │            │            │                          │                       │
┌──▼──────┐ ┌───▼─────┐ ┌────▼──────┐                ┌──▼────────┐    ┌─────────▼────┐
│ Config  │ │ LogicNode│ │ LogicMgr │                │ ShowNode  │    │  ShowMgr     │
│ 配置数据 │ │ 逻辑节点 │ │ 树管理器 │                │ 表现节点  │    │  表现管理器  │
└─────────┘ └─────────┘ └────┬──────┘                └─────┬─────┘    └──────┬───────┘
                             │                             │                 │
                             │  OnNodeDirty (变脏入队)      │  OnStateChanged │
                             │  ─────────────►             │ ◄───────────── │
                             │     DirtyQueue              │   订阅回调       │
                             │                             │                 │
                             └─────── LateUpdate ──────────►─────────────────┘
                                       Flush 主动推送

关键思路:

  • 逻辑层 RedPointLogicMgr :进游戏时根据配置一次性构建红点树;运行期通过事件驱动逻辑节点变化;变化的节点入 DirtyQueue ,在 LateUpdate 帧末统一冲刷推送。
  • 表现层 RedPointShowMgr :只负责登记 / 注销 ShowNodeShowNode 通过订阅 LogicNode.OnStateChanged 事件实现 精准刷新(只刷自己)

二、配置设计

表格设计:

数据结构:

csharp 复制代码
public class RedPointConfig
{
    public int           RedID;                  // 红点ID(唯一)
    public RedShowType   RedShowType;            // 显示类型
    public RedLogicType  RedLogicType;           // 逻辑类型(Static/Dynamic)
    public List<int>     PreRedIDs;              // 直接前置红点(父节点ID列表)
    public bool          ShowTypeDependOnChild;  // 显示样式是否依赖子节点
}

核心字段说明:

字段 含义
RedID 红点的唯一标识,用于在管理器中查找节点
RedShowType 红点显示样式:Empty / Common / Number / New / Star / Up / Add
RedLogicType Static:进游戏构建静态树;Dynamic:运行期动态创建(用于服务器/任务红点)
PreRedIDs 前置红点 ID 列表,用于反向构建父子关系(一个节点可有多个父节点)
ShowTypeDependOnChild 父节点是否继承子节点的最高优先级;为 false 时父节点只关心"有/无",自身样式由配置决定

三、数据结构

逻辑层

RedPointLogicNode(红点逻辑节点)

RedPointLogicNode 是红点树上的最小逻辑单元,承载红点的状态、关系、以及向上冒泡逻辑。

字段定义
csharp 复制代码
public class RedPointLogicNode
{
    // 红点ID
    public int RedID { get; private set; }

    // 红点配置
    public RedPointConfig RedPointConfig { get; private set; }

    // 直接父节点(构建红点树时设置)
    public List<RedPointLogicNode> Parents  = new List<RedPointLogicNode>();
    // 直接子节点(构建红点树时设置)
    public List<RedPointLogicNode> Children = new List<RedPointLogicNode>();

    // 红点优先级(位掩码,受子节点影响)
    public int  CurRedPriority { get; private set; }
    // 标脏(仅用于内部去重,避免一次推送中重复入队)
    public bool IsDirty        { get; private set; }
    // 激活状态
    public bool IsActive       { get; private set; }

    // 节点状态变化事件 ------ 表现层订阅它接收"脏点通知",无需每帧轮询
    public event Action<RedPointLogicNode> OnStateChanged;

    // 当节点变脏时,由 Node 主动回调通知 LogicMgr 入队(DirtyQueue)
    public static Action<RedPointLogicNode> OnNodeDirty;

    private const int EmptyState = (1 << (int)RedShowType.Empty);
}

字段语义:

字段 说明
Parents / Children 父子关系列表,由 LogicMgr.InsertRedPoint 在构建期回填
CurRedPriority 位掩码 表示当前节点同时拥有的所有红点样式;取最高位即为最终展示样式(参见 GetCurrentHighestShowType)。例如子节点同时含 CommonUp,其值为 100010 (二进制) = 34,最高位为 Up
IsDirty 内部去重标记,避免同一节点在一个帧内被多次入队
IsActive 快捷判断:CurRedPriority > EmptyState
OnStateChanged 实例事件:本节点变化的精准回调(表现层订阅)
OnNodeDirty 静态委托 :由 LogicMgr 在初始化时绑定,用于"变脏即入队"
显示样式枚举
csharp 复制代码
public enum RedShowType
{
    Empty  = 0,
    Common = 1,
    Number = 2,
    New    = 3,
    Star   = 4,
    Up     = 5,
    Add    = 6,
}

最终展示时,从位掩码取出最高位作为最终样式:

csharp 复制代码
public RedShowType GetCurrentHighestShowType()
{
    RedShowType currentShowType = RedShowType.Empty;
    int currentShowTypeInt = CurRedPriority;
    while (currentShowTypeInt > EmptyState)
    {
        currentShowType++;
        currentShowTypeInt = currentShowTypeInt >> 1;
    }
    return currentShowType;
}
设置红点状态(直接 / 间接)

红点状态变化分为两类:直接设置 (业务事件触发)和 间接设置(子节点变化冒泡)。

1) 直接设置:业务事件触发

csharp 复制代码
// 复合型:在原有样式基础上叠加 showType 这一位
public void SetDirectActive(RedShowType showType, bool state)
{
    int res = EmptyState;
    if (showType != RedShowType.Empty)
    {
        res  = 1 << (int)showType;
        res |= CurRedPriority;
    }
    SetRedPriority(res);
}

// 复合型:直接传入位掩码
public void SetDirectActive(int showType)
{
    if (showType <= 0) return;
    SetRedPriority(showType);
}

// 单样式:根据配置中的 RedShowType 切换显隐
public void SetDirectActive(bool isActive)
{
    if (IsActive == isActive) return;
    int res = EmptyState;
    if (isActive)
    {
        res = GetConfigRedShowType();   // 1 << (int)RedPointConfig.RedShowType
    }
    SetRedPriority(res);
}

2) 间接设置:子节点变化驱动父节点重算

csharp 复制代码
public void SetPassiveActive()
{
    int childRed = EmptyState;
    for (int i = 0; i < Children.Count; i++)
    {
        childRed |= Children[i].CurRedPriority;
    }

    if (RedPointConfig.ShowTypeDependOnChild)
    {
        // 父节点完全继承子节点位掩码
        if (childRed != CurRedPriority) SetRedPriority(childRed);
    }
    else
    {
        // 父节点只关心"有没有",样式由自己配置决定
        if (childRed != EmptyState && CurRedPriority == EmptyState)
            SetDirectActive(true);
        else if (childRed == EmptyState && CurRedPriority != EmptyState)
            SetDirectActive(false);
    }
}
核心:脏标 + 入队 + 向上冒泡

SetRedPriority 是所有状态写入的最终路径,包含 去重 / 入队 / 冒泡 三步:

csharp 复制代码
private void SetRedPriority(int priority, bool refreshParents = true)
{
    // 值未变化则什么也不做(精准刷新的关键)
    if (CurRedPriority == priority) return;

    CurRedPriority = priority;
    IsActive       = CurRedPriority > EmptyState;

    // 标脏 + 入队(去重)
    if (!IsDirty)
    {
        IsDirty = true;
        if (OnNodeDirty != null) OnNodeDirty(this);
    }

    // 向上冒泡:父节点重算(递归冒泡,不再全量遍历整棵树)
    if (refreshParents)
    {
        for (int i = 0; i < Parents.Count; i++)
        {
            Parents[i].SetPassiveActive();
        }
    }
}
主动通知表现层

LogicMgr 在帧末冲刷 DirtyQueue 时调用:

csharp 复制代码
public void NotifyStateChanged()
{
    if (OnStateChanged != null) OnStateChanged(this);
    ResumeDirty();           // IsDirty = false
}
RedPointLogicMgr(红点树逻辑管理器)

RedPointLogicMgr 负责:构建逻辑树 → 管理 DirtyQueue → 帧末主动推送

字段与单例
csharp 复制代码
public class RedPointLogicMgr : MonoBehaviour
{
    // 配置层红点
    private Dictionary<int, RedPointConfig>    redPointConfigMap;
    // 逻辑层红点关系树
    private Dictionary<int, RedPointLogicNode> redPointLogicMap;

    // 脏节点队列(DirtyQueue):HashSet 去重 + List 稳定枚举
    private HashSet<RedPointLogicNode> dirtySet;
    private List<RedPointLogicNode>    dirtyQueue;

    public static RedPointLogicMgr Instance { get; }
}
初始化:绑定脏点回调 + 构建树
csharp 复制代码
private void Init()
{
    redPointLogicMap = new Dictionary<int, RedPointLogicNode>();
    dirtySet         = new HashSet<RedPointLogicNode>();
    dirtyQueue       = new List<RedPointLogicNode>();

    // 把 Node 的"变脏"事件接到本 Mgr 的 DirtyQueue 入队逻辑上
    RedPointLogicNode.OnNodeDirty = EnqueueDirty;

    // 加载配置 ...
    foreach (var item in redPointConfigMap)
    {
        InsertRedPoint(item.Key);
    }

    // 初始构建过程中产生的"脏"先清空(此时还没有 ShowNode 监听)
    dirtySet.Clear();
    dirtyQueue.Clear();
}
构建逻辑树:递归插入 + 关系回填
csharp 复制代码
private RedPointLogicNode InsertRedPoint(int redID)
{
    if (redPointLogicMap.ContainsKey(redID)) return null;

    RedPointConfig    config    = GetRedPointConfig(redID);
    RedPointLogicNode logicNode = new RedPointLogicNode(config);
    redPointLogicMap.Add(redID, logicNode);

    // 递归确保前置节点先存在,再回填父子关系
    List<int> preRedIDs = config.PreRedIDs;
    for (int i = 0; i < preRedIDs.Count; i++)
    {
        RedPointLogicNode parent = InsertRedPoint(preRedIDs[i]);
        if (parent == null) parent = redPointLogicMap[preRedIDs[i]];

        parent.Children.Add(logicNode);
        logicNode.Parents.Add(parent);
    }
    return logicNode;
}
DirtyQueue:帧末主动推送
csharp 复制代码
private void LateUpdate()
{
    FlushDirtyQueue();
}

private void EnqueueDirty(RedPointLogicNode node)
{
    if (node == null) return;
    if (dirtySet.Add(node))   // HashSet 去重
    {
        dirtyQueue.Add(node);
    }
}

public void FlushDirtyQueue()
{
    if (dirtyQueue.Count == 0) return;

    // 处理过程中可能再次产生脏节点(嵌套通知),用 for 索引遍历更安全
    for (int i = 0; i < dirtyQueue.Count; i++)
    {
        RedPointLogicNode node = dirtyQueue[i];
        if (node == null) continue;
        node.NotifyStateChanged();
    }
    dirtyQueue.Clear();
    dirtySet.Clear();
}
对外 API
csharp 复制代码
public void SetRedPointState(int redID, bool isActive);                 // 单样式
public void SetRedPointState(int redID, RedShowType showType, bool s);  // 复合叠加
public void SetRedPointState(int redID, int showType);                  // 复合掩码
public RedPointLogicNode GetLogicNodeByRedID(int redID);

表现层

RedPointShowNode(红点表现节点)

挂载在 UI GameObject 上,通过订阅 LogicNode.OnStateChanged 实现"按需刷新"

csharp 复制代码
public class RedPointShowNode : MonoBehaviour
{
    public  int RedID;
    private RedPointLogicNode LogicNode;
    private Dictionary<RedShowType, GameObject> RedPointShowTemplete;
}

字段说明:

字段 说明
RedID 在 Inspector 中配置的关联红点 ID
LogicNode 通过 BindLogicNode 绑定的逻辑节点
RedPointShowTemplete RedShowType → GameObject 的样式模板字典;通过子物体顺序自动收集
自动收集样式模板
csharp 复制代码
private void Awake()
{
    RedPointShowTemplete = new Dictionary<RedShowType, GameObject>();
    for (int i = (int)RedShowType.Common; i <= (int)RedShowType.Add; i++)
    {
        RedPointShowTemplete.Add((RedShowType)i, transform.GetChild(i - 1).gameObject);
    }
}

约定:节点下子物体顺序与 RedShowType 枚举顺序一致。

绑定逻辑节点 ⇒ 订阅事件
csharp 复制代码
public void BindLogicNode(RedPointLogicNode node)
{
    if (LogicNode != null) LogicNode.OnStateChanged -= OnLogicNodeStateChanged;

    LogicNode = node;
    if (LogicNode != null)
    {
        // 监听回调:逻辑层有变化时主动通知本 ViewNode(精准刷新)
        LogicNode.OnStateChanged += OnLogicNodeStateChanged;
        // 绑定时同步一次当前状态,保证初始显示正确
        ApplyState();
    }
}
精准刷新:只刷自己
csharp 复制代码
private void OnLogicNodeStateChanged(RedPointLogicNode node) => ApplyState();

private void ApplyState()
{
    if (LogicNode == null) return;

    if (LogicNode.IsActive)
    {
        RedShowType showType = LogicNode.GetCurrentHighestShowType();
        foreach (var item in RedPointShowTemplete)
        {
            item.Value.SetActive(item.Key == showType);
        }
        gameObject.SetActive(true);
    }
    else
    {
        foreach (var item in RedPointShowTemplete)
        {
            item.Value.SetActive(false);
        }
        gameObject.SetActive(false);
    }
}
释放:解订阅,避免悬挂引用
csharp 复制代码
public void Release()
{
    if (LogicNode != null) LogicNode.OnStateChanged -= OnLogicNodeStateChanged;
    LogicNode = null;
}

private void OnDestroy() => Release();
RedPointShowMgr(红点树表现管理器)

ShowMgr 在新方案中只承担 登记 / 注销 职责。

csharp 复制代码
public class RedPointShowMgr : MonoBehaviour
{
    // 表现层红点登记表(仅作登记/查询用,不再用于每帧轮询)
    private Dictionary<int, RedPointShowNode> redPointShowMap;

    public static RedPointShowMgr Instance { get; }

    private void Init()
    {
        redPointShowMap = new Dictionary<int, RedPointShowNode>();
        // 拉起所有挂载的业务模块
        IRedPointSystemModule[] allModules = GetComponentsInChildren<IRedPointSystemModule>();
        for (int i = 0; i < allModules.Length; i++) allModules[i].OnSystemInit();
    }

    public void AddRedPoint(RedPointShowNode node)
    {
        if (node == null) return;
        if (redPointShowMap.ContainsKey(node.RedID)) return;
        redPointShowMap.Add(node.RedID, node);
    }

    public void RemoveRedPoint(RedPointShowNode node)
    {
        if (node == null) return;
        if (!redPointShowMap.ContainsKey(node.RedID)) return;
        redPointShowMap.Remove(node.RedID);
    }
}
业务模块统一接入:RedPointSystemModule

为了让红点逻辑与业务解耦,每个业务模块挂载 RedPointSystemModule,由它负责在 UI 启用/禁用时批量绑定/释放本模块下的所有 ShowNode

csharp 复制代码
public interface IRedPointSystemModule
{
    void OnSystemInit();    // 注册逻辑层事件(业务触发)
    void OnSystemDeInit();  // 反注册逻辑层事件
    void OnViewInit();      // 注册表现层红点
    void OnViewDeInit();    // 释放表现层红点
}

public class RedPointSystemModule : MonoBehaviour, IRedPointSystemModule
{
    public void OnEnable()  => OnViewInit();
    public void OnDisable() => OnViewDeInit();

    public virtual void OnViewInit()
    {
        var showNodes = transform.GetComponentsInChildren<RedPointShowNode>(true);
        var logicMgr  = RedPointLogicMgr.Instance;
        var showMgr   = RedPointShowMgr.Instance;
        for (int i = 0; i < showNodes.Length; i++)
        {
            showNodes[i].BindLogicNode(logicMgr.GetLogicNodeByRedID(showNodes[i].RedID));
            showMgr.AddRedPoint(showNodes[i]);
        }
    }

    public virtual void OnViewDeInit()
    {
        var showNodes = transform.GetComponentsInChildren<RedPointShowNode>(true);
        var showMgr   = RedPointShowMgr.Instance;
        for (int i = 0; i < showNodes.Length; i++)
        {
            showNodes[i].Release();
            showMgr.RemoveRedPoint(showNodes[i]);
        }
    }
}

附录:完整数据流时序

复制代码
[业务事件] ──► RedPointLogicMgr.SetRedPointState(redID, ...)
                           │
                           ▼
              RedPointLogicNode.SetDirectActive(...)
                           │
                           ▼
                 SetRedPriority(priority)
                  │           │
                  │           └─► 父节点 SetPassiveActive() ──► 递归冒泡
                  │
                  ▼
        if (!IsDirty) IsDirty = true; OnNodeDirty(this);
                           │
                           ▼
              RedPointLogicMgr.EnqueueDirty(node)
                  HashSet 去重,加入 dirtyQueue
                           │
                           ▼
        ─── 等到本帧 LateUpdate ───
                           │
                           ▼
              RedPointLogicMgr.FlushDirtyQueue()
                           │
                           ▼
            node.NotifyStateChanged()  ──► OnStateChanged 事件
                           │
                           ▼
            RedPointShowNode.OnLogicNodeStateChanged
                           │
                           ▼
                    ApplyState() ------ 仅刷新自己

关键性能优化点小结:

  1. 状态值未变 ⇒ 立即返回SetRedPriorityif (CurRedPriority == priority) return;
  2. 同帧重复变脏 ⇒ 去重IsDirty 标记 + HashSet 双重保障
  3. 冒泡限定父链SetPassiveActive 只在父节点链上递归,避免遍历整树
  4. 帧末统一推送LateUpdate 中 Flush,避免一帧内多次抖动刷新
相关推荐
步步为营DotNet2 小时前
借助 C# 14 特性强化 .NET 后端数据验证的深度实践
java·c#·.net
影寂ldy3 小时前
C# 泛型委托
java·算法·c#
z落落3 小时前
Timer与DateTimePicker:控件使用全解析
开发语言·c#
2601_961845154 小时前
2026法考资料pdf|电子版|资料已整理
开发语言·前端框架·pdf·c#·xhtml·csrf·view design
@insist1234 小时前
系统架构设计师-嵌入式系统核心概念与关键机制
架构·系统架构·软考·系统架构设计师·软件水平考试
GISer_Jing6 小时前
常见CI/CD选型与实现
系统架构
ceclar1236 小时前
C#字节流与字符流
算法·c#·.net
z落落16 小时前
C#WinForm 窗体切换与窗体传值(登录跳转案例)+WinForm 窗体传值(从上往下传、从下往上传)
开发语言·windows·c#