Unity MyFramework 用法说明(二十六):使用 RedPointSystem 构建树形红点逻辑

邮件、私聊、成就和活动入口都可能显示红点。真正复杂的地方不是让一个节点显示出来,而是如何让子功能变化后,自动刷新上层入口,并让同一份红点状态同步到多个界面。

MyFramework 使用 RedPointSystem 管理红点数据、UI 绑定、事件刷新和父子节点传播。

项目地址:

github.com/ZHOURUIH/My...

一、红点应该是独立的数据对象

最直接的写法,是在界面打开时查询数据:

bash 复制代码
mMailRedPoint.setActive(
	mMailManager.hasUnreadMail());

这种方式存在几个问题:

bash 复制代码
只有界面打开时才会刷新
多个界面需要重复查询
下层数据变化后,上层入口不会自动更新
红点逻辑逐渐散落到各个UI中

MyFramework 将红点状态独立为 RedPoint

bash 复制代码
业务数据
    ↓
RedPoint 计算是否显示
    ↓
绑定一个或多个 UI 节点
    ↓
RedPointSystem 统一刷新

UI 只负责展示,不负责判断业务条件。

二、使用树形结构组织红点

假设主界面中包含邮件、私聊和成就:

bash 复制代码
主菜单红点
├── 邮件入口红点
│   ├── 邮件1
│   ├── 邮件2
│   └── 邮件3
├── 私聊入口红点
│   ├── 玩家A
│   └── 玩家B
└── 成就入口红点
    ├── 战斗成就
    └── 收集成就

创建根节点和分类节点:

bash 复制代码
using static FrameBaseHotFix;

public class RedPointManager : FrameSystem
{
	public static RedPoint mMain;
	public static RedPoint mMainMail;
	public static RedPoint mMainPrivateChat;
	public static RedPoint mMainAchievement;

	public override void init()
	{
		base.init();

		mRedPointSystem.createRedPoint(
			out mMain);

		mRedPointSystem.createRedPoint(
			out mMainMail,
			mMain);

		mRedPointSystem.createRedPoint(
			out mMainPrivateChat,
			mMain);

		mRedPointSystem.createRedPoint(
			out mMainAchievement,
			mMain);
	}
}

这些中间节点直接使用基础的 RedPoint

基础节点不判断具体业务,只根据子节点决定自己是否显示:

bash 复制代码
public virtual void refresh()
{
	bool enable = false;

	foreach (RedPoint point in mChildren)
	{
		if (point.isEnable())
		{
			enable = true;
			break;
		}
	}

	setEnable(enable);
}

只要任意子节点开启,父节点就会开启。

三、只有叶节点负责业务判断

以单封邮件的红点为例:

bash 复制代码
public class RedPointMail : RedPoint
{
	protected Mail mMail;

	protected override void initEventType()
	{
		base.initEventType();

		addEvent<EventUnreadMailChanged>();
	}

	public override void refresh()
	{
		setEnable(!mMail.isRead());
	}

	public override void resetProperty()
	{
		base.resetProperty();

		mMail = null;
	}

	public void setMail(Mail mail)
	{
		mMail = mail;
	}
}

叶节点负责两件事:

bash 复制代码
监听哪些事件
根据什么数据判断是否显示

收到 EventUnreadMailChanged 后,红点不会立刻重复查询数据,而是先标记:

bash 复制代码
protected void onEventTrigger()
{
	if (mChildren.Count > 0)
	{
		return;
	}

	mIsDirty = true;
}

RedPointSystem.update() 每帧统一刷新脏节点:

bash 复制代码
public override void update(float elapsedTime)
{
	base.update(elapsedTime);

	foreach (RedPoint point in mRedPointList)
	{
		if (!point.isDirty())
		{
			continue;
		}

		point.setDirty(false);
		point.refresh();
		notifyRedPointChanged(point);
	}
}

即使同一帧连续收到多次事件,同一个叶节点也只会刷新一次。

四、叶节点变化后如何更新父节点

叶节点刷新完成后,会向父节点递归传播:

bash 复制代码
protected void onRedPointChanged(
	RedPoint node)
{
	RedPoint parent = node.getParent();
	if (parent == null)
	{
		return;
	}

	parent.refresh();
	onRedPointChanged(parent);
}

例如一封邮件从未读变成已读:

bash 复制代码
RedPointMail 关闭
    ↓
刷新邮件分类节点
    ↓
检查是否还有其他未读邮件
    ↓
刷新主菜单节点

业务代码不需要分别刷新邮件入口和主菜单入口。

这也是树形红点结构最主要的价值。

五、为业务数据创建叶节点

每一封邮件创建时,同时创建对应红点:

bash 复制代码
using static FrameBaseHotFix;
using static RedPointManager;

public class Mail : ClassObject
{
	protected RedPointMail mRedPoint;
	protected bool mHasRead;

	public void init()
	{
		mRedPointSystem
			.createRedPoint(
				out mRedPoint,
				mMainMail)
			.setMail(this);
	}

	public override void destroy()
	{
		base.destroy();

		mRedPointSystem?.destroyRedPoint(
			mRedPoint);

		mRedPoint = null;
	}

	public override void resetProperty()
	{
		base.resetProperty();

		mRedPoint = null;
		mHasRead = false;
	}

	public bool isRead()
	{
		return mHasRead;
	}

	public RedPointMail getRedPoint()
	{
		return mRedPoint;
	}
}

红点应该尽早创建。

如果先收到未读邮件变化事件,之后才创建红点,那么这次事件就无法触发它刷新。

动态业务对象销毁时,也必须销毁自己的红点:

bash 复制代码
mRedPointSystem.destroyRedPoint(
	mRedPoint);

销毁叶节点后,框架会自动刷新它原来的父节点。

