红点框架系统设计文档
本文是对红点系统核心代码梳理,抽出部分关键代码,仅供参考,介绍一种"事件驱动 + 脏节点队列(DirtyQueue) + 主动推送"的高性能红点系统实现。
核心文件:
RedPointLogicNode.csRedPointLogicMgr.csRedPointShowNode.csRedPointShowMgr.csRedPointSystemModule.cs
一、思维导图
红点系统整体分为 逻辑层 与 表现层 两条主线,通过 事件 + 脏队列 解耦。
┌─────────────────────────────────────────────┐
│ 红点框架系统 │
└─────────────────────────────────────────────┘
│
┌─────────────────────────┴─────────────────────────┐
│ │
┌───────▼────────┐ ┌───────▼────────┐
│ 逻辑层 │ │ 表现层 │
│ (Logic Layer) │ │ (View Layer) │
└───────┬────────┘ └───────┬────────┘
│ │
┌────────────┼────────────┐ ┌───────────┼───────────┐
│ │ │ │ │
┌──▼──────┐ ┌───▼─────┐ ┌────▼──────┐ ┌──▼────────┐ ┌─────────▼────┐
│ Config │ │ LogicNode│ │ LogicMgr │ │ ShowNode │ │ ShowMgr │
│ 配置数据 │ │ 逻辑节点 │ │ 树管理器 │ │ 表现节点 │ │ 表现管理器 │
└─────────┘ └─────────┘ └────┬──────┘ └─────┬─────┘ └──────┬───────┘
│ │ │
│ OnNodeDirty (变脏入队) │ OnStateChanged │
│ ─────────────► │ ◄───────────── │
│ DirtyQueue │ 订阅回调 │
│ │ │
└─────── LateUpdate ──────────►─────────────────┘
Flush 主动推送
关键思路:
- 逻辑层
RedPointLogicMgr:进游戏时根据配置一次性构建红点树;运行期通过事件驱动逻辑节点变化;变化的节点入 DirtyQueue ,在LateUpdate帧末统一冲刷推送。 - 表现层
RedPointShowMgr:只负责登记 / 注销ShowNode,ShowNode通过订阅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)。例如子节点同时含 Common 与 Up,其值为 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() ------ 仅刷新自己
关键性能优化点小结:
- 状态值未变 ⇒ 立即返回 :
SetRedPriority中if (CurRedPriority == priority) return; - 同帧重复变脏 ⇒ 去重 :
IsDirty标记 +HashSet双重保障 - 冒泡限定父链 :
SetPassiveActive只在父节点链上递归,避免遍历整树 - 帧末统一推送 :
LateUpdate中 Flush,避免一帧内多次抖动刷新