初学ToLua框架,浅谈对框架及MVC的理解

刚进公司,公司做的手游用的是现在比较流行的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或增加新功能的效率提高很多。


你可能感兴趣的:(Unity,ToLua,MVC)