刚进公司,公司做的手游用的是现在比较流行的ToLua框架,框架采用MVC模式设计,学习了一段时间,写一篇博文记录一下感受和心得
那么我们看一下MVC框架,以背包系统为例子
背包Model脚本:
local ModuleDataObject=require("Core/ModuleDataObject") local InventoryModel=class("InventoryModel",ModuleDataObject) function InventoryModel:ctor(...) InventoryModel.super.ctor(self,"InventoryModel"); end --通过ItemType找服务器对应的物品数据 function InventoryModel:GetServerDataByType(itemType) local data; if(itemType == EnumStatic.ItemType.Equip) then data = _G_DataObjectManager:GetDataObject(DataObjectSave.ArmInfo); elseif(itemType == EnumStatic.ItemType.Jewel) then data = _G_DataObjectManager:GetDataObject(DataObjectSave.GemInfo); elseif(itemType == EnumStatic.ItemType.Prop) then data = _G_DataObjectManager:GetDataObject(DataObjectSave.PropInfo); end return data; end --通过配置id和itemType查找对应item的数量 function InventoryModel:FindItemNumByConfigIdAndType(configId,itemType) local num; -- 不叠加 if itemType == EnumStatic.ItemType.Equip then --or itemType == EnumStatic.ItemType.Jewel then num = self:FindNoStackItemCountById(configId, itemType) -- 叠加 else num = self:FindStackItemCountById(configId, itemType) end return num; end -- 通过配置ID查找 对应不叠加物品的数量 function InventoryModel:FindNoStackItemCountById(configId, itemType) local data = self:GetServerDataByType(itemType); local items = SearchObjectsByKeyValue(data, "config.id", configId); local num = #items return num end -- 通过配置ID查找 对应叠加物品的数量 function InventoryModel:FindStackItemCountById(configId, itemType) local data = self:GetServerDataByType(itemType); local items = SearchObjectsByKeyValue(data, "config.id", configId); local num = 0 for key, var in ipairs(items) do num = num + var.count end return num end --通过ItemType和实例id获得该实例Item function InventoryModel:FindItemByIdAndType(id,itemType) local data = self:GetServerDataByType(itemType); return SearchObjectByKeyValue(data,"id",id); end --通过实例id获得Item --尽量使用FindItemByIdAndType方法 function InventoryModel:FindItemById(id) local result = self:FindItemByIdAndType(EnumStatic.ItemType.Prop,id); if(result ~= nil) then return result; end result = self:FindItemByIdAndType(EnumStatic.ItemType.Jewel,id); if(result ~= nil) then return result; end result = self:FindItemByIdAndType(EnumStatic.ItemType.Equip,id); if(result ~= nil) then return result; end return nil; end --通过ItemType和subId获取对应ItemList --subId可选 function InventoryModel:FindItemsByItemTypeAndSubId(itemType,subId) local data = self:GetServerDataByType(itemType); if(subId == nil) then return data; else return SearchObjectsByKeyValue(data,"config.subType",subId); end end function InventoryModel:SortEquipData(equipData) function FCSequence(a,b) return _G_ConfigManager:FightNum(a) > _G_ConfigManager:FightNum(b) end function GradeSequence(a,b) return a.config.grade > b.config.grade end table.sort( equipData,FCSequence) table.sort( equipData,GradeSequence) return equipData end --获得InventoryView/MainRect的数据(移到UIHelper?) function InventoryModel:GetInventoryMainRectData() --[[ 排序规则: 全部 时间(新的在前面) 装备 装备碎片在装备前 品质/subType 装备碎片 品质/subType(好的在前面,subType小的在前面) 武将碎片 品质/subType 宝石 品质/等级/subType 消耗品 宝箱类/材料类 宝箱 品质/subType 材料 品质/subType ]]-- local allProp = _G_DataObjectManager:GetDataObject(DataObjectSave.PropInfo); local allEquip = _G_DataObjectManager:GetDataObject(DataObjectSave.ArmInfo); local allGems = _G_DataObjectManager:GetDataObject(DataObjectSave.GemInfo); if(allProp == nil) then allProp = {} end; if(allEquip == nil) then allEquip = {} end; if(allGems == nil) then allGems = {} end; --需要过滤已镶嵌宝石 allGems = SearchObjectsByKeyValue(allGems,"equip","") --需要过滤已装备的装备 allEquip = SearchObjectsByKeyValue(allEquip,"equip",0); --需要过滤已羁绊的装备 allEquip = SearchObjectsByKeyValue(allEquip, "beRelation", false); -- 对装备按战力和品质进行排序 --allEquip = self:SortEquipData(allEquip) local heroFragment = SearchObjectsByKeyValue(allProp,"config.subType",610); local equipFragment = SearchObjectsByKeyValue(allProp,"config.subType",620); local consumable = SearchObjectsByKeyValue(allProp,"config.subType",610,false); consumable = SearchObjectsByKeyValue(consumable,"config.subType",620,false); local chest = SearchObjectsByKeyValue(consumable,"config.subType",800); local material = SearchObjectsByKeyValue(consumable,"config.subType",800,false); -- 普通排序 local compare = function(n1,n2) if(n1.config.quality == n2.config.quality) then return n1.config.subType < n2.config.subType; else return n1.config.quality > n2.config.quality; end end; -- 装备排序 local equipCompare = function(n1,n2) if(n1.config.quality == n2.config.quality) then if n1.config.subType == n2.config.subType then return n1.config.grade > n2.config.grade; else return n1.config.subType < n2.config.subType; end else return n1.config.quality > n2.config.quality; end end; --consumable self.consumable = {}; table.sort(chest,compare); table.sort(material,compare); for i=1,#chest do table.insert(self.consumable, chest[i]); end for i=1,#material do table.insert(self.consumable, material[i]); end --heroFragment self.heroFragment = heroFragment; table.sort(self.heroFragment,compare); --jewel self.jewel = {}; for i=1,#allGems do table.insert(self.jewel, allGems[i]); end table.sort(self.jewel,function(n1,n2) if(n1.config.quality == n2.config.quality) then if(n1.level == n2.level) then return n1.config.subType < n2.config.subType; else return n1.level > n2.level; end else return n1.config.quality > n2.config.quality; end end); --equip self.equip = {}; table.sort(equipFragment,compare); table.sort(allEquip,equipCompare); for i=1,#equipFragment do table.insert(self.equip, equipFragment[i]); end for i=1,#allEquip do table.insert(self.equip, allEquip[i]); end --All self.all = {}; for i=1,#self.heroFragment do table.insert(self.all,self.heroFragment[i]); end for i=1,#self.equip do table.insert(self.all,self.equip[i]); end for i=1,#self.jewel do table.insert(self.all,self.jewel[i]); end for i=1,#self.consumable do table.insert(self.all,self.consumable[i]); end --按照时间由大到小 table.sort(self.all,function(n1,n2) return n1.createTime > n2.createTime; end); end function InventoryModel:Dispose() end return InventoryModel
背包Model这个脚本,继承自ModuleDataObject这个脚本,也就是管理所有Model的脚本。在背包Model脚本中,可以定义一些背包系统需要的常量、变量或者一些配置数据的方法,供Controller和View来调用,通过传入对应的物品ID和物品类型,获取物品的详细数据
我们再来看一下背包Controller:
local ModuleCtrl = require("Core/ModuleCtrl") local InventoryCtrl = class("InventoryCtrl",ModuleCtrl) local InventoryView = require("Module/Inventory/InventoryView") local InventoryModel=require ("Module/Inventory/InventoryModel") function InventoryCtrl:ctor() InventoryCtrl.super.ctor(self,"inventoryCtrl") if(self.model == nil) then self.model = InventoryModel.New(); end end function InventoryCtrl:EventInit() _G_Dispatcher:AddPropertyEvent(UIStatic.UI_SHOW_INVENTORY,InventoryCtrl.OpenUI,self) _G_Dispatcher:AddPropertyEvent(UIStatic.ITEM_CHANGE,InventoryCtrl.ItemChange,self) _G_Dispatcher:AddPropertyEvent(UIStatic.UI_SHOW_INVENTORY_SELECT_PANEL,InventoryCtrl.OpenSelectPanel,self) end function InventoryCtrl:RemoveEvent() _G_Dispatcher:RemovePropertyEvent(UIStatic.UI_SHOW_INVENTORY,InventoryCtrl.OpenUI,self) _G_Dispatcher:RemovePropertyEvent(UIStatic.ITEM_CHANGE,InventoryCtrl.ItemChange,self) _G_Dispatcher:RemovePropertyEvent(UIStatic.UI_SHOW_INVENTORY_SELECT_PANEL,InventoryCtrl.OpenSelectPanel,self) end function InventoryCtrl:SocketInit() _G_Socket:AddPropertyEvent(rpc_info.rpc_dict.openChest, InventoryCtrl.OnOpenChest, self) _G_Socket:AddPropertyEvent(rpc_info.rpc_dict.openAllChest, InventoryCtrl.OnOpenChest, self) end function InventoryCtrl:RemoveSocket() _G_Socket:RemovePropertyEvent(rpc_info.rpc_dict.openChest, InventoryCtrl.OnOpenChest, self) _G_Socket:RemovePropertyEvent(rpc_info.rpc_dict.openAllChest, InventoryCtrl.OnOpenChest, self) end function InventoryCtrl:OpenUI() self.model:GetInventoryMainRectData(); self.ui = InventoryView.New(EnumStatic.InventoryOpenMode.Normal); self.ui.dispatcher:AddPropertyEvent("Dispose", InventoryCtrl.ViewDispose, self) self.ui.dispatcher:AddPropertyEvent("SendOpenChest", InventoryCtrl.SendOpenChest, self) self.ui.dispatcher:AddPropertyEvent("SendOpenAllChest", InventoryCtrl.SendOpenAllChest, self) self.ui:Create(); end function InventoryCtrl:ViewDispose() self.ui.dispatcher:RemovePropertyEvent("Dispose",InventoryCtrl.ViewDispose,self) self.ui.dispatcher:RemovePropertyEvent("SendOpenChest",InventoryCtrl.SendOpenChest,self) self.ui.dispatcher:RemovePropertyEvent("SendOpenAllChest",InventoryCtrl.SendOpenAllChest,self) self.ui=nil end --打开选择面板 --当前data格式:{itemType,subId(可选),targetId} function InventoryCtrl:OpenSelectPanel(event,data) local allData = {}; local modeType; if(data.itemType == EnumStatic.ItemType.Equip) then --装备 allData.selectPanelData = self.model:FindItemsByItemTypeAndSubId(data.itemType,data.subId); allData.targetId = data.targetId; allData.selectPanelData = SearchObjectsByKeyValue(allData.selectPanelData,"equip",0); allData.selectPanelData = SearchObjectsByKeyValue(allData.selectPanelData, "beRelation", false); allData.selectPanelData = _G_DataObjectManager.ins.InventoryModel:SortEquipData(allData.selectPanelData) allData.currEquipFightScore = data.FightScore modeType = EnumStatic.InventorySelectType.Equip; elseif(data.itemType == EnumStatic.ItemType.Jewel) then --镶嵌 allData.selectPanelData = data.gemData allData.targetId = data.targetId allData.pos = data.pos modeType = EnumStatic.InventorySelectType.Gem; elseif(data.itemType == EnumStatic.ItemType.Prop) then --其他,马魂? end self.ui = InventoryView.New(EnumStatic.InventoryOpenMode.SelectPanel, modeType, allData); self.ui.bgMaskMode = UIBGMaskStatic.NORMAL; self.ui.dispatcher:AddPropertyEvent("Dispose", InventoryCtrl.ViewDispose, self) self.ui.dispatcher:AddPropertyEvent("SendOpenChest", InventoryCtrl.SendOpenChest, self) self.ui.dispatcher:AddPropertyEvent("SendOpenAllChest", InventoryCtrl.SendOpenAllChest, self) self.ui:Create(); end --收到物品改变信息后,调用InventoryModel 函数更新数据,再重新刷新Rect function InventoryCtrl:ItemChange() if(self.ui ~= nil) then self.model:GetInventoryMainRectData(); self.ui:ToggleChange(); end end -- 发送开宝箱 function InventoryCtrl:SendOpenChest(eventType, instId) local rpcid = rpc_info.rpc_dict.openChest local data = { ["instId"] = instId, ["count"] = 1, } Network.SendMessage(rpcid, data) end -- 发送一键开宝箱 function InventoryCtrl:SendOpenAllChest(eventType) local rpcid = rpc_info.rpc_dict.openAllChest local data = {} Network.SendMessage(rpcid, data) end -- 响应开宝箱 function InventoryCtrl:OnOpenChest(eventType, data) local status = data["status"] print("状态码:"..status) if status == "ok" then local items = data["protos"] CommonShowGetItemsPanel(items) else if status == "invalid_param" then CommonTips("宝箱开启失败") end end end function InventoryCtrl:Dispose() end return InventoryCtrl
我们可以看到,关于事件的添加和移除,是在Controller层定义的,AddPropertyEvent和RemovePropertyEvent是我们在内部封装好的添加及移除的方法,以及物品信息发生改变后,背包刷新,所有逻辑层需要处理的方法,是要放在这里的。
背包View层因为代码量太大,所以只贴出一小段:
function InventoryView:InitializeScrollRect(go,index) --强化 local enhanceData = self.EquipDetailItemData[index + 1]["enhance"]; --标题 local titleData = self.EquipDetailItemData[index + 1]["title"]; --宝石 local gemsData = self.EquipDetailItemData[index + 1]["gems"]; --空行 local emptyData = self.EquipDetailItemData[index + 1]["empty"]; --宝石详情 local gemInfoData = self.EquipDetailItemData[index + 1]["gemInfo"]; local enhance = go.transform:FindChild("EquipEnhance").gameObject; local gems = go.transform:FindChild("EquipStoneImage").gameObject; local title = go.transform:FindChild("Title").gameObject; local titleText = go.transform:FindChild("Title/TitleText"):GetComponent("Text"); --hide enhance:SetActive(false); gems:SetActive(false); title:SetActive(false); titleText.gameObject:SetActive(false); titleText.text = ""; if(titleData ~= nil) then title:SetActive(true); titleText.gameObject:SetActive(true); titleText.text = titleData; elseif(enhanceData ~= nil) then enhance:SetActive(true); enhance.transform:FindChild("EquipEnhanceText"):GetComponent("Text").text = enhanceData.name; enhance.transform:FindChild("EquipEnhanceValueText"):GetComponent("Text").text = enhanceData.value; elseif(gemsData ~= nil) then gems:SetActive(true); gems.transform:FindChild("StoneText"):GetComponent("Text").text = gemsData.name; gems.transform:FindChild("StoneValueText"):GetComponent("Text").text = "+"..gemsData.value; self:AtlasSprite(gems,UIHelper:GetItemImage(gemsData.icon)) elseif(emptyData ~= nil) then --empty elseif(gemInfoData ~= nil) then enhance:SetActive(true); enhance.transform:FindChild("EquipEnhanceText"):GetComponent("Text").text = gemInfoData.name; enhance.transform:FindChild("EquipEnhanceValueText"):GetComponent("Text").text = gemInfoData.value; end end ----以数据作为参数,更新显示Detail内容 function InventoryView:ShowDetail(data) self.agingInfo:SetActive(false) self.systemAgingInfo:SetActive(false) self:SetItemCDTimer() --数据 local itemType = data.itemType; local subType = data.config.subType; local itemId = data.protoId; local instanceId = data.id; local DetailName = self.animationContentTF:FindChild("DetailPanel/DetailName"):GetComponent("Text"); local DetailNameEquipPart = self.animationContentTF:FindChild("DetailPanel/EquipPart"); local DetailItemPart = self.animationContentTF:FindChild("DetailPanel/ItemPart"); local DetailStonePart = self.animationContentTF:FindChild("DetailPanel/StonePart"); local FragmentImage = self.animationContentTF:FindChild("DetailPanel/DetailItem/FragmentImage").gameObject; UIHelper:ActiveFragment(subType,FragmentImage); local plotImage = self.animationContentTF:FindChild("DetailPanel/DetailItem/PlotImage"); self:AtlasSprite(plotImage, UIHelper:GetItemFrame(data.config.quality)); local detailImage = self.animationContentTF:FindChild("DetailPanel/DetailItem/ItemImage"); self:AtlasSprite(detailImage,UIHelper:GetItemImage(data.config.icon)); local redImage = self.animationContentTF:FindChild("DetailPanel/DetailItem/Red").gameObject; redImage:SetActive(false); local DetailItemCount = self.animationContentTF:FindChild("DetailPanel/DetailItem/ItemCount"):GetComponent("Text"); DetailItemCount.text = ""; local configData; if(itemType == EnumStatic.ItemType.Equip) then self.ScrollRect:SetActive(true); self.TextRect:SetActive(false); DetailNameEquipPart.gameObject:SetActive(true); DetailItemPart.gameObject:SetActive(false); DetailStonePart.gameObject:SetActive(false); local uiRect = self.ScrollRect.transform:FindChild("Viewport"):GetComponent("UIRectContent"); self.EquipDetailItemData = UIHelper:GetEquipRect(data); uiRect.onInitializeItem = UIRectContent.OnInitializeItem(function(go,index)self:InitializeScrollRect(go,index) end) uiRect:Init(#self.EquipDetailItemData); DetailNameEquipPart:FindChild("FightNum"):GetComponent("Text").text = _G_ConfigManager:FightNum(data); DetailNameEquipPart:FindChild("RankText"):GetComponent("Text").text = string.format("品级 <size=30>%d</size>",data.config.grade); configData = _G_ConfigManager:GetEquipDataById(itemId); elseif(itemType == EnumStatic.ItemType.Jewel) then --显示下一等级 当前等级等 self.ScrollRect:SetActive(true); self.TextRect:SetActive(false); DetailNameEquipPart.gameObject:SetActive(false); DetailItemPart.gameObject:SetActive(false); DetailStonePart.gameObject:SetActive(true); --设置实例数据 DetailItemCount.text = string.format("<color=#25AA28FF>+%d</color>",data.level); DetailStonePart:FindChild("StoneSlider"):GetComponent("Slider").value = data.exp/data.config.needExp; DetailStonePart:FindChild("StoneSlider/StoneText"):GetComponent("Text").text = string.format("%d/%d",data.exp, data.config.needExp); configData = _G_ConfigManager:GetJewelsDataById(itemId); local uiRect = self.ScrollRect.transform:FindChild("Viewport"):GetComponent("UIRectContent"); self.EquipDetailItemData = UIHelper:GetDetailJewelRect(data); uiRect.onInitializeItem = UIRectContent.OnInitializeItem(function(go,index)self:InitializeScrollRect(go,index) end) uiRect:Init(#self.EquipDetailItemData); --subType判断要放在对应大类之前 elseif(subType == EnumStatic.SubType.EquipFragment or subType == EnumStatic.SubType.HeroFragment) then self.ScrollRect:SetActive(false); DetailNameEquipPart.gameObject:SetActive(false); DetailItemPart.gameObject:SetActive(true); DetailStonePart.gameObject:SetActive(false); DetailItemPart:FindChild("ItemCount"):GetComponent("Text").text = string.format("拥有<color=#25AA28FF><size=30> %d </size></color>件",data.count); configData = _G_ConfigManager:GetItemDataById(itemId); self:SetTextRect(configData["desc"]); elseif(itemType == EnumStatic.ItemType.Prop) then self.ScrollRect:SetActive(false); DetailNameEquipPart.gameObject:SetActive(false); DetailItemPart.gameObject:SetActive(true); DetailStonePart.gameObject:SetActive(false); DetailItemPart:FindChild("ItemCount"):GetComponent("Text").text = string.format("拥有<color=#25AA28FF><size=30> %d </size></color>件",data.count); configData = _G_ConfigManager:GetItemDataById(itemId); self:SetTextRect(configData["desc"]); local cdType = configData["cdType"] -- 时效性 if cdType == 2 then self.agingInfo:SetActive(true) local SetItemCDFunc = function() -- 道具的时效性 local cdTime = configData["cdTime"] -- 道具获取时间 local createTime = data.createTime -- 当前时间 local currentTime = _G_TimerManager:GetServerTime() -- 从获取到现在的间隔时间 local intervalTime = currentTime - createTime -- 道具剩余时间 local remainTime = cdTime - intervalTime -- 时效已到 if intervalTime >= cdTime then local txt = "0小时" self.agingInfoText.text = txt -- 时效未到 else local txt = GetIntervalTimeHour(remainTime) txt = string.format("%s小时", txt) self.agingInfoText.text = txt end end SetItemCDFunc() self:SetItemCDTimer(SetItemCDFunc) -- 系统时效性 elseif cdType == 3 then self.systemAgingInfo:SetActive(true) local txt = configData["cdTime"] local l = string.sub(txt, 1, 4) local y = string.sub(txt, 6, 7) local r = string.sub(txt, 9, 10) txt = string.format("%s年%s月%s日", l, y, r) self.systemAgingInfoText.text = txt end end DetailName.text = configData["name"]; DetailName.color = QualityColor[configData["quality"]]; if(self.currentMode == EnumStatic.InventoryOpenMode.SelectPanel) then self:SetSelectItemBtn(); self:CheckSellAble(0, configData["sellPrice"]); elseif(self.currentMode == EnumStatic.InventoryOpenMode.Normal) then self:SetDetailBtn(configData["leftButton"], configData["rightButton"], data); self:CheckSellAble(configData["canSell"], configData["sellPrice"]); end end
背包View界面,也就是用户显示界面,是和用户最密切相关的脚本,View层的逻辑基本是,先找到要赋值的对象,判断不同的物品类型,获取美术资源的图片,特效,或者其他Config文件的table数据,给UI赋值,其中,我们内部有封装好AtlasSprite、GetItemFrame、GetItemImage等方法,方便图片赋值及根据资源名称获取对应文件夹下的资源。
总的来说,目前项目的框架非常好,结构分明,MVC框架咋看之下,会让代码量增加,但是它低耦合的特性可以让项目在后期维护的时候,修改Bug或增加新功能的效率提高很多。