Lua初体验
背景
项目中后期需要支持热更新,因此只能把之前基于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#端
LuaHelper.cs
Lua的启动脚本并传递各种游戏状态事件。LuaStrHelper.cs
Lua有部分字符串实现暂时没有找到Lua版本的,还包括C#的字符串格式化逻辑的转接,用以兼容现有配置。LuaMsgHelper.cs
既然是兼容方案,那么socket只能直接获取现有的C#端实例来中转。这里有一个待解决的问题,是Lua到C#的传参过程,并没有找到方便的传输byte[]
的方法。临时使用了tolua示例中的LuaByteBuffer
。LuaResHelper.cs
资源的操作中转类,目前只加了PlayerPrefs
,用到再添加。LuaUIHelper.cs
UI中转类,打开关闭窗口等,因为之前的C#端用到了大量泛型,并不方便直接提供UIManager
,所以这里短期内是会一直使用的。LuaObsoleteHelper.cs
这是一个终将会废弃的类,把一些暂时没有办法处理的中转逻辑都集中到这个类中,以后逐个修改到Lua端实现,最终完全删除该类。Lua端
Global.lua
Lua端的入口代码,与C#端直接交互(仅此一个)。Common/SandBox.lua
利用子类沙盒模式,给Service.lua
和UIView.lua
提供一系列的基本操作,把代码中大量的全局变量调用集中到这个基类中,降低耦合度,同时方便维护。Common/Service.lua
,Common/ServiceManager.lua
这两个类就是UI框架设计中提到的数据层的两个基础类了。Common/UIView.lua
是所有UI窗口和视图的基类。封装一系列简化操作的窗口API,分为获取组件、设置组件事件、属性绑定与自动解绑、窗口事件,以及一些常用的UI层操作。Common/UIPath.lua
这里是UI的资源路径字典,规范应该保证的是资源和代码同名,这样会减少后续维护的麻烦。Common/XPlayerPrefs.lua
本地的一个数据缓存。Common/XProperty.lua
,Common/XListProperty.lua
mvp架构的核心思想,单项数据绑定。Common/ConfigReader.lua
本地配置表的读取类,支持两种读取方式,数组和字典。Common/Collections/XList.lua
一个基本的list实现,内部是一个数组,未采用链表的形式是处于性能考虑。
还未执行的工作
- 生成获取组件的代码
- 自动拦截替换现有的C#端实现
- 代码模板
待探讨的做法
- class 目前大众一点的面向对象思路一共有三种。一种是metatable直接来(项目中目前就是如此,缺点是如果继承层级过多会有额外的metatable开销)。二是copy父类方法到子类中。三是执行时copy(参考云风的blog)。
- 配置表 目前参考了一些市面上的项目方案,直接将配置表导出成对应的代码。需要讨论的有两个点,一是解析配置表好还是直接就是代码好。二是代码中的重复关键词如何消除,目前考虑可以使用数组+metatable实现,还没有动手。
- 协议
目前协议使用了
tolua
自带的proto-gen-lua
,缺陷是不支持[packed=true]
字段,该字段可以减少列表长度n的字段 (n - 1) * 8bit的数据量。 - 全局变量
全局变量是否有额外的开销,开销是多少目前我还不是很清楚。因此,
UIView
等是否使用全局变量还需要进一步了解。
经验技巧
- 显示子视图时,需要判断该视图是一个全局的还是一个独立的实例
-- 显示顶部菜单中的货币视图 因为各个界面都有该类,那么其应该是一个单独的实例 所以需要调用 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)
endself.lstContainer:setLuaDatas(list)
self.lstContainer:display()
- 代码提示的功能添加
-- 最常用的应该这个引用标签了,这里直接声明了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
- 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
项目规范
- 所有的变量,添加详细的注释,可以是英文
- 类型名称的每个单词首字母均大写
- 变量名和函数名均小写字母开头
- 常量名全大写或者k字母开头并在注释中声明
- 窗口代码和资源名称要匹配
- 窗口以
Window
,视图以View
为后缀方便区分 - 一些公共的数据结构,到
api
目录下创建对应的接口提示 likeItem
- 一些常用的数据,和方法,要是用标签指定其对应参数和返回值类型,方便大家写代码
- 方法一定要注意
:
和.
的区别,尽量少用.
减少全局变量的调用,并且保持一致的:
风格