背景

项目中后期需要支持热更新,因此只能把之前基于UGUI的C#外围逻辑全部修改为Lua,修改过程中的方案是做一个兼容处理,新逻辑用Lua,并在项目推进过程中逐步把外围都改为Lua编码。

项目中的接入实践

工程目录结构

XFramework/lua          -- C#端脚本根目录
------------------------------------------------------
luaScripts              -- Lua端脚本根目录
├── api                 -- 用于写代码时只能提示Api信息
├── cfg_sheet           -- 本地配置表对应的代码
├── common              -- 核心常用代码
├── libs                -- 第三方库 例如 protobuf,cjson...
├── proto               -- 协议对应代码
├── service             -- 数据层服务代码
├── ui                  -- UI代码
├── Global.lua          -- 集中管理和C#层交互的地方
└── LuaDebug.lua        -- 调试器

核心代码库

C#端

  1. LuaHelper.cs Lua的启动脚本并传递各种游戏状态事件。
  2. LuaStrHelper.cs Lua有部分字符串实现暂时没有找到Lua版本的,还包括C#的字符串格式化逻辑的转接,用以兼容现有配置。
  3. LuaMsgHelper.cs 既然是兼容方案,那么socket只能直接获取现有的C#端实例来中转。这里有一个待解决的问题,是Lua到C#的传参过程,并没有找到方便的传输byte[]的方法。临时使用了tolua示例中的LuaByteBuffer
  4. LuaResHelper.cs 资源的操作中转类,目前只加了PlayerPrefs,用到再添加。
  5. LuaUIHelper.cs UI中转类,打开关闭窗口等,因为之前的C#端用到了大量泛型,并不方便直接提供UIManager,所以这里短期内是会一直使用的。
  6. LuaObsoleteHelper.cs 这是一个终将会废弃的类,把一些暂时没有办法处理的中转逻辑都集中到这个类中,以后逐个修改到Lua端实现,最终完全删除该类。

    Lua端

  7. Global.lua Lua端的入口代码,与C#端直接交互(仅此一个)。
  8. Common/SandBox.lua 利用子类沙盒模式,给Service.luaUIView.lua提供一系列的基本操作,把代码中大量的全局变量调用集中到这个基类中,降低耦合度,同时方便维护。
  9. Common/Service.lua, Common/ServiceManager.lua 这两个类就是UI框架设计中提到的数据层的两个基础类了。
  10. Common/UIView.lua 是所有UI窗口和视图的基类。封装一系列简化操作的窗口API,分为获取组件、设置组件事件、属性绑定与自动解绑、窗口事件,以及一些常用的UI层操作。
  11. Common/UIPath.lua 这里是UI的资源路径字典,规范应该保证的是资源和代码同名,这样会减少后续维护的麻烦。
  12. Common/XPlayerPrefs.lua 本地的一个数据缓存。
  13. Common/XProperty.lua,Common/XListProperty.lua mvp架构的核心思想,单项数据绑定。
  14. Common/ConfigReader.lua 本地配置表的读取类,支持两种读取方式,数组和字典。
  15. Common/Collections/XList.lua 一个基本的list实现,内部是一个数组,未采用链表的形式是处于性能考虑。

还未执行的工作

  1. 生成获取组件的代码
  2. 自动拦截替换现有的C#端实现
  3. 代码模板

待探讨的做法

  1. class 目前大众一点的面向对象思路一共有三种。一种是metatable直接来(项目中目前就是如此,缺点是如果继承层级过多会有额外的metatable开销)。二是copy父类方法到子类中。三是执行时copy(参考云风的blog)。
  2. 配置表 目前参考了一些市面上的项目方案,直接将配置表导出成对应的代码。需要讨论的有两个点,一是解析配置表好还是直接就是代码好。二是代码中的重复关键词如何消除,目前考虑可以使用数组+metatable实现,还没有动手。
  3. 协议 目前协议使用了tolua自带的proto-gen-lua,缺陷是不支持[packed=true]字段,该字段可以减少列表长度n的字段 (n - 1) * 8bit的数据量。
  4. 全局变量 全局变量是否有额外的开销,开销是多少目前我还不是很清楚。因此,UIView等是否使用全局变量还需要进一步了解。

经验技巧

  1. 显示子视图时,需要判断该视图是一个全局的还是一个独立的实例
     -- 显示顶部菜单中的货币视图 因为各个界面都有该类,那么其应该是一个单独的实例 所以需要调用 new() 方法
     self:showView(CommonTopView_Coins:new(), self.mContextCoin)
    

    – 邮件详情界面这里就只有一个全局的实例了 并不需要也不应该去 new()
    local content = require(UIPath.MailWindow_ContentView)
    content.selectedMail = self.selectedMail
    self:showView(content, self.mUIContext_mail_content_not_empty_6)

    – 列表的显示每个子视图都是独立的实例 这里需要注意的是传参的方式 列表会有一个特殊字段 prefab 需要赋值
    local list = XList:new()
    for _,item in ipairs(self.items) do
    –@RefType [ui.Common.CommonItemView#CommonItemView]
    local cell = CommonItemViewPrefab:new()
    cell.item = item
    cell.couldClick = true
    cell.prefab = self.lstCell
    list:add(cell)
    end

    self.lstContainer:setLuaDatas(list)
    self.lstContainer:display()

  2. 代码提示的功能添加
     -- 最常用的应该这个引用标签了,这里直接声明了cfg变量的类型是对应配置表,方便写代码
     --@RefType [cfg_sheet.common_config.t_item_config#t_item_config]
     Item.cfg = nil
    

    – 这里同时指定了参数类型和返回值类型,方便调用
    – 获取指定id的道具信息
    –@itemId: 道具ID
    –@return [api.Item#Item]
    function BagService:getItem(itemId)
    local ret = self.items[itemId]
    if(ret == nil) then
    print(string.format("道具错误 %d => 找玥祺&海哥", itemId))
    end
    return ret
    end

  3. Public or Private 函数
     local M = BagService
     -- 外部会调用的逻辑直接用全局名称,这样写代码会有智能提示
     -- 出售道具
     function BagService:C2SSellItem(itemId, itemNum)
         local msg = MsgStruct:C2SSellItem()
         msg.itemID = itemId
         msg.itemNum = itemNum
         self:sendMain(NetMsgId.C2SSellItem, msg)
     end
    

    – 内部函数就用M打头,外部没有提示
    – 出售道具
    function M:S2CSellItem(msg)
    self:showErrorCode(msg.ret)
    end

项目规范

  1. 所有的变量,添加详细的注释,可以是英文
  2. 类型名称的每个单词首字母均大写
  3. 变量名和函数名均小写字母开头
  4. 常量名全大写或者k字母开头并在注释中声明
  5. 窗口代码和资源名称要匹配
  6. 窗口以Window,视图以View为后缀方便区分
  7. 一些公共的数据结构,到api目录下创建对应的接口提示 like Item
  8. 一些常用的数据,和方法,要是用标签指定其对应参数和返回值类型,方便大家写代码
  9. 方法一定要注意:.的区别,尽量少用.减少全局变量的调用,并且保持一致的:风格