Unity实现通用红点树
在使用Unity开发游戏的时候经常用到红点系统,当玩家点击之后,或者收到服务器数据之后,都需要刷新红点的显示。如果每个人都自己写自己的红点模块,会增加不少的重复任务量,因此迫切需要一个通用的红点系统,其他模块只需要编写自己模块的表现层代码即可。所以单独抽取了一个红点树的逻辑。简单用UGUI拼了个界面,可以先看效果(暂时以数字表示红点数,0就是没有红点):
业务结构
1RedPointSystem---> 红点树结构:
2 main
3| |----club
4| |----msg
5| | |----msg2
6| | |----msg3
7| | |----msg1
8| |----dress
9| | |----d1
10| | |----d2
这是初始化的时候打印的红点树结构日志,其中 club 为工会,msg 为邮件系统,msg1-3分别表示好友、战队、系统邮件,dress表示装扮,d1 日常装扮,d2 战斗装扮。
代码实现
下面是使用 middleclass 和 lua 实现的静态红点树的代码:
1local class = require("middleclass")
2
3---@class RedPointNode
4local RedPointNode = class("RedPointNode")
5
6local log = function(...)
7 print("zcl RedPointNode--->", ...)
8end
9
10---@param numChangeFunc function
11function RedPointNode:initialize()
12 ---@type string
13 self.nodeName = ""
14 ---@type number
15 self.pointNum = 0
16 ---@type RedPointNode
17 self.parentNode = nil
18 ---@type function
19 self.numChangeFunc = nil
20 ---key:name value:RedPointNode
21 ---@type table<string, RedPointNode>
22 self.childNodeMap = {}
23end
24
25function RedPointNode:SetRedPointNum(num)
26 if self:Size() > 0 then
27 log("红点数量只能设置叶子节点")
28 return
29 end
30
31 self.pointNum = num
32 self:NotifyPointNumChange()
33 if self.parentNode then
34 self.parentNode:ChangePredPointNum()
35 end
36end
37
38function RedPointNode:ChangePredPointNum()
39 local num = 0
40 for key, value in pairs(self.childNodeMap) do
41 num = num + value.pointNum
42 end
43
44 if num ~= self.pointNum then --- 红点数量变化
45 self.pointNum = num
46 self:NotifyPointNumChange()
47 end
48end
49
50function RedPointNode:NotifyPointNumChange()
51 if self.numChangeFunc then
52 self:numChangeFunc(self)
53 end
54
55 --- 通知上级Node
56 if self.parentNode then
57 self.parentNode:ChangePredPointNum()
58 end
59end
60
61function RedPointNode:Size()
62 local count = 0
63 for key, value in pairs(self.childNodeMap) do
64 count = count + 1
65 end
66 return count
67end
68
69return RedPointNode
1local class = require("middleclass")
2
3---@type RedPointNode
4local RedPointNode = require(App.ModName.."/config/lua_lib/RedPointNode")
5
6local log = function(...)
7 print("zcl RedPointSystem--->", ...)
8end
9
10---@class RedPointSystem
11local RedPointSystem = class("RedPointSystem")
12
13---@alias ChangeHandler fun(node: RedPointNode)
14---@param nodeNameList table 红点节点名字列表(根结点必须放在第一个位置)
15---@param changeCallback ChangeHandler 红点数量变化回调
16function RedPointSystem:initialize(nodeNameList)
17 ---@type RedPointNode
18 self.rootNode = nil
19 ---@type table<RedPointNode>
20 self.nodeList = {}
21 -- g_Log("zcl RedPointNode--->", RedPointNode)
22 --- 根据名称列表初始化出节点
23 self:InitRootNode(nodeNameList)
24end
25
26function RedPointSystem:InitRootNode(nodeNameList)
27 self.rootNode = RedPointNode:new()
28 self.rootNode.nodeName = nodeNameList[1]
29 for i = 2, #nodeNameList do
30 local node = self.rootNode
31 local names = self:split(nodeNameList[i], ".")
32 if #names > 1 then
33 for index, value in ipairs(names) do
34 if node.childNodeMap[value] == nil then
35 local newNode = RedPointNode:new()
36 node.childNodeMap[value] = newNode
37 end
38 node.childNodeMap[value].parentNode = node
39 node.childNodeMap[value].nodeName = value
40 node = node.childNodeMap[value]
41 end
42 end
43 end
44
45 --- print node by tree
46 -- self:PrintNode(self.rootNode)
47 local result = {}
48 self:TraverseNode(self.rootNode.childNodeMap["main"], 0, result)
49 local logStr = table.concat(result, "\n")
50 log("红点树结构:\n", logStr)
51end
52
53---@param node RedPointNode
54function RedPointSystem:TraverseNode(node, indent, result)
55 indent = indent or 0
56 local space = string.rep("| ", indent)
57 if indent > 0 then
58 space = space .. "|----"
59 end
60 table.insert(result, space .. node.nodeName)
61 for _, childNode in pairs(node.childNodeMap) do
62 self:TraverseNode(childNode, indent + 1, result)
63 end
64end
65
66---@param nodeStr string
67---@param changeCallback ChangeHandler
68function RedPointSystem:SetRedPointNodeCallBack(nodeStr, changeCallback)
69 local nodeList = self:split(nodeStr, ".")
70 if #nodeList == 1 and nodeList[1] ~= "main" then
71 log("唯一节点还是非根节点,错误!!")
72 return
73 end
74
75 local node = self.rootNode
76 for index, value in ipairs(nodeList) do
77 if node.childNodeMap[value] == nil then
78 log("不存在的Node --->", value)
79 return
80 end
81
82 node = node.childNodeMap[value]
83 if index == #nodeList then
84 --- 最后一个节点
85 node.numChangeFunc = changeCallback
86 return
87 end
88 end
89end
90
91--- 设置节点数量
92---@param nodeStr string
93---@param num number
94function RedPointSystem:SetInvoke(nodeStr, num)
95 local nodeList = self:split(nodeStr, ".")
96 if #nodeList == 1 and nodeList[1] ~= "main" then
97 log("唯一节点还是非根节点,错误!!")
98 return
99 end
100
101 local node = self.rootNode
102 for index, value in ipairs(nodeList) do
103 if node.childNodeMap[value] == nil then
104 log("不存在的Node --->", value)
105 return
106 end
107
108 node = node.childNodeMap[value]
109 if index == #nodeList then
110 --- 最后一个节点
111 node:SetRedPointNum(num)
112 end
113 end
114end
115
116function RedPointSystem:split(str, delimiter)
117 local result = {}
118 local pattern = string.format("([^%s]+)", delimiter)
119 for token in string.gmatch(str, pattern) do
120 table.insert(result, token)
121 end
122 return result
123end
124
125return RedPointSystem
实际使用示例:
1local NodeEnum = {
2 MAIN = "main",
3 MAIN_CLUB = "main.club",
4 MAIN_DRESS = "main.dress",
5 MAIN_DRESS_D1 = "main.dress.d1",
6 MAIN_DRESS_D2 = "main.dress.d2",
7 MAIN_MSG = "main.msg",
8 MAIN_MSG_MSG1 = "main.msg.msg1",
9 MAIN_MSG_MSG2 = "main.msg.msg2",
10 MAIN_MSG_MSG3 = "main.msg.msg3",
11}
12
13---@param worldElement CS.Tal.framesync.WorldElement
14function FsyncElement:initialize(worldElement)
15 FsyncElement.super.initialize(self, worldElement)
16 self:InitService()
17 self:InitView()
18
19 ---@type RedPointSystem
20 local RedPointSystem = require(App.ModName .. "/config/lua_lib/RedPointSystem")
21 ---@type RedPointSystem
22 self.rps = RedPointSystem:new(
23 {
24 NodeEnum.MAIN,
25 NodeEnum.MAIN_CLUB,
26 NodeEnum.MAIN_DRESS,
27 NodeEnum.MAIN_DRESS_D1,
28 NodeEnum.MAIN_DRESS_D2,
29 NodeEnum.MAIN_MSG,
30 NodeEnum.MAIN_MSG_MSG1,
31 NodeEnum.MAIN_MSG_MSG2,
32 NodeEnum.MAIN_MSG_MSG3
33 }
34 )
35
36 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN, function(node)
37 -- log("node--->", node.nodeName, node.pointNum)
38 self.treeRootText.text = "消息总数量:" .. node.pointNum
39 end)
40
41 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_CLUB, function(node)
42 self.clubNodeText.text = "" .. node.pointNum
43 end)
44
45
46 ------------------------
47 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_DRESS, function(node)
48 self.dressNodeText.text = "" .. node.pointNum
49 end)
50 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_DRESS_D1, function(node)
51 self.d1NodeText.text = "日常装扮:" .. node.pointNum
52 end)
53 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_DRESS_D2, function(node)
54 self.d2NodeText.text = "战斗装扮" .. node.pointNum
55 end)
56
57 ------------------------
58 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG, function(node)
59 self.msgNodeText.text = "" .. node.pointNum
60 end)
61 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG_MSG1, function(node)
62 self.msg1NodeText.text = "好友邮件:" .. node.pointNum
63 end)
64 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG_MSG2, function(node)
65 self.msg2NodeText.text = "战队邮件:" .. node.pointNum
66 end)
67 self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG_MSG3, function(node)
68 self.msg3NodeText.text = "系统邮件:" .. node.pointNum
69 end)
70
71 self:InitEventListener()
72end
73
74function FsyncElement:InitEventListener()
75 self.commonService:AddEventListener(self.clubAddbtn, "onClick", function()
76 self.clubNum = self.clubNum + 1
77 self.rps:SetInvoke(NodeEnum.MAIN_CLUB, self.clubNum)
78 end)
79 self.commonService:AddEventListener(self.clubSubbtn, "onClick", function()
80 self.clubNum = self.clubNum - 1
81 if self.clubNum < 0 then self.clubNum = 0 end
82 self.rps:SetInvoke(NodeEnum.MAIN_CLUB, self.clubNum)
83 end)
84
85 self.commonService:AddEventListener(self.d1Addbtn, "onClick", function()
86 self.d1Num = self.d1Num + 1
87 self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D1, self.d1Num)
88 end)
89 self.commonService:AddEventListener(self.d1Subbtn, "onClick", function()
90 self.d1Num = self.d1Num - 1
91 if self.d1Num < 0 then self.d1Num = 0 end
92 self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D1, self.d1Num)
93 end)
94 self.commonService:AddEventListener(self.d2Addbtn, "onClick", function()
95 self.d2Num = self.d2Num + 1
96 self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D2, self.d2Num)
97 end)
98 self.commonService:AddEventListener(self.d2Subbtn, "onClick", function()
99 self.d2Num = self.d2Num - 1
100 if self.d2Num < 0 then self.d2Num = 0 end
101 self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D2, self.d2Num)
102 end)
103
104
105
106 self.commonService:AddEventListener(self.msg1Addbtn, "onClick", function()
107 self.mgs1Num = self.mgs1Num + 1
108 self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG1, self.mgs1Num)
109 end)
110 self.commonService:AddEventListener(self.msg1Subbtn, "onClick", function()
111 self.mgs1Num = self.mgs1Num - 1
112 if self.mgs1Num < 0 then self.mgs1Num = 0 end
113 self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG1, self.mgs1Num)
114 end)
115 self.commonService:AddEventListener(self.msg2Addbtn, "onClick", function()
116 self.msg2Num = self.msg2Num + 1
117 self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG2, self.msg2Num)
118 end)
119 self.commonService:AddEventListener(self.msg2Subbtn, "onClick", function()
120 self.msg2Num = self.msg2Num - 1
121 if self.msg2Num < 0 then self.msg2Num = 0 end
122 self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG2, self.msg2Num)
123 end)
124 self.commonService:AddEventListener(self.msg3Addbtn, "onClick", function()
125 self.msg3Num = self.msg3Num + 1
126 self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG3, self.msg3Num)
127 end)
128 self.commonService:AddEventListener(self.msg3Subbtn, "onClick", function()
129 self.msg3Num = self.msg3Num - 1
130 if self.msg3Num < 0 then self.msg3Num = 0 end
131 self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG3, self.msg3Num)
132 end)
133end
134
135function FsyncElement:InitView()
136 ---@type CS.UnityEngine.RectTransform
137 self.rootUI = self.VisElement.gameObject.transform:Find("屏幕画布")
138 --- 红点树的根
139 ---@type CS.UnityEngine.RectTransform
140 self.treeRootText = self.rootUI:Find("主页/num").gameObject:GetComponent("Text")
141
142 ---@type CS.UnityEngine.RectTransform
143 self.clubNode = self.rootUI:Find("工会")
144 self.clubNodeText = self.clubNode:Find("num").gameObject:GetComponent("Text")
145 self.clubAddbtn = self.clubNode:Find("addbtn").gameObject:GetComponent("Button")
146 self.clubSubbtn = self.clubNode:Find("subbtn").gameObject:GetComponent("Button")
147 self.clubNum = 0
148
149 ---@type CS.UnityEngine.RectTransform
150 self.dressNode = self.rootUI:Find("装扮")
151 self.dressNodeText = self.dressNode:Find("num").gameObject:GetComponent("Text")
152
153 ---@type CS.UnityEngine.RectTransform
154 self.d1Node = self.rootUI:Find("日常装扮")
155 self.d1NodeText = self.d1Node:Find("文本属性").gameObject:GetComponent("Text")
156 self.d1Addbtn = self.d1Node:Find("addbtn").gameObject:GetComponent("Button")
157 self.d1Subbtn = self.d1Node:Find("subbtn").gameObject:GetComponent("Button")
158 self.d1Num = 0
159
160 ---@type CS.UnityEngine.RectTransform
161 self.d2Node = self.rootUI:Find("战斗装扮")
162 self.d2NodeText = self.d2Node:Find("文本属性").gameObject:GetComponent("Text")
163 self.d2Addbtn = self.d2Node:Find("addbtn").gameObject:GetComponent("Button")
164 self.d2Subbtn = self.d2Node:Find("subbtn").gameObject:GetComponent("Button")
165 self.d2Num = 0
166
167 ---@type CS.UnityEngine.RectTransform
168 self.msgNode = self.rootUI:Find("邮件")
169 self.msgNodeText = self.msgNode:Find("num").gameObject:GetComponent("Text")
170
171 ---@type CS.UnityEngine.RectTransform
172 self.msg1 = self.rootUI:Find("好友邮件")
173 self.msg1NodeText = self.msg1:Find("文本属性").gameObject:GetComponent("Text")
174 self.msg1Addbtn = self.msg1:Find("addbtn").gameObject:GetComponent("Button")
175 self.msg1Subbtn = self.msg1:Find("subbtn").gameObject:GetComponent("Button")
176 self.mgs1Num = 0
177
178 ---@type CS.UnityEngine.RectTransform
179 self.msg2 = self.rootUI:Find("战队邮件")
180 self.msg2NodeText = self.msg2:Find("文本属性").gameObject:GetComponent("Text")
181 self.msg2Addbtn = self.msg2:Find("addbtn").gameObject:GetComponent("Button")
182 self.msg2Subbtn = self.msg2:Find("subbtn").gameObject:GetComponent("Button")
183 self.msg2Num = 0
184
185 ---@type CS.UnityEngine.RectTransform
186 self.msg3 = self.rootUI:Find("系统邮件")
187 self.msg3NodeText = self.msg3:Find("文本属性").gameObject:GetComponent("Text")
188 self.msg3Addbtn = self.msg3:Find("addbtn").gameObject:GetComponent("Button")
189 self.msg3Subbtn = self.msg3:Find("subbtn").gameObject:GetComponent("Button")
190 self.msg3Num = 0
191end
核心要点
:::note{title=“NOTE”} 整个系统参考了 《Unity游戏开发】客户端红点系统架构》
原文中有一处BUG,当超过三级节点的时候并子节点没有反馈红点变化事件,因为必须要通知上级Node
1function RedPointNode:NotifyPointNumChange()
2 if self.numChangeFunc then
3 self:numChangeFunc(self)
4 end
5
6 --- 通知上级Node
7 if self.parentNode then
8 self.parentNode:ChangePredPointNum()
9 end
10end
::: 其实理解起来很简单,现在归纳总结一下核心要点:
1、红点节点定义
这一步很重要,因为整个树结构都依赖于这种通过名称定义的方式,通过名称确定 ParentNode、ChildNodes,驱动时也是按照名称去驱动的。
2、驱动层和表现层分离
Node之需要关心当前节点的消息数量即可,至于如何处理UI层,红点树系统本身并不关心,交给回调函数处理
3、只能修改叶子节点的规则
只能修改叶子节点,这样通过层层向上驱动,各个层级的 ParentNode 包括 RootNode 才能正确的更新,具体更新逻辑就是叶子节点发生改变,通知其 ParentNode 扫描本层的节点数量,如果没变,则不会向上层反馈。
可以优化的点
1、同一帧多个子节点同时改变状态,将导致父节点进行额外的无用刷新的问题。
2、可以丰富 Node 信息,根据 type 驱动不同表现层逻辑