六、将红点绑定到 UI

邮件列表项创建后,只需要绑定显示节点:

bash 复制代码
public void setMail(Mail mail)
{
	mMail = mail;

	mMail.getRedPoint().bindPointUI(
		mUnreadMark);
}

绑定时会立即同步当前状态:

bash 复制代码
public void bindPointUI(
	myUGUIObject point,
	bool showError = true)
{
	mPointUIMap.TryAdd(
		point,
		getGameObjectPath(
			point.getGameObject()));

	point.setActive(mEnable);
}

所以无论 UI 在红点刷新前还是刷新后创建,都能获得正确的显示状态。

同一个红点可以同时绑定多个 UI:

bash 复制代码
RedPointManager.mMainMail.bindPointUI(
	mMainMailRedPoint);

RedPointManager.mMainMail.bindPointUI(
	mMenuMailRedPoint);

邮件状态变化后,两个位置会同时刷新。

七、UI 销毁或回收时必须解除绑定

可回收列表项离开界面时,需要解除绑定:

bash 复制代码
public override void recycle()
{
	if (mMail.isValid())
	{
		mMail.getRedPoint()?.removePointUI(
			mUnreadMark);
	}

	mMail = null;

	base.recycle();
}

普通界面可以在销毁时处理:

bash 复制代码
public override void destroy()
{
	RedPointManager.mMainMail.removePointUI(
		mMailRedPoint);

	base.destroy();
}

如果 UI 已经销毁,但仍保留在红点中,下一次刷新时会访问无效对象。

框架会输出提示:

bash 复制代码
红点的UI节点已经被销毁了,
但是仍然没有被移除绑定

因此红点使用需要关注四个生命周期节点:

bash 复制代码
创建红点
绑定 UI
解除 UI 绑定
销毁红点

八、显示未读数量

私聊入口除了显示红点,还需要显示未读数量,可以继承 RedPointCount

bash 复制代码
public class RedPointPrivateChat : RedPointCount
{
	protected PrivateChatInfo mChat;

	protected override void initEventType()
	{
		base.initEventType();

		addEvent<EventPrivateChange>();
	}

	public override void refresh()
	{
		setCount(
			mChat.getUnreadCount());
	}

	public override void resetProperty()
	{
		base.resetProperty();

		mChat = null;
	}

	public void setPrivateChat(
		PrivateChatInfo chat)
	{
		mChat = chat;
	}
}

绑定红点和数字文本:

bash 复制代码
mPrivateChat.getRedPoint().setPointUI(
	mMessagePoint,
	mUnreadMessageCount);

解除绑定:

bash 复制代码
mPrivateChat.getRedPoint().removePointUI(
	mMessagePoint,
	mUnreadMessageCount);

setCount() 会同时更新文本和显示状态:

bash 复制代码
public void setCount(int count)
{
	mCount = count;

	mPointCountText?.setText(mCount);

	setEnable(mCount > 0);
}

数量为零时,整个红点自动隐藏。

九、主动刷新全部红点

系统初始化完成,或者一次性加载大量服务器数据后,可以主动刷新整棵树:

bash 复制代码
mRedPointSystem.refresh();

它只从根节点开始遍历:

bash 复制代码
public void refresh()
{
	foreach (RedPoint point in mRedPointList)
	{
		if (point.getParent() == null)
		{
			refreshRedPoint(point);
		}
	}
}

递归过程会先刷新叶节点,再刷新父节点:

bash 复制代码
刷新全部子节点
    ↓
刷新当前父节点

这样父节点读取到的一定是子节点的最新状态。

十、哪些场景不适合使用这套红点树

并不是所有看起来像红点的需求,都适合创建长期红点对象。

例如:

bash 复制代码
翻页列表中,只表示当前页某个奖励是否可领取
红点关联的数据对象频繁更换
显示前面所有页面是否存在奖励
纯粹由当前筛选条件临时计算的标记

这些状态与固定业务对象没有稳定的一对一关系。

如果为每次翻页反复创建和销毁红点,反而会增加管理复杂度。此类临时状态直接由当前界面计算通常更合适。

十一、总结

MyFramework 的红点系统使用树形结构组织数据:

bash 复制代码
叶节点
监听业务事件并计算自身状态

父节点
根据子节点自动汇总状态

RedPointSystem
统一刷新脏节点并向上传播

UI节点
只绑定和显示,不判断业务逻辑

基本接入流程是:

  1. 尽早创建根节点和分类节点
  2. 为具体业务对象创建叶节点
  3. 在叶节点中监听事件并重写 refresh
  4. UI 创建时调用 bindPointUI
  5. UI 回收时调用 removePointUI
  6. 业务对象销毁时调用 destroyRedPoint

这样邮件、私聊、成就和活动等红点可以形成统一的数据树。下层状态变化后,上层入口自动刷新,同一份红点状态也能同时驱动多个界面。

相关推荐
SmalBox4 小时前
【节点】[Not节点]原理解析与实际应用
unity3d·游戏开发·图形学
万亿少女的梦16821 小时前
基于Unity3D的双状态场景解密游戏设计与实现
unity3d·游戏开发·场景渲染·交互系统·叙事设计
SmalBox1 天前
【节点】[Nand节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[IsNaN节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[IsInfinite节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_4 天前
Unity MyFramework 用法说明(二十二):使用 AudioManager 统一管理背景音乐与 2D/3D 音效
unity3d
SmalBox4 天前
【节点】[IsFrontFace节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_5 天前
Unity MyFramework 用法说明(二十一):使用 UndoManager 为游戏内 UGC 编辑器实现撤销与重做
unity3d
SmalBox5 天前
【节点】[Comparison节点]原理解析与实际应用
unity3d·游戏开发·图形学