Lua 游戏开发学习

使用 Lua 带来的便利性

个人经历过的大部分游戏服务器项目, 用到 Lua 的地方都不多, 借着这次重构项目的机会, 可以好好学习一下. Lua 在游戏开发中有很多优点:

  • 业务逻辑开发效率的提升, 这点在业务复杂的 mmo 项目中尤为明显.
  • 摆脱繁重的数据配置, 当然策划是基本指望不上的.
  • 天然的热加载机制, 无论是配置还是业务逻辑, 都可以很方便的做不停服更新.
  • Lua 虚拟机提供了一个安全的沙箱, 增强了服务器的稳定性, 至少不用太过于担心线上的 coredump.
  • Lua 语言提供的 coroutine 机制, 可以很容易的维护多状态的异步逻辑, 比起多线程 C/C++ 来的更轻量.

Anyway, 不管 Lua 宣称多高效, 它毕竟是一个脚本, 不可能和 C/C++ 达到一样数量级的处理速度 (最理想的情况下, LuaJit 可以达到接近 C++ 1/10 - 1/5 的性能). 所以需要在设计时多加斟酌, 尽量把 CPU 消耗型的操作放到 C/C++ 层去处理.

Lua 的加载路径

Lua 的加载机制依赖于路径, 默认的 Lua 加载路径是_G.package.path, C 加载路径是_G.package.cpath, 支持 ? 表示文件名.

现在的做法是在程序启动或者重载的时候, 通过一个 Lua 的初始化函数去做这些设置.

之前的项目会在每个路径下写一个 file_list, 包括了依赖的目录和文件, 然后自动读取加载. 不是很喜欢这种方式, 这不是什么复杂的问题.

Lua 的模块化

Lua 的模块化有点类似 java 的 package 或者 C 的 lib, 使开发更清晰.

早期的 Lua 模块化, 是通过 module("...", package.seeall) 来显式声明的, 调用者 reqiure 文件名即可. 之所以需要 package.seeall, 大概是将 module 写入 _G 表中, 因此也带来了全局表污染的问题.

Lua5.2 之后已经不推荐这种方式了, 更好的做法是下面这样:

-- file: mod.lua
local mod = {}
function mod.hello()
    print("hello, module")
end
return mod
-- file: test.lua
local M = require "mod.lua"
M.hello()

Lua 的热更新

在 Lua 中, 一般使用 loadfile 或者 require 来加载模块, 区别在于 require 是按需加载, 如果已经加载过则不会重复加载.

在使用 require 时做热更, 需要把之前加载的内容置为 nil, 即 _G.package.loaded 表中的 module 项设置为 nil, 再重新 require.

热更新最好做到按需加载, 否则在脚本量比较大的时候, 瞬间的重新加载会带来 CPU 的波峰, 导致游戏瞬卡.

除此之外, 可能还有一些全局数据不能重置, 这里的观点是数据和逻辑分开, 数据做持久化, 尽量不要放到脚本层, 从根源上避免这个问题.

C 调用 Lua

C 与 Lua 通信的关键在于一个虚拟的栈, 几乎所有的 Lua C API 都是在对这个虚拟栈在操作. 这个虚拟栈解决了两个困难的问题:

  • Lua 是一个有自动垃圾回收的语言, 而 C 没有, 两者矛盾. 而虚拟栈是由 Lua 管理的, 所以 Lua gc 时知道栈上的哪个值被 C 使用, 哪些是可以回收的.
  • Lua 的动态类型, 与 C 的静态类型, 不一致, 做类型映射比较困难. 通过 lua_toXXX() 接口可以把栈上的 Lua 数据转为 C 可以使用的数据.

Lua调用C

Lua 调用 C 是动态链接的思想, 需要在 C 中注册函数, 即, 需要把 C 函数的地址以一个适当的方式传递给 Lua 虚拟机, 常见的是编译一个 dll 或者 so, 给 Lua require.

一个 Lua 的 C 库, 除了定义了一系列的 C 函数, 还必须定一个和 Lua 库的主 chunk 通信的特殊函数. 一个简单示例如下:

// 函数必然是lua_CFunction类型
// in: string log-content}
// out:
static int l_log(lua_State* L)
{
    ...
    return 0;
}

...

// 需要luaopen_XX格式
// XX最好就是编译成so的name, 在lua中直接require就能调用之
int luaopen_lpolar(lua_State* L)
{
    luaL_Reg reg[] = {
        {"log", l_log},
        {NULL, NULL},
    };

    luaL_newlib(L, reg);
    return 1;
}

Lua 的 debug 日志

现在 Lua 中的日志接口是封装在 C Lib 中的, 代码如下, 其中最重要的是通过 lua_getstack 获取当前 Lua 栈上的数据信息, 例如行号和文件名, 并打印出来.

日志能非常好的辅助调试, 但是这种方式的开销会比较大, 在线上的正式环境中不建议使用.

// in: string log-content
// out:
static int l_log(lua_State* L)
{
    lua_Debug ar;
    lua_getstack(L, 1, &ar);
    lua_getinfo(L, "Sl", &ar);
    const char* content = luaL_checkstring(L, 1);
    WLOGLUA("[%s:%d][LUA] %s", ar.source, ar.currentline, content);
    return 0;
}

参考文章

  1. 《Programming In Lua》
  2. 《Lua 模块化开发》
  3. 《如何实现 Lua 代码的热更新》