首先简单介绍下前缀树红点系统:
游戏中有很多红点用于标识我们有未完成的任务和未领取的奖励等,比如一个公会红点,里面包含公会任务,公会商店,公会技能等红点,公会任务下又包含积分红点,任务奖励红点等等...如下图:
根据上图中的红点结构图可知,在管理红点系统的时候利用树结构管理最为合理。
在通常的红点管理代码中,我们通常会实现所有叶结点(任务奖励红点,积分奖励红点)的红点检查函数,例如 :
CheckTaskRewardRedDot()CheckTaskPointRedDot()CheckShopType1RedDot()CheckOrdinarySkillRedDot()CheckCoreSkillRedDot()
然后通过组合 CheckTaskRewardRedDot() 和 CheckTaskPointRedDot() 来实现公会任务的红点检查函数 CheckTaskRedDot(), 其他同理,于是分别有了 CheckShopRedDot(),CheckSkillRedDot 红点检查函数,随后再组合成公会总红点检查函数 CheckFactionRedDot()
这是最直接的红点系统方案,缺点也很明显,比如红点的检查是由上及下的,比如主界面要检查公会红点,就要调用 CheckFactionRedDot(),而 CheckFactionRedDot() 要调用 CheckTaskRedDot(),CheckShopRedDot(),CheckSkillRedDot(),也就是要检查所有的公会红点,如果只是一个简单的任务奖励红点改变了,这个时候发出红点更新的通知,主界面监听到更新红点却要更新整个公会红点,花费十分巨大,很不划算。倘若是主界面还有定时器更新红点更新函数,那更是一场灾难!
还有一个缺点是主界面需要监听所有红点改变的通知,才能正确刷新红点状态。
所以我们的首个改变思路是可以将由上及下的红点检查,改为由下及上的,让叶子结点红点变化后自发的通知它的父节点也去更新,一层一层的通知到 Root 结点。这样的话对于红点检查的算力的安排就十分合理。不过要这样做的话,我们需要构建一个红点树,并且让子节点和父结点之间能够相互关联。
那么当子节点更新发出通知后,父节点如何判断自己的是否该显示红点呢?我们在结点代码中定义了一个value 值,这 value 值是的它子节点的 value 值的和,当 value 大于 0 就代表它的子结点中有正在展示红点的结点,它本身也就需要展示红点。而叶节点的 value 值就根据它自身的红点检查函数来获取。
还有一个问题需要解决的是结点的查找问题,如果采用遍历的话也可以,虽然解决了由上及下的问题,却增加了遍历结点算力消耗问题,并不是很划算,自此,便可以引入前缀树了。
前缀树的优点是查找时间复杂度是O (k), k 为路径的长度,并且通过路径结构将树划分。接下来看代码。
-
前缀树红点结点
---@class RedDotTreeNode : LuaClass local RedDotTreeNode = class("RedDotTreeNode") ---@param name string ---@param parent RedDotTreeNode function RedDotTreeNode:Ctor(name, parent) self:SuperCall(RedDotTreeNode) self.Value = 0 self.Name = name or "" ---@type RedDotTreeNode self.Parent = parent self.m_fullPath = "" ---@type table<integer, fun(value:number):nil> self.m_changeCallBackList = {} ---@type table<string, RedDotTreeNode> self.m_children = {} end ---@return string function RedDotTreeNode:GetFullPath() if self.m_fullPath == "" then if self.Parent and self.Parent ~= RedDotManager.GetInstance().Root then self.m_fullPath = self.Parent:GetFullPath() .. RedDotManager.SplitChar .. self.Name else self.m_fullPath = self.Name end end return self.m_fullPath end ---@return integer function RedDotTreeNode:GetChildCount() return table.nums(self.m_children) end ---添加监听,禁止传入匿名函数 ---@param func fun(value:number):nil function RedDotTreeNode:AddListener(func) for _, callBackFunc in ipairs(self.m_changeCallBackList) do if callBackFunc == func then return end end table.insert(self.m_changeCallBackList, func) end ---移除监听,禁止传入匿名函数 ---@param func fun(value:number):nil function RedDotTreeNode:RemoveListener(func) for index, callBackFunc in ipairs(self.m_changeCallBackList) do if callBackFunc == func then table.remove(self.m_changeCallBackList, index) break end end end ---改变节点值 ---@param value? number function RedDotTreeNode:ChangeValue(value) if value then if self:GetChildCount() > 0 then error("不允许直接改变非叶子节点的值" .. self) end self:InternalChangeValue(value) else local sum = 0 if self:GetChildCount() > 0 then for _, v in pairs(self.m_children) do sum = sum + v.Value end end self:InternalChangeValue(sum) end end ---@param key string ---@return RedDotTreeNode function RedDotTreeNode:GetOrAddChild(key) local child = self:GetChild(key) if not child then child = self:AddChild(key) end return child; end ---@param key string ---@return RedDotTreeNode function RedDotTreeNode:AddChild(key) if self.m_children[key] then error("子节点添加失败,不允许重复添加:" .. self:GetFullPath()) end ---@type RedDotTreeNode local child = RedDotTreeNode.New(key, self) self.m_children[key] = child return child end ---@return RedDotTreeNode | nil function RedDotTreeNode:GetChild(key) return self.m_children[key] end ---@param path string ---@return boolean function RedDotTreeNode:RemoveChild(path) if self:GetChildCount() == 0 then return false end local child = self:GetChild(path) if child then RedDotManager.GetInstance():MarkDirtyNode(child) self.m_children[path] = nil return true end return false end function RedDotTreeNode:RemoveAllChild() self.m_children = {} end ---改变节点值 ---@private function RedDotTreeNode:InternalChangeValue(newValue) if self.Value == newValue then return end self.Value = newValue if self.m_changeCallBackList then for _, callBackFunc in ipairs(self.m_changeCallBackList) do callBackFunc(newValue) end end RedDotManager.GetInstance():MarkDirtyNode(self.Parent) end RedDotTreeNode.__tostring = function (self) return self:GetFullPath() end return RedDotTreeNode -
前缀树红点系统管理代码
local RedDotTreeNode = require("Reddot/RedDotTreeNode") ---@class RedDotManager : LuaClass local RedDotManager = class("RedDotManager") RedDotManager.SplitChar = "/" function RedDotManager:Ctor() ---@type RedDotManager self.m_instance = nil ---@type table<string, RedDotTreeNode> self.m_allNodes = {} ---@type table<string, RedDotTreeNode> self.m_dirtyNodes = {} ---@type RedDotTreeNode[] self.m_tempDirtyNodes = {} ---@type RedDotTreeNode self.Root = RedDotTreeNode.New("Root") end ---@return RedDotManager function RedDotManager.GetInstance() if RedDotManager.m_instance == nil then RedDotManager.m_instance = RedDotManager.New() end return RedDotManager.m_instance end ---添加监听,禁止传入匿名函数,AddListener与RemoveListener必须成对出现 ---@param path string ---@param callBackFunc fun(value:number):nil ---@return RedDotTreeNode | nil function RedDotManager:AddListener(path, callBackFunc) if not callBackFunc then return nil end local node = self:GetTreeNode(path) if node then node:AddListener(callBackFunc) end return node end ---删除监听,禁止传入匿名函数 ---@param path string ---@param callBackFunc fun(value:number):nil ---@return RedDotTreeNode | nil function RedDotManager:RemoveListener(path, callBackFunc) if not callBackFunc then return end local node = self:GetTreeNode(path) node:RemoveListener(callBackFunc) end function RedDotManager:RemoveAllListener(path) local node = self:GetTreeNode(path) node:RemoveAllListener() end ---@return number function RedDotManager:GetValue(path) local node = self:GetTreeNode(path) if not node then return 0 end return node.Value end ---@param path string ---@return RedDotTreeNode function RedDotManager:GetTreeNode(path) if path == "" then error("路径不能为空") end if self.m_allNodes[path] then return self.m_allNodes[path] end -- 这里根据路径插入前缀树,并保存起来 local cur = self.Root local length = #path local startIndex = 1 for i = 1, length do local char = path:sub(i, i) if char == RedDotManager.SplitChar then if i == length then error("路径不合法,不能以分隔符结尾" .. path) end local endIndex = i - 1 if endIndex < startIndex then error("路径不合法,不能存在连续的路径分隔符或以路径分隔符开头:" .. path) end local child = cur:GetOrAddChild(path:sub(startIndex, endIndex)) startIndex = i + 1 cur = child end end local target = cur:GetOrAddChild(path:sub(startIndex, length)) self.m_allNodes[path] = target return target end ---@param path string ---@return boolean function RedDotManager:RemoveTreeNode(path) if not self.m_allNodes[path] then return false end local node = self:GetTreeNode(path) self.m_allNodes[path] = nil if node.Parent then return node.Parent:RemoveChild(node.Name) else return true end end function RedDotManager:RemoveAllTreeNode() self.Root:RemoveAllChild() self.m_allNodes = {} end ---@param node RedDotTreeNode function RedDotManager:MarkDirtyNode(node) if not node or node == self.Root then return end self.m_dirtyNodes[node:GetFullPath()] = node end ---管理器轮询 function RedDotManager:Update() if table.nums(self.m_dirtyNodes) == 0 then return end self.m_tempDirtyNodes = {} -- 这里可以控制每帧更新多少个结点 for _, v in pairs(self.m_dirtyNodes) do table.insert(self.m_tempDirtyNodes, v) end self.m_dirtyNodes = {} for _, v in ipairs(self.m_tempDirtyNodes) do v:ChangeValue() end end return RedDotManager -
构建红点系统代码
---@class FactionRedDotUtil : LuaClass local FactionRedDotUtil = class("FactionRedDotUtil") -- 这里每一个路径代表一个结点,多层路径的代表是子节点 FactionRedDotUtil.RedDotPathDic = { Faction = "Faction", -- 大厅 Lobby = "Faction/Lobby", -- 技能 Skill = "Faction/Skill", -- 任务 Mission = "Faction/Mission", MissionPointReward = "Faction/Mission/MissionPointReward", MissionReward = "Faction/Mission/MissionReward", -- GVG GVG = "Faction/GVG", } FactionRedDotUtil.isInit = false function FactionRedDotUtil.Init() if not FactionRedDotUtil.isInit then ---@type table<string, fun(value:number):nil> FactionRedDotUtil.RedDotChangeCallFuncDic = { [FactionRedDotUtil.RedDotPathDic.Faction] = FactionRedDotUtil.FactionReddotChangeCallback, [FactionRedDotUtil.RedDotPathDic.Lobby] = FactionRedDotUtil.LobbyReddotChangeCallback, [FactionRedDotUtil.RedDotPathDic.Skill] = FactionRedDotUtil.SkillReddotChangeCallback, [FactionRedDotUtil.RedDotPathDic.Mission] = FactionRedDotUtil.MissionReddotChangeCallback, [FactionRedDotUtil.RedDotPathDic.MissionPointReward] = FactionRedDotUtil.MissionPointRewardReddotChangeCallback, [FactionRedDotUtil.RedDotPathDic.MissionReward] = FactionRedDotUtil.MissionRewardReddotChangeCallback, [FactionRedDotUtil.RedDotPathDic.GVG] = FactionRedDotUtil.FactionGVGReddotChangeCallback, } for _, v in pairs(FactionRedDotUtil.RedDotPathDic) do RedDotManager.GetInstance():AddListener(v, FactionRedDotUtil.RedDotPathDic[v]) end FactionRedDotUtil.isInit = true end end -- 公会主红点值改变回调 function FactionRedDotUtil.FactionReddotChangeCallback(value) LuaAppFacade.getIns():SendNotification(NoticeConst.FACTION_MAIN_REDDOT_CHANGE, value) end -- 公会大厅红点值改变回调 function FactionRedDotUtil.LobbyReddotChangeCallback(value) LuaAppFacade.getIns():SendNotification(NoticeConst.FACTION_LOBBY_REDDOT_CHANGE, value) end -- 公会技能红点值改变回调 function FactionRedDotUtil.SkillReddotChangeCallback(value) LuaAppFacade.getIns():SendNotification(NoticeConst.FACTION_SKILL_REDDOT_CHANGE, value) end -- 公会任务红点值改变回调 function FactionRedDotUtil.MissionReddotChangeCallback(value) LuaAppFacade.getIns():SendNotification(NoticeConst.FACTION_MISSION_REDDOT_CHANGE, value) end -- 公会任务积分奖励红点值改变回调 function FactionRedDotUtil.MissionPointRewardReddotChangeCallback(value) LuaAppFacade.getIns():SendNotification(NoticeConst.FACTION_MISSION_POINT_REWARD_REDDOT_CHANGE, value) end -- 公会任务奖励红点值改变回调 function FactionRedDotUtil.MissionRewardReddotChangeCallback(value) LuaAppFacade.getIns():SendNotification(NoticeConst.FACTION_MISSION_REWARD_REDDOT_CHANGE, value) end -- 公会GVG红点值改变回调 function FactionRedDotUtil.FactionGVGReddotChangeCallback(value) -- GVG模块主红点 -- 这里不用发出通知的原因是GVG被设计成了一个单独模块,也有属于自己模块的前缀树系统,只需要监听GVG模块的前缀树红点系统就可以 -- 不过也可以放在这里通知并监听,这样会让模块更加内聚 end FactionRedDotUtil.Init() return FactionRedDotUtilLuaAppFacade.getIns():SendNotification (NoticeConst. FACTION_MISSION_REWARD_REDDOT_CHANGE, value)是本项目中一种监听机制,读者可替换为其他。 -
红点值监听代码
此时,一个模块的红点系统已经构建好了,如果我们需要关注某一个红点的更新,只需要去监听这个红点改变回调发出的通知即可,比如主界面需要关注公会总红点更新,就需要去监听NoticeConst.FACTION_MAIN_REDDOT_CHANGE这个通知。公会界面需要关注任务总红点的更新,就需要去监听NoticeConst.FACTION_MISSION_REDDOT_CHANGE这个通知。---@class MainUI : LuaModule local MainUI = class("MainUI", LuaModule) function MainUI:ModuleAwake( ... ) self:SuperCall(MainUI, "ModuleAwake"); self.m_dicNoticeHandle[NoticeConst.FACTION_MAIN_REDDOT_CHANGE] = self.UpdateFactionPageRed end ---更新公会红点 function MianUI:UpdateFactionPageRedDot(value) value = value or RedDotManager.GetInstance():GetValue(FactionRedDotUtil.RedDotPathDic.Faction) self.btnFactionRedDot:SetActive(value > 0) end return MainUI ---@class Faction : LuaModule local Faction = class("Faction", LuaModule) function Faction:ModuleAwake( ... ) self:SuperCall(Faction, "ModuleAwake"); self.m_dicNoticeHandle[NoticeConst.FACTION_MISSION_REDDOT_CHANGE] = self.UpdateFactionMissionRedDot end ---更新公会任务红点 function Faction:UpdateFactionMissionRedDot(value) value = value or RedDotManager.GetInstance():GetValue(FactionRedDotUtil.RedDotPathDic.Mission) self.btnMissionRedDot:SetActive(value > 0) end return Faction -
红点值控制代码
红点系统和红点值监听都搭建好了以后,我们就要根据实际情况去实现更新红点值的代码,来使整个红点系统运作起来---@Controller : SimpleCommand local Controller = class("Controller", Puremvc.SimpleCommand) function Controller:Ctor() self.m_noticeHandle = { -- 这里只简单展示任务红点的更新 ---角色value值初始化 -- 由于主界面一上线就要显示各个模块的红点,此时需要根据角色信息来初始化红点系统的值,和主界面初始化刷新红点是一个道理 -- LOGIN_UPDATE_ROLE_ALL_VALUES 这个通知就是上线角色数据改变的通知 [NoticeConst.LOGIN_UPDATE_ROLE_ALL_VALUES] = function () self:UpdateMissionPointRedDot() self:UpdateMissionRedDot() end, [NoticeConst.VALUE_ROLE_FACTION_MISSION_AWARD_CHANGE] = self.UpdateMissionPointRedDot, [NoticeConst.VALUE_ROLE_FACTION_MISSION_POINT_CHANGE] = self.UpdateMissionPointRedDot, [NoticeConst.TASK_FINISH_AWARD_RET] = self.UpdateMissionRedDot, [NoticeConst.TASK_NOTIFY_DATA] = self.UpdateMissionRedDot, } end -- 任务积分红点值更新 function Controller:UpdateMissionPointRedDot() -- GameData.factionData.CheckMissionPointRewardReddot()是任务积分的红点检查函数 local value = GameData.factionData.CheckMissionPointRewardReddot() and 1 or 0 local redDotTreeNode = RedDotManager.GetInstance():GetTreeNode(FactionRedDotUtil.RedDotPathDic.MissionPointReward) if redDotTreeNode:GetChildCount() == 0 then -- 只有叶子结点可以改变红点值 redDotTreeNode:ChangeValue(value) end end -- 任务奖励红点值更新 function Controller:UpdateMissionRedDot() local value = GameData.factionData.CheckMissionRewardReddot() and 1 or 0 local redDotTreeNode = RedDotManager.GetInstance():GetTreeNode(FactionRedDotUtil.RedDotPathDic.MissionReward) if redDotTreeNode:GetChildCount() == 0 then -- 只有叶子结点可以改变红点值 redDotTreeNode:ChangeValue(value) end end return Controller
大功告成!此红点系统的优点就是易于使用,可以很方便的融入当前的项目,且大概率不和以前的旧红点系统产生冲突。如果是新项目,也可以不分模块来创建红点系统,直接一个创建一个总的红点系统管理所有红点。