首页 > 技术文章 > 热更新实践--xlua实现背包面板逻辑学习笔记

movin2333 原文

一.简介

  对于xlua热更新项目来说,如果是已经使用C#做好的项目,可以使用hotfix补丁的形式对C#方法进行覆盖,使项目具备热更新功能,但是如果是没有开始实现的项目,要想具备热更新功能,完全使用lua实现可能更加方便维护。从另一个方面来说,一个项目如果全部使用lua实现游戏逻辑,游戏的运行速度肯定是有折扣的,所以具体的选择还是要根据实际需求调整。

  这几天集中学习了从lua语法到xlua的应用等各种知识,对于热更新的实现有了大致的了解,尤其是今天,学习了使用xlua实现一个游戏背包逻辑,对xlua如何实现游戏又加深了了解,但是距离能够熟练使用还需要接下来勤加练习。现在主要是对于今天学习的xlua背包的总结和逻辑梳理。

二.xlua代码分类粘贴

  1.lua主入口Main.lua(相当于游戏逻辑管理类或者C#工程中的Main方法类),这个lua脚本负责执行其他lua脚本,Unity中只需要任意位置挂载一个用于执行这个lua脚本的C#脚本即可,其他都交由lua实现。

print("main.lua 启动")
--lua初始化
require("InitClass")
--读取item列表
require("Item")
--读取玩家信息
--从服务器读取(一般网络游戏)或者从本地读取(单机游戏本地序列化的信息)
require("Player")

--显示主面板
require("BasePanel")
require("MainPanel")
require("BagPanel")
require("ItemGrid")
MainPanel:ShowMe("MainPanel")

  2.lua初始化类InitClass.lua,这个lua脚本负责初始化项目中使用的初始的全局变量,如为Unity的各种API取别名方便调用,执行一些lua工具脚本等。

print("InitClass.lua启动")
--常用别名都在这里定位

--准备之前导入的脚本
--面向对象相关
require("Object")
--字符串拆分
require("SplitTools")
--Json解析
Json = require("JsonUtility")

--Unity相关
GameObject = CS.UnityEngine.GameObject
Resources = CS.UnityEngine.Resources
Transform = CS.UnityEngine.Transform
RectTransform = CS.UnityEngine.RectTransform
TextAsset = CS.UnityEngine.TextAsset
--图集对象类
SpriteAtlas = CS.UnityEngine.U2D.SpriteAtlas

Vector3 = CS.UnityEngine.Vector3
Vector2 = CS.UnityEngine.Vector2

--UI相关
UI = CS.UnityEngine.UI
Image = UI.Image
Text = UI.Text
Button = UI.Button
Toggle = UI.Toggle
ScrollRect = UI.ScrollRect
UIBehaviour = CS.UnityEngine.EventSystems.UIBehaviour

--用于设置父类的,需要找到实例
Canvas = GameObject.Find("Canvas").transform

--自定义的相关C#脚本
AssetBundleManager = CS.AssetBundleManager.Instance

  3.背包物品脚本Item.lua,这个脚本负责从json文件中读取物品列表并存储为全局变量,方便调用,使用物品时直接从全局变量中读取物品信息再从AB包中读取相应的文件,当然热更新时物品的资源、json文件和lua代码一并打包。

print("Item.lua启动")
--在这里读取物品数据

--加载json
local txt = AssetBundleManager:LoadRes("json","Item",typeof(CS.UnityEngine.TextAsset))
--读取json,得到一个table组成的list
local itemList = Json.decode(txt.text)
--table组成的list不方便使用,使用表将其转存方便使用
ItemData = {}
for _, value in pairs(itemList) do
    ItemData[value.id] = value
end

  4.玩家信息脚本Player.lua,这个脚本负责实例化玩家信息,同样存储到全局变量中方便使用。如果是单机游戏,玩家信息可以存储到本地,但是对于网络游戏,玩家信息一般存储在服务器上,这里为了方便直接新建的玩家信息。

print("Player.lua启动")
--目前只做背包功能,只需要道具信息即可
--这里为了测试方便,直接实例化一个Player,实际情况是需要从服务器或者本地读取的
Player = {}
Player.equips = {}
Player.items = {}
Player.gems = {}

--提供一个初始化方法,方便修改
function Player:Init()
    --道具信息,存储的时候,一定不是把整个道具信息存储进去,一般存储道具的ID和数量即可
    --目前没有服务器,为方便测试这里写死了
    table.insert(self.equips,{id = 1,num = 1})
    table.insert(self.equips,{id = 2,num = 1})

    table.insert(self.items,{id = 3,num = 50})
    table.insert(self.items,{id = 4,num = 20})

    table.insert(self.gems,{id = 5,num = 99})
    table.insert(self.gems,{id = 6,num = 88})
end

Player:Init()

  5.“万物之父”Object.lua,这个脚本提供了基本的继承和new方法,所有的游戏物体基类都需要继承自这个脚本,相当于Unity中的Object类。

--面向对象实现 
--万物之父 所有对象的基类 Object
--封装
Object = {}
--实例化方法
function Object:new()
    local obj = {}
    --给空对象设置元表 以及 __index
    self.__index = self
    setmetatable(obj, self)
    return obj
end
--继承
function Object:subClass(className)
    --根据名字生成一张表 就是一个类
    _G[className] = {}
    local obj = _G[className]
    --设置自己的“父类”
    obj.base = self
    --给子类设置元表 以及 __index
    self.__index = self
    setmetatable(obj, self)
end

  5.面板基类MainPanel.lua,面板类是基于面向对象的思想制作,使用lua制作面板一般将面板信息存储到一个全局变量表中,对于所有面板而言,在表中都需要持有面板的相关控件、提供显隐面板的方法等,所以提供一个面板基类来完成这些共同的东西。面板基类继承自Object。

--继承Object
Object:subClass("BasePanel")

BasePanel.panelObj = nil
--使用一个表存储控件
BasePanel.controls = {}
--是否已经初始化
BasePanel.isInitEvent = false

function BasePanel:Init(name)
    if self.panelObj == nil then
        --公共的实例化对象的方法
        self.panelObj = AssetBundleManager:LoadRes("UI",name,typeof(GameObject))
        self.panelObj.transform:SetParent(Canvas,false)
        --找控件
        local allControls = self.panelObj:GetComponentsInChildren(typeof(UIBehaviour))

        for i=0,allControls.Length-1 do
            local controlName = allControls[i].name
            if string.find(controlName,"Btn") ~= nil or
                string.find(controlName,"Tog") or
                string.find(controlName,"Img") or
                string.find(controlName,"SV") or
                string.find(controlName,"Txt") then
                    local typeName = allControls[i]:GetType().Name
                    if self.controls[controlName] ~= nil then
                        self.controls[allControls[i].name][typeName] = allControls[i]
                    else
                        self.controls[controlName] = {[typeName] = allControls[i]}
                    end
            end
        end
    end
end

--得到控件的方法
function BasePanel:GetControl(name,typeName)
    if self.controls[name] ~= nil then
        local sameNameControls = self.controls[name]
        if sameNameControls[typeName] ~= nil then
            return sameNameControls[typeName]
        end
    end
    return nil
end

--显隐方法
function BasePanel:ShowMe(name)
    self:Init(name)
    self.panelObj:SetActive(true)
end
function BasePanel:HideMe()
    self.panelObj:SetActive(false)
end

  6.主面板MainPanel.lua,这个脚本负责主UI面板的相关逻辑,面板继承自BasePanel,已经持有控件和提供了显隐方法,面板只需要书写自身逻辑即可,如为自己身上的相关控件添加监听方法,如果有必要的话可以对BasePanel的一些方法进行重写。

print("MainPanel.lua启动")
--继承
BasePanel:subClass("MainPanel")

--提供一个初始化方法,实例化对象,监听控件等
function MainPanel:Init(name)
    self.base.Init(self,name)
    if(not self.isInitEvent) then
        self:GetControl("BtnRole","Button").onClick:AddListener(function()
            self:BtnRoleClick()
        end)

        self.isInitEvent = true
    end
end

function MainPanel:BtnRoleClick()
    BagPanel:ShowMe("BagPanel")
end

  7.背包面板BagPanel.lua,这个脚本同样继承自BasePanel,已经持有控件和提供了显隐方法,只需要书写自身逻辑即可。这里这个面板重写了Init方法和ShowMe方法,并注册了toggle的监听方法(这些都是根据面板书写的逻辑,根据实际的面板不同而不同,但是思路可以是相似的)。在背包面板中,根据玩家的数据和当前背包面板上Toggle控件的是否被点选的数据从刚才读取好的物品表中读取应该显示的物品信息,然后根据取出的物品信息从AB包中取出物品的Sprite等,从AB包中取出用于显示物品的prefab,将物品的sprite等赋值并实例化prefab即可。

print("BagPanel.lua启动")
--继承
BasePanel:subClass("BagPanel")

BagPanel.content = nil
--存储当前显示的格子
BagPanel.items = {}
--当前页签编号
BagPanel.nowType = -1

--初始化方法
function BagPanel:Init(name)
    self.base.Init(self,name)
    if not self.isInitEvent then
        self.content = self:GetControl("SVBag","ScrollRect").transform:Find("Viewport"):Find("Content")

        self:GetControl("BtnClose","Button").onClick:AddListener(function()
            self:HideMe()
        end)
        self:GetControl("TogEquip","Toggle").onValueChanged:AddListener(function(value)
            if value then
                self:ChangeType(1)
            end
        end)
        self:GetControl("TogItem","Toggle").onValueChanged:AddListener(function(value)
            if value then
                self:ChangeType(2)
            end
        end)
        self:GetControl("TogGem","Toggle").onValueChanged:AddListener(function(value)
            if value then
                self:ChangeType(3)
            end
        end)
        self.isInitEvent = true
    end
end

--显隐方法
function BagPanel:ShowMe(name)
    self.base.ShowMe(self,name)
    --初次显示面板时显示数据
    if self.nowType == -1 then
        self:ChangeType(1)
    end
end

--切页签的逻辑
--type 显示的页签种类:1-装备,2-道具,3-宝石
function BagPanel:ChangeType(type)
    --如果已经是当前页签,就不再更新
    if self.nowType == type then
        return
    else
        self.nowType = type
    end
    --遍历之前清除格子数据
    for i=1,#self.items do
        self.items[i]:Destroy()
    end
    self.items = {}

    --根据页签种类确定数据
    local nowItems = nil
    if type == 1 then
        nowItems = Player.equips
    elseif type == 2 then
        nowItems = Player.items
    elseif type == 3 then
        nowItems = Player.gems
    end

    --遍历创建格子
    for i = 1,#nowItems do
        --创建一个格子对象
        local grid = ItemGrid:new()
        --实例化
        grid:Init(self.content,(i-1)%4 * 180,math.floor((i-1)/4) * 180)
        --初始化信息
        grid:InitData(nowItems[i])
        
        --存储到容器中
        table.insert(self.items,grid)
    end
end

  8.物品显示的prefab对应脚本ItemGrid.lua,这个脚本对应用于显示物品的格子prefab,继承自Object(最好继承自BasePanel,可以将每一个格子都视为一个小panel),所以需要自己持有控件并提供初始化等方法。

print("ItemGrid.lua启动")
--继承Object
Object:subClass("ItemGrid")
--格子的控件
ItemGrid.obj = nil
ItemGrid.imgIcon = nil
ItemGrid.Text = nil
--初始化格子
function ItemGrid:Init(father,posX,posY)
    self.obj = AssetBundleManager:LoadRes("UI","ItemGrid")
    --设置父对象
    self.obj.transform:SetParent(father,false)
    --设置位置
    self.obj.transform.localPosition = Vector3(posX,posY,0)
    --设置图标和数量
    self.imgIcon = self.obj.transform:Find("Icon"):GetComponent(typeof(Image))
    self.Text = self.obj.transform:Find("Text"):GetComponent(typeof(Text))
end
--根据信息初始化格子信息
function ItemGrid:InitData(d)
    local data = ItemData[d.id]
    local strs = string.split(data.icon,"_")
    local spriteAtlas = AssetBundleManager:LoadRes("UI",strs[1],typeof(SpriteAtlas))
    self.imgIcon.sprite = spriteAtlas:GetSprite(strs[2])
    self.Text.text = d.num
end

--删除方法
function ItemGrid:Destroy()
    GameObject.Destroy(self.obj)
    self.obj = nil
end

三.总结

  今天的实践学习下来,谈谈感悟:

    1.xlua的执行是单线的,文件读取到哪里执行到哪里

    2.有需要提供给其他脚本引用的变量存储为全局变量,不需要提供出去的变量存储为local变量

    3.lua使用表去描述游戏,一个panel可以是一个表,一个游戏角色同样可以是一个表,一个游戏中需要和玩家交互的物体同样可以是一个表

    4.使用一个lua文件去实例化3中提到的这个表,存储为全局变量,在表中存储如控件、重要的Transform等信息和提供给外部的方法等等,如游戏中有一个坦克,我们可以使用一个表去对应它,表中存储坦克的prefab实例,还可以存储坦克的开炮位置的Transform、坦克上半部分旋转的Transform等,然后再存储一些方法执行开炮、上半部分旋转等行为

    5.这里的这个表其实是在模仿类的实现

    6.使用一个Main脚本对所有lua脚本进行管理

    7.使用一个Init脚本进行常用别名的存储和工具类等的初始化操作

    8.使用lua实现继承非常重要,对于一些同类型的物品(如各种panel),可以将其中的一些公共部分抽象出来做成父类,有利于简化代码和清晰逻辑

推荐阅读