Unity实现通用红点树
在使用Unity开发游戏的时候经常用到红点系统,当玩家点击之后,或者收到服务器数据之后,都需要刷新红点的显示。如果每个人都自己写自己的红点模块,会增加不少的重复任务量,因此迫切需要一个通用的红点系统,其他模块只需要编写自己模块的表现层代码即可。所以单独抽取了一个红点树的逻辑。简单用UGUI拼了个界面,可以先看效果(暂时以数字表示红点数,0就是没有红点):
业务结构
RedPointSystem---> 红点树结构:
main
| |----club
| |----msg
| | |----msg2
| | |----msg3
| | |----msg1
| |----dress
| | |----d1
| | |----d2
这是初始化的时候打印的红点树结构日志,其中 club 为工会,msg 为邮件系统,msg1-3分别表示好友、战队、系统邮件,dress表示装扮,d1 日常装扮,d2 战斗装扮。
代码实现
下面是使用 middleclass 和 lua 实现的静态红点树的代码:
local class = require("middleclass")
---@class RedPointNode
local RedPointNode = class("RedPointNode")
local log = function(...)
print("zcl RedPointNode--->", ...)
end
---@param numChangeFunc function
function RedPointNode:initialize()
---@type string
self.nodeName = ""
---@type number
self.pointNum = 0
---@type RedPointNode
self.parentNode = nil
---@type function
self.numChangeFunc = nil
---key:name value:RedPointNode
---@type table<string, RedPointNode>
self.childNodeMap = {}
end
function RedPointNode:SetRedPointNum(num)
if self:Size() > 0 then
log("红点数量只能设置叶子节点")
return
end
self.pointNum = num
self:NotifyPointNumChange()
if self.parentNode then
self.parentNode:ChangePredPointNum()
end
end
function RedPointNode:ChangePredPointNum()
local num = 0
for key, value in pairs(self.childNodeMap) do
num = num + value.pointNum
end
if num ~= self.pointNum then --- 红点数量变化
self.pointNum = num
self:NotifyPointNumChange()
end
end
function RedPointNode:NotifyPointNumChange()
if self.numChangeFunc then
self:numChangeFunc(self)
end
--- 通知上级Node
if self.parentNode then
self.parentNode:ChangePredPointNum()
end
end
function RedPointNode:Size()
local count = 0
for key, value in pairs(self.childNodeMap) do
count = count + 1
end
return count
end
return RedPointNode
local class = require("middleclass")
---@type RedPointNode
local RedPointNode = require(App.ModName.."/config/lua_lib/RedPointNode")
local log = function(...)
print("zcl RedPointSystem--->", ...)
end
---@class RedPointSystem
local RedPointSystem = class("RedPointSystem")
---@alias ChangeHandler fun(node: RedPointNode)
---@param nodeNameList table 红点节点名字列表(根结点必须放在第一个位置)
---@param changeCallback ChangeHandler 红点数量变化回调
function RedPointSystem:initialize(nodeNameList)
---@type RedPointNode
self.rootNode = nil
---@type table<RedPointNode>
self.nodeList = {}
-- g_Log("zcl RedPointNode--->", RedPointNode)
--- 根据名称列表初始化出节点
self:InitRootNode(nodeNameList)
end
function RedPointSystem:InitRootNode(nodeNameList)
self.rootNode = RedPointNode:new()
self.rootNode.nodeName = nodeNameList[1]
for i = 2, #nodeNameList do
local node = self.rootNode
local names = self:split(nodeNameList[i], ".")
if #names > 1 then
for index, value in ipairs(names) do
if node.childNodeMap[value] == nil then
local newNode = RedPointNode:new()
node.childNodeMap[value] = newNode
end
node.childNodeMap[value].parentNode = node
node.childNodeMap[value].nodeName = value
node = node.childNodeMap[value]
end
end
end
--- print node by tree
-- self:PrintNode(self.rootNode)
local result = {}
self:TraverseNode(self.rootNode.childNodeMap["main"], 0, result)
local logStr = table.concat(result, "\n")
log("红点树结构:\n", logStr)
end
---@param node RedPointNode
function RedPointSystem:TraverseNode(node, indent, result)
indent = indent or 0
local space = string.rep("| ", indent)
if indent > 0 then
space = space .. "|----"
end
table.insert(result, space .. node.nodeName)
for _, childNode in pairs(node.childNodeMap) do
self:TraverseNode(childNode, indent + 1, result)
end
end
---@param nodeStr string
---@param changeCallback ChangeHandler
function RedPointSystem:SetRedPointNodeCallBack(nodeStr, changeCallback)
local nodeList = self:split(nodeStr, ".")
if #nodeList == 1 and nodeList[1] ~= "main" then
log("唯一节点还是非根节点,错误!!")
return
end
local node = self.rootNode
for index, value in ipairs(nodeList) do
if node.childNodeMap[value] == nil then
log("不存在的Node --->", value)
return
end
node = node.childNodeMap[value]
if index == #nodeList then
--- 最后一个节点
node.numChangeFunc = changeCallback
return
end
end
end
--- 设置节点数量
---@param nodeStr string
---@param num number
function RedPointSystem:SetInvoke(nodeStr, num)
local nodeList = self:split(nodeStr, ".")
if #nodeList == 1 and nodeList[1] ~= "main" then
log("唯一节点还是非根节点,错误!!")
return
end
local node = self.rootNode
for index, value in ipairs(nodeList) do
if node.childNodeMap[value] == nil then
log("不存在的Node --->", value)
return
end
node = node.childNodeMap[value]
if index == #nodeList then
--- 最后一个节点
node:SetRedPointNum(num)
end
end
end
function RedPointSystem:split(str, delimiter)
local result = {}
local pattern = string.format("([^%s]+)", delimiter)
for token in string.gmatch(str, pattern) do
table.insert(result, token)
end
return result
end
return RedPointSystem
实际使用示例:
local NodeEnum = {
MAIN = "main",
MAIN_CLUB = "main.club",
MAIN_DRESS = "main.dress",
MAIN_DRESS_D1 = "main.dress.d1",
MAIN_DRESS_D2 = "main.dress.d2",
MAIN_MSG = "main.msg",
MAIN_MSG_MSG1 = "main.msg.msg1",
MAIN_MSG_MSG2 = "main.msg.msg2",
MAIN_MSG_MSG3 = "main.msg.msg3",
}
---@param worldElement CS.Tal.framesync.WorldElement
function FsyncElement:initialize(worldElement)
FsyncElement.super.initialize(self, worldElement)
self:InitService()
self:InitView()
---@type RedPointSystem
local RedPointSystem = require(App.ModName .. "/config/lua_lib/RedPointSystem")
---@type RedPointSystem
self.rps = RedPointSystem:new(
{
NodeEnum.MAIN,
NodeEnum.MAIN_CLUB,
NodeEnum.MAIN_DRESS,
NodeEnum.MAIN_DRESS_D1,
NodeEnum.MAIN_DRESS_D2,
NodeEnum.MAIN_MSG,
NodeEnum.MAIN_MSG_MSG1,
NodeEnum.MAIN_MSG_MSG2,
NodeEnum.MAIN_MSG_MSG3
}
)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN, function(node)
-- log("node--->", node.nodeName, node.pointNum)
self.treeRootText.text = "消息总数量:" .. node.pointNum
end)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_CLUB, function(node)
self.clubNodeText.text = "" .. node.pointNum
end)
------------------------
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_DRESS, function(node)
self.dressNodeText.text = "" .. node.pointNum
end)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_DRESS_D1, function(node)
self.d1NodeText.text = "日常装扮:" .. node.pointNum
end)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_DRESS_D2, function(node)
self.d2NodeText.text = "战斗装扮" .. node.pointNum
end)
------------------------
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG, function(node)
self.msgNodeText.text = "" .. node.pointNum
end)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG_MSG1, function(node)
self.msg1NodeText.text = "好友邮件:" .. node.pointNum
end)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG_MSG2, function(node)
self.msg2NodeText.text = "战队邮件:" .. node.pointNum
end)
self.rps:SetRedPointNodeCallBack(NodeEnum.MAIN_MSG_MSG3, function(node)
self.msg3NodeText.text = "系统邮件:" .. node.pointNum
end)
self:InitEventListener()
end
function FsyncElement:InitEventListener()
self.commonService:AddEventListener(self.clubAddbtn, "onClick", function()
self.clubNum = self.clubNum + 1
self.rps:SetInvoke(NodeEnum.MAIN_CLUB, self.clubNum)
end)
self.commonService:AddEventListener(self.clubSubbtn, "onClick", function()
self.clubNum = self.clubNum - 1
if self.clubNum < 0 then self.clubNum = 0 end
self.rps:SetInvoke(NodeEnum.MAIN_CLUB, self.clubNum)
end)
self.commonService:AddEventListener(self.d1Addbtn, "onClick", function()
self.d1Num = self.d1Num + 1
self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D1, self.d1Num)
end)
self.commonService:AddEventListener(self.d1Subbtn, "onClick", function()
self.d1Num = self.d1Num - 1
if self.d1Num < 0 then self.d1Num = 0 end
self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D1, self.d1Num)
end)
self.commonService:AddEventListener(self.d2Addbtn, "onClick", function()
self.d2Num = self.d2Num + 1
self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D2, self.d2Num)
end)
self.commonService:AddEventListener(self.d2Subbtn, "onClick", function()
self.d2Num = self.d2Num - 1
if self.d2Num < 0 then self.d2Num = 0 end
self.rps:SetInvoke(NodeEnum.MAIN_DRESS_D2, self.d2Num)
end)
self.commonService:AddEventListener(self.msg1Addbtn, "onClick", function()
self.mgs1Num = self.mgs1Num + 1
self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG1, self.mgs1Num)
end)
self.commonService:AddEventListener(self.msg1Subbtn, "onClick", function()
self.mgs1Num = self.mgs1Num - 1
if self.mgs1Num < 0 then self.mgs1Num = 0 end
self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG1, self.mgs1Num)
end)
self.commonService:AddEventListener(self.msg2Addbtn, "onClick", function()
self.msg2Num = self.msg2Num + 1
self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG2, self.msg2Num)
end)
self.commonService:AddEventListener(self.msg2Subbtn, "onClick", function()
self.msg2Num = self.msg2Num - 1
if self.msg2Num < 0 then self.msg2Num = 0 end
self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG2, self.msg2Num)
end)
self.commonService:AddEventListener(self.msg3Addbtn, "onClick", function()
self.msg3Num = self.msg3Num + 1
self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG3, self.msg3Num)
end)
self.commonService:AddEventListener(self.msg3Subbtn, "onClick", function()
self.msg3Num = self.msg3Num - 1
if self.msg3Num < 0 then self.msg3Num = 0 end
self.rps:SetInvoke(NodeEnum.MAIN_MSG_MSG3, self.msg3Num)
end)
end
function FsyncElement:InitView()
---@type CS.UnityEngine.RectTransform
self.rootUI = self.VisElement.gameObject.transform:Find("屏幕画布")
--- 红点树的根
---@type CS.UnityEngine.RectTransform
self.treeRootText = self.rootUI:Find("主页/num").gameObject:GetComponent("Text")
---@type CS.UnityEngine.RectTransform
self.clubNode = self.rootUI:Find("工会")
self.clubNodeText = self.clubNode:Find("num").gameObject:GetComponent("Text")
self.clubAddbtn = self.clubNode:Find("addbtn").gameObject:GetComponent("Button")
self.clubSubbtn = self.clubNode:Find("subbtn").gameObject:GetComponent("Button")
self.clubNum = 0
---@type CS.UnityEngine.RectTransform
self.dressNode = self.rootUI:Find("装扮")
self.dressNodeText = self.dressNode:Find("num").gameObject:GetComponent("Text")
---@type CS.UnityEngine.RectTransform
self.d1Node = self.rootUI:Find("日常装扮")
self.d1NodeText = self.d1Node:Find("文本属性").gameObject:GetComponent("Text")
self.d1Addbtn = self.d1Node:Find("addbtn").gameObject:GetComponent("Button")
self.d1Subbtn = self.d1Node:Find("subbtn").gameObject:GetComponent("Button")
self.d1Num = 0
---@type CS.UnityEngine.RectTransform
self.d2Node = self.rootUI:Find("战斗装扮")
self.d2NodeText = self.d2Node:Find("文本属性").gameObject:GetComponent("Text")
self.d2Addbtn = self.d2Node:Find("addbtn").gameObject:GetComponent("Button")
self.d2Subbtn = self.d2Node:Find("subbtn").gameObject:GetComponent("Button")
self.d2Num = 0
---@type CS.UnityEngine.RectTransform
self.msgNode = self.rootUI:Find("邮件")
self.msgNodeText = self.msgNode:Find("num").gameObject:GetComponent("Text")
---@type CS.UnityEngine.RectTransform
self.msg1 = self.rootUI:Find("好友邮件")
self.msg1NodeText = self.msg1:Find("文本属性").gameObject:GetComponent("Text")
self.msg1Addbtn = self.msg1:Find("addbtn").gameObject:GetComponent("Button")
self.msg1Subbtn = self.msg1:Find("subbtn").gameObject:GetComponent("Button")
self.mgs1Num = 0
---@type CS.UnityEngine.RectTransform
self.msg2 = self.rootUI:Find("战队邮件")
self.msg2NodeText = self.msg2:Find("文本属性").gameObject:GetComponent("Text")
self.msg2Addbtn = self.msg2:Find("addbtn").gameObject:GetComponent("Button")
self.msg2Subbtn = self.msg2:Find("subbtn").gameObject:GetComponent("Button")
self.msg2Num = 0
---@type CS.UnityEngine.RectTransform
self.msg3 = self.rootUI:Find("系统邮件")
self.msg3NodeText = self.msg3:Find("文本属性").gameObject:GetComponent("Text")
self.msg3Addbtn = self.msg3:Find("addbtn").gameObject:GetComponent("Button")
self.msg3Subbtn = self.msg3:Find("subbtn").gameObject:GetComponent("Button")
self.msg3Num = 0
end
核心要点
:::note{title=“NOTE”} 整个系统参考了 《Unity游戏开发】客户端红点系统架构》
原文中有一处BUG,当超过三级节点的时候并子节点没有反馈红点变化事件,因为必须要通知上级Node
function RedPointNode:NotifyPointNumChange()
if self.numChangeFunc then
self:numChangeFunc(self)
end
--- 通知上级Node
if self.parentNode then
self.parentNode:ChangePredPointNum()
end
end
::: 其实理解起来很简单,现在归纳总结一下核心要点:
1、红点节点定义
这一步很重要,因为整个树结构都依赖于这种通过名称定义的方式,通过名称确定 ParentNode、ChildNodes,驱动时也是按照名称去驱动的。
2、驱动层和表现层分离
Node之需要关心当前节点的消息数量即可,至于如何处理UI层,红点树系统本身并不关心,交给回调函数处理
3、只能修改叶子节点的规则
只能修改叶子节点,这样通过层层向上驱动,各个层级的 ParentNode 包括 RootNode 才能正确的更新,具体更新逻辑就是叶子节点发生改变,通知其 ParentNode 扫描本层的节点数量,如果没变,则不会向上层反馈。
可以优化的点
1、同一帧多个子节点同时改变状态,将导致父节点进行额外的无用刷新的问题。
2、可以丰富 Node 信息,根据 type 驱动不同表现层逻辑