编辑
2023-08-16
Unity游戏开发
00
请注意,本文编写于 47 天前,最后修改于 47 天前,其中某些信息可能已经过时。

目录

业务结构
代码实现
核心要点
1、红点节点定义
2、驱动层和表现层分离
3、只能修改叶子节点的规则
可以优化的点

在使用Unity开发游戏的时候经常用到红点系统,当玩家点击之后,或者收到服务器数据之后,都需要刷新红点的显示。如果每个人都自己写自己的红点模块,会增加不少的重复任务量,因此迫切需要一个通用的红点系统,其他模块只需要编写自己模块的表现层代码即可。所以单独抽取了一个红点树的逻辑。简单用UGUI拼了个界面,可以先看效果(暂时以数字表示红点数,0就是没有红点):

业务结构

RedPointSystem---> 红点树结构: main | |----club | |----msg | | |----msg2 | | |----msg3 | | |----msg1 | |----dress | | |----d1 | | |----d2

这是初始化的时候打印的红点树结构日志,其中 club 为工会,msg 为邮件系统,msg1-3分别表示好友、战队、系统邮件,dress表示装扮,d1 日常装扮,d2 战斗装扮。

代码实现

下面是使用 middleclass 和 lua 实现的静态红点树的代码:

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
lua
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

实际使用示例:

lua
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

整个系统参考了 《Unity游戏开发】客户端红点系统架构》

原文中有一处BUG,当超过三级节点的时候并子节点没有反馈红点变化事件,因为必须要通知上级Node

lua
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 驱动不同表现层逻辑

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!