嗨,大家好,我是新发。
有小伙伴私信我让我写一个红点系统教程,
可以说,红点系统在游戏中是必备的系统,
可能有的同学会说,不就是根据条件设置红点的显示吗?
如果你的游戏足够简单,系统不多,那么你可以不考虑写管理层逻辑,怎么简单怎么来。
但实际项目中,模块系统是很多的,情况会相对复杂,比如下面这样子的结构,
好了,现在我问你,要做到高效、易拓展、易维护,你对这些红点如何进行组织管理?
聪明的小伙伴应该已经看出来了,红点系统很适合使用 树 这种数据结构来组织,那具体如何实现呢?
本文,我就来讲讲红点系统的具体实现吧~
我之前封装了一个游戏框架:UnityXFramework
,我对应的博客文章:《【游戏开发框架】自制Unity通用游戏框架UnityXFramework,详细教程(Unity3D技能树 | tolua | 框架 | 热更新)》
框架中我集成了tolua
,业务逻辑使用lua
来开发的,我打算在框架中去实现红点系统(使用lua
来实现)
注:建议先看我上面这篇
UnityXFramework
框架教程的文章,这样对你理解下文中我写的lua
代码有帮助。
注:如果你想用纯
C#
实现也可以,只要看懂了本文的原理,相信可以轻松写出C#
版本的
首先我们先明确红点系统的规则,一般都是如下的规则:
1 红点的显示方式分两种:带数字和不带数字;
2 如果子节点有红点,父节点也要显示红点,父节点红点数为子节点红点数的和;
3 当子节点红点更新时,对应的父节点也要更新;
4 当所有子节点都没有红点时,父节点才不显示红点。
好了,上面我们说了使用 树 这种数据结构来组织红点数据,而树有很多种,比如红黑树、B+树、霍夫曼树等等,到底用什么树呢?
我们分析一下,首先,使用 树 来组织,叶子节点可能会超过2
,所以 不是二叉树;子节点之间没有顺序关系,所以它是一个棵 无序树;我们要实现高效搜索和修改操作,前缀树 可以满足我们的需求。
先科普一下什么是前缀树。
前缀树,也叫Trie
树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
比如我们插入abc
、abh
、acg
三个单词,在树中的结构是这样:
那如果我再插入abc
,怎么办呢?结构依然是上面那样,节点本身会记录字母出现的次数,比如我们设计节点存储的信息如下:
所以插入两次abc
后,树节点的信息如下,
当我们要去树中查询abc
出现过几次的时候,只需要把abc
分割成a
、b
、c
,从根节点依次往下查询是否存在a
、b
、c
,最终返回c
节点的endCnt
即可,如果想查询以ab
为前缀的单词在树中出现了多少次,则分割为a
、b
后,从根节点往下查询a
、b
,然后返回b
节点的passCnt
即可,这也是前缀树的命名的由来。
我们只需要在上面的基础上,给节点加一个红点数的数据即可,如下
另外我们通过逻辑来实现父节点的红点数为子节点红点数之和即可。
我们将红点进行规范命名:层级1|层级2|层级3
,例Root|ModelA|ModelA_Sub_1
,我们把它以|
符号分割,然后插入树中,树变成这样子:
我们再插入一个Root|ModelA|ModelA_Sub_2
,树变成这样子:
我们再插入Root|ModelB|ModelB_Sub_1
,树变成这样子:
假设ModelA_Sub_1
节点有一个红点,那么它的父节点ModelA
也会有一个红点,同理Root
也会有一个红点,如下,
如果ModelA_Sub_2
节点也有一个红点,那么树的状态就是这样子:
当我们要查询ModelA
有多少个红点的时候,则通过Root|ModelA
来查询,以|
为分割符,从根节点出发,找到ModelA
节点后,返回ModelA
的redpointCnt
即为对应的红点数。
好了,下面我们开始动手写代码吧~
在Assets/LuaFramework/Lua/Logic
目录中新建Redpoint
文件夹,分别创建RedpointNode.lua
脚本和RedpointTree.lua
脚本,如下:
RedpointNode
脚本代码很简单,提供一个New
方法,构造节点,如下:
-- RedpointNode.lua
-- 红点系统,树节点
RedpointNode = RedpointNode or {}
RedpointNode.__index = RedpointNode
-- 构造节点
function RedpointNode.New(name)
local self = {}
-- 节点名
self.name = name
-- 节点被经过的次数
self.passCnt = 0
-- 节点作为末尾节点的次数
self.endCnt = 0
-- 红点数(子节点的红点数的和)
self.redpointCnt = 0
-- 子节点
self.children = {}
-- 红点更新时回调
self.updateCb = {}
setmetatable(self, RedpointNode)
return self
end
RedpointTree.lua
脚本封装树的行为。
先定义一个根节点root
,在Init
函数中创建根节点,如下
-- RedpointTree.lua
-- 红点系统树,前缀树结构
RedpointTree = RedpointTree or {}
local this = RedpointTree
this.root = nil
-- 初始化
function RedpointTree.Init()
-- 先创建根节点
this.root = RedpointNode.New("Root")
-- TODO 构建树结构
end
上面TODO
要构建树结构,我们需要先定义树节点的名,按层级1|层级2|层级3
这种格式命名,
-- RedpointTree.lua
-- 节点名
RedpointTree.NodeNames = {
Root = "Root",
ModelA = "Root|ModelA",
ModelA_Sub_1 = "Root|ModelA|ModelA_Sub_1",
ModelA_Sub_2 = "Root|ModelA|ModelA_Sub_2",
ModelB = "Root|ModelB",
ModelB_Sub_1 = "Root|ModelB|ModelB_Sub_1",
ModelB_Sub_2 = "Root|ModelB|ModelB_Sub_2",
}
封装一个InsertNode
方法,提供插入节点的功能,如下
-- RedpointTree.lua
-- 插入节点
function RedpointTree.InsertNode(name)
if LuaUtil.IsStrNullOrEmpty(name) then
return
end
if this.SearchNode(name) then
-- 如果已经存在,则不重复插入
log("你已经插入过节点了, name: " .. name)
return
end
-- node从根节点出发
local node = this.root
node.passCnt = node.passCnt + 1
-- 将名字按|符号分割
local pathList = LuaUtil.SplitString(name, "|")
for _, path in pairs(pathList) do
if nil == node.children[path] then
node.children[path] = RedpointNode.New(path)
end
node = node.children[path]
node.passCnt = node.passCnt + 1
end
node.endCnt = node.endCnt + 1
end
其中SearchNode
是搜索节点,代码如下
-- RedpointTree.lua
-- 查询节点是否在树中并返回节点
function RedpointTree.SearchNode(name)
if LuaUtil.IsStrNullOrEmpty(name) then
return nil
end
local node = this.root
local pathList = LuaUtil.SplitString(name, "|")
for _, path in pairs(pathList) do
if nil == node.children[path] then
return nil
end
node = node.children[path]
end
if node.endCnt > 0 then
return node
end
return nil
end
再封装一个删除节点的方法,
-- RedpointTree.lua
-- 删除某个节点
function RedpointTree.DeleteNode(name)
if nil == this.SearchNode(name) then
return
end
local node = this.root
node.passCnt = node.passCnt - 1
local pathList = LuaUtil.SplitString(name, '.')
for _, path in pairs(pathList) do
local childNode = node.children[path]
childNode.passCnt = childNode.passCnt - 1
if 0 == childNode.passCnt then
node.children[path] = nil
return
end
node = childNode
end
node.endCnt = node.endCnt - 1
end
上面我们提供了节点的插入、查询和删除操作,并没有操作节点的红点数,我们还需要封装一个修改节点红点数的方法,这里我使用的是增量操作,你也可以使用赋值操作,
-- RedpointTree.lua
-- 修改节点的红点数
function RedpointTree.ChangeRedpointCnt(name, delta)
local targetNode = this.SearchNode(name)
if nil == targetNode then
return
end
-- 如果是减红点,并且红点数不够减了,则调整delta,使其不减为0
if delta < 0 and targetNode.redpointCnt + delta < 0 then
delta = -targetNode.redpointCnt
end
local node = this.root
local pathList = LuaUtil.SplitString(name, "|")
for _, path in pairs(pathList) do
local childNode = node.children[path]
childNode.redpointCnt = childNode.redpointCnt + delta
node = childNode
-- 调用回调函数
for _, cb in pairs(node.updateCb) do
cb(node.redpointCnt)
end
end
end
上面修改红点数时,会调用节点的updateCb
回调,方便我们更新UI
界面的红点,这里我们封装一个设置回调的方法,
-- RedpointTree.lua
-- 设置红点更新回调函数
-- name: 节点名
-- key: 回调key,自定义字符串
-- cb: 回调函数
function RedpointTree.SetCallBack(name, key, cb)
local node = this.SearchNode(name)
if nil == node then
return
end
node.updateCb[key] = cb
end
我们UI
上要显示红点数量,需要查询模块的红点数,我们封装一个查询红点的方法,如下
-- RedpointTree.lua
-- 查询节点的红点数
function RedpointTree.GetRedpointCnt(name)
local node = this.SearchNode(name)
if nil == node then
return 0
end
return node.redpointCnt or 0
end
我们回到Init
方法中,构建整颗前缀树,并插入一些红点数据,如下
-- RedpointTree.lua
function RedpointTree.Init()
-- 先创建根节点
this.root = RedpointNode.New("Root")
-- 构建前缀树
for _, name in pairs(RedpointTree.NodeNames) do
this.InsertNode(name)
end
-- for test-----------------------------------------------
-- 塞入红点数据
this.ChangeRedpointCnt(this.NodeNames.ModelA_Sub_1, 1)
this.ChangeRedpointCnt(this.NodeNames.ModelA_Sub_2, 1)
this.ChangeRedpointCnt(this.NodeNames.ModelB_Sub_1, 1)
this.ChangeRedpointCnt(this.NodeNames.ModelB_Sub_2, 1)
end
我们在Main.lua
脚本中加上红点树的Init
方法调用,如下
-- Main.lua
function Main.Init()
log("Lua Main.Init")
-- 红点系统
RedpointTree.Init()
...
end
到此,我们的红点系统的数据组织逻辑就全部写完了,接下来就是UI
部分了。
我们在大厅界面加上红点UI
,如下,这个红点系统模块的入口,就是Root
节点,当然,实际项目中这个Root
节点应该是不可见的,一般是Root
的一级子节点作为各自模块入口来显示红点,这里我只是为了演示,做了一个红点系统的入口,
我们再做一个红点系统界面,专门来测试红点,界面如下,左侧两个标签页:ModelA
和ModelB
,分别作为两个模块。
模块里面又有两个子按钮,比如ModelA
模块中有ModelA_Sub_1
和ModelA_Sub_2
,
节点关系如下
我们先使用PrefabBinder
将大厅红点系统入口的红点文本进行对象绑定,方便在代码中获取UI
对象,如下
接着我们打开大厅的脚本GameHallPanel.lua
,在SetUi
方法中添加红点UI
的逻辑,如下
-- GameHallPanel:SetUi.lua
...
local RT = RedpointTree
-- UI交互
function GameHallPanel:SetUi(binder)
...
-- 获取红点文本UI对象
self.redpointText = binder:GetObj("redpointText")
-- 注册红点回调
RT.SetCallBack(RT.NodeNames.Root, "Root", function (redpointCnt)
self:UpdateRedPoint(redpointCnt)
end)
self:UpdateRedPoint(RT.GetRedpointCnt(RT.NodeNames.Root))
end
-- 更新红点
function GameHallPanel:UpdateRedPoint(redpointCnt)
self.redpointText.text = tostring(redpointCnt)
LuaUtil.SafeActiveObj(self.redpointText.transform.parent, redpointCnt > 0)
end
因为我们在RedpointTree.lua
脚本的Init
函数中已经塞入了红点数据,如下
所以,理论上我们Root
节点应该是有四个红点,我们运行看看,
可以看到,显示正确,下面,我们去写红点系统界面内部的代码吧。
首先给红点系统界面绑定UI
对象,如下
注册界面资源,分配一个资源ID
,方便我们通过资源ID
实例化界面,
接着,在Assets/LuaFramework/Lua/View
目录中创建Redpoint
文件夹,再创建一个RedpointPanel.lua
脚本,如下
首先编写界面的常规方法,如下
-- RedpointPanel.lua
-- 红点系统界面
RedpointPanel = RedpointPanel or {}
RedpointPanel.__index = RedpointPanel
-- 红点数
local RT = RedpointTree
-- 界面对象
local instance = nil
-- 显示界面
function RedpointPanel.Show()
instance = UITool.CreatePanelObj(instance, RedpointPanel, 'RedpointPanel', PANEL_ID.REDPOINT_PANEL_ID, GlobalObjs.s_windowPanel)
end
-- 关闭界面
function RedpointPanel.Hide()
UITool.HidePanel(instance)
end
-- 界面显示回调
function RedpointPanel:OnShow(parent)
-- 实例化界面预设,资源ID是15
local panelObj = UITool.Instantiate(parent, 15)
self.panelObj = panelObj
local binder = panelObj:GetComponent("PrefabBinder")
-- 设置UI交互
self:SetUi(binder)
end
-- UI交互
function RedpointPanel:SetUi(binder)
UGUITool.SetButton(binder, "closeBtn", function (btn)
self.Hide()
LoginLogic.DoLogout()
end)
self.propItem = binder:GetObj("propItem")
-- TODO 获取UI对象
-- TODO 注册红点更新回调
end
function RedpointPanel:OnHide()
LuaUtil.SafeDestroyObj(self.panelObj)
instance = nil
-- TODO 注销红点回调
end
我们通过binder
来获取绑定的UI
对象,如下,
-- RedpointPanel.lua
function RedpointPanel:SetUi(binder)
...
self.modelARedpointText = binder:GetObj("modelARedpointText")
self.modelBRedpointText = binder:GetObj("modelBRedpointText")
self.modelASub1Redpoint = binder:GetObj("modelASub1Redpoint")
self.modelASub2Redpoint = binder:GetObj("modelASub2Redpoint")
self.modelBSub1Redpoint = binder:GetObj("modelBSub1Redpoint")
self.modelBSub2Redpoint = binder:GetObj("modelBSub2Redpoint")
...
end
接着我们设置红点更新回调,并获取当前红点数据设置红点UI
,以ModelA
为例,
-- RedpointPanel.lua
function RedpointPanel:SetUi(binder)
...
-- 注册红点更新回调
RT.SetCallBack(RT.NodeNames.ModelA, "ModelA", function (redpointCnt)
self:UpdateRedPoint_ModelA(redpointCnt)
end)
self:UpdateRedPoint_ModelA(RedpointTree.GetRedpointCnt(RT.NodeNames.ModelA))
...
end
function RedpointPanel:UpdateRedPoint_ModelA(redpointCnt)
self.modelARedpointText.text = tostring(redpointCnt)
LuaUtil.SafeActiveObj(self.modelARedpointText.transform.parent, redpointCnt > 0)
end
最后,在界面关闭时记得注销红点回调,
-- RedpointPanel.lua
function RedpointPanel:OnHide()
...
-- 注销红点回调
RT.SetCallBack(RT.NodeNames.ModelA, "ModelA", nil)
...
end
然后,我希望点击模块Sub
按钮的时候会扣除对应的红点,我们加上对应的逻辑,
-- RedpointPanel.lua
function RedpointPanel:SetUi(binder)
...
-- 点击ModelA_Sub_1按钮,减少对应的红点
UGUITool.SetButton(binder, "modelASub1Btn", function (btn)
RT.ChangeRedpointCnt(RT.NodeNames.ModelA_Sub_1, -1)
end)
...
end
依次编写其他红点UI
逻辑即可。
最后,运行测试效果如下,可以看到,我们已经实现了红点系统的功能了,
本文项目源码已在GitCode
开源,感兴趣的同学可自行下载学习,如果喜欢记得给我一个星星,感激万分~
项目源码地址:https://codechina.csdn.net/linxinfa/UnityXFramework
好啦,就到这里吧~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~