0%

游戏项目客户端开发备忘录(三) - Lua 项目框架

列举了一些 Lua 项目开发中的技术备忘录,基于 Cocos2d-x v3.16 和 Lua 5.1.5。

Lua 风格 API

  • 借鉴了 quick-cocos2d-x 中的 Lua 风格 API,( quick-cocos2d-x 目前已被 Cocos2d-x 官方弃用)。
  • 对常用的变量或者方法进行缩写封装。例如:
Lua
1
2
3
4
5
6
7
d = display
function d.node()
return cc.Node:create()
end
function d.sprite()
return cc.Sprite:create()
end
  • 方法最后返回 self ,形成链式调用方法。例如:
Lua
1
2
3
4
5
6
7
8
9
10
function Node:pos(x, y)
-- ...
return self
end
function Node:addTo(parentNode, z, name)
-- ...
return self
end
-- e.g.
local node = d.node():pos(100, 100):size(100, 100):scale(2):addTo(parentNode)
  • 使用 IntelliJ IDEA + EmmyLua,Cocos2d-x 可用此 API 提示库 生成更详尽的代码补全。

UI 框架

  • 由于没有用到编辑器来进行 UI 编写,为了方便开发,使用了一套自用的 UI 框架。封装了常用的控件 ButtonScrollListViewWin等。
  • 避免混用 quick 与原生 Cocos2d-x 的 UI 和触摸机制。

热更新

  • 每次发布的版本带有一个小版本号作为标志,从高版本到低版本比对生成差异代码和资源,作为更新包。
  • 使用 AssetsManager 来下载更新包,解压到指定的 update 目录下。
  • 确保 update 目录在 FileUtils 类的 SearchPath 中优先级比较高。添加 SearchPath
C++
1
FileUtils::getInstance()->addSearchPath("update_path", true);
  • 热更新前需要删除缓存的 Lua 模块和资源。例如:
Lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cc.Director:getInstance():getTextureCache():removeAllTextures()
cc.SpriteFrameCache:getInstance():removeSpriteFrames()
cc.FileUtils:getInstance():purgeCachedEntries()
cc.Director:getInstance():purgeCachedData()
-- remove animation resources
-- remove all event listeners
-- close socket
-- remove loaded modules in package.preload
-- remove loaded modules in package.loaded
-- e.g.
for module, _ in pairs(package.loaded) do
if needRemove(module) then
package.loaded[module] = nil
end
end
  • 然后重新加载需要的模块文件。
  • 注意:从应用商店下载或安装 apk 包后更新应用版本,如果安装包中带有新的代码或资源,需要删除 update 目录中下载的代码与资源,防止加载旧的代码。

原生平台交互

iOS

  • Lua 层相关方法封装在 luaoc.lua
Lualuaoc.lua
1
function LuaObjcBridge.callStaticMethod(className, methodName, args) end
  • 应用层 LuaObjcBridge 类,使用 Objective-C 中的反射机制调用对应类的类方法。
  • 应用层方法必须为类方法,接受无参数或者一个 NSDictionary 类型的参数。例如:
Objective-C
1
2
3
@interface AppBridge : NSObject
+ (void) openWebView:(NSDictionary*) dict;
@end
  • Lua 层参数中可传入回调函数。例如:
Lua
1
2
3
LuaObjcBridge.callStaticMethod("className", "methodName", {
callback = function() end,
})
  • 应用层获取回调函数为 functionIdLuaBridge 执行函数。
Objective-C
1
2
3
4
int functionId = [[dict objectForKey:@"callback"] intValue];
cocos2d::LuaBridge::pushLuaFunctionById(functionId); //回调函数入栈
cocos2d::LuaBridge::getStack()->executeFunction(0); //执行函数,参数个数为0
cocos2d::LuaBridge::releaseLuaFunctionById(functionId); //release之前被retain的函数

Android

  • Lua 层相关方法封装在 luaj.lua
Lualuaj.lua
1
function LuaJavaBridge.callStaticMethod(className, methodName, args, sig) end
  • 应用层 LuaJavaBridge 类,使用 JNI 技术 调用对应的 Java 类中的类方法。

  • 应用层方法必须为类方法,可以接受多个参数。必须设置对应的参数与返回值签名。例如:

Java
1
2
3
public class AppBridge {
public static void openWebView(String url){}
}
  • Lua 层参数中可传入回调函数,回调函数支持无参数或一个 string 类型参数。例如:
Lua
1
LuaJavaBridge.callStaticMethod("className", "methodName", function(paramString) end)
  • 应用层获取回调函数为 functionIdCocos2dxLuaJavaBridge 执行函数。例如:
Java
1
2
3
int functionId;
Cocos2dxLuaJavaBridge.callLuaFunctionWithString(functionId, "string");
Cocos2dxLuaJavaBridge.releaseLuaFunction(functionId);
  • 由于游戏在 GL 线程上运行,如果需要调用系统 UI 接口,需要在 UI 线程上运行,回调 Lua 时需要在 GL 线程上运行。例如:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
Context context;
context.runOnUiThread(new Runnable() {
@Override
public void run() {
// do something on ui thread
context.runOnGLThread(new Runnable() {
@Override
public void run() {
// callback
}
})
}
});

优化

  • Android:可将向应用层的参数统一封装为 JSON 格式。

网络请求

HTTP

  • CCHTTPRequest 类,Lua 层将 Response 封装为回调函数。

Socket

Protocol Buffers

配置文件

  • 将 Excel 表转换为 Lua Table,项目中加载配置 Lua 文件。
  • 使用 Python 编写工具转换,Excel 读取库 xlrd

数据持久化

  • 方案一:使用了 quick 提供的 GameState 类,通过 io 模块读写文件。
  • 方案二:使用 lsqlite3 模块,读写本地数据库。

国际化

  • 使用 Application::getCurrentLanguage() 获取当前系统语言。

文本

  • 将文本提取至语言包,代码中使用 key 来获取对应语言的文本。封装替换文本方法,例如:
Lua
1
2
3
4
5
function __text(key, ...)
return langPack[key]
end
-- e.g.
print(__text('hello_world'))
  • 不同语言包可在文件名后加上语言后缀以区分,例如:lang_en.lualang_cn.lualang_fr.lua 等。
  • 为便于编辑维护,可用配置文件来生成语言包文件。

资源

  • 资源在路径上做区分,加入语言 code 作为后缀名。例如: res/example.pngres_cn/example.pngres_fr/example.png 等。

插件化

  • 将同一个插件内的文件放入某一文件夹下,例如:pluginA
  • 同一插件文件之间的引用通过 import 相对路径来代替 require 绝对路径 。import 方法实现参考这里。例如:import(".fileA"),而不是 require("pluginA.fileA")

分辨率适配

  • 都采用 NO_BORDER 策略,根据屏幕宽高比 frameSize 调整设计分辨率 designSize 大小。
  • 全局变量 CC_DESIGN_RESOLUTION 存储设计分辨率和适配策略。autoscale 表示适配策略。
    • 横屏游戏:如果 frameSize 宽高比大于 designSize 宽高比,autoscale 采用 FIXED_HEIGHT。反之用 FIXED_WIDTH
    • 竖屏游戏:如果 frameSize 宽高比小于 designSize 宽高比,autoscale 采用 FIXED_WIDTH。反之用 FIXED_HEIGHT
Lua
1
2
3
4
5
CC_DESIGN_RESOLUTION = {
width = 640,
height = 1136,
autoscale = "FIXED_HEIGHT",
}