刚进公司,公司做的手游用的是现在比较流行的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("品级 %d ",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("+%d ",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("拥有 %d 件",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("拥有 %d 件",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或增加新功能的效率提高很多。