Lua游戏脚本热更新机制分享

2015年03月23日 16:50 0 点赞 0 评论 更新于 2025-11-21 18:20

设计要点

在服务器运行期间,热更新机制需实现更新程序逻辑代码,以达到修正程序 Bug、修改游戏数据的目的。不过,游戏框架代码的热更新暂不考虑。以下是具体示例:

  • 修复业务逻辑 Bug:若某个业务处理函数存在逻辑 Bug,在服务器运营期间被发现,可在不停机的情况下及时更新代码进行修复。
  • 修正游戏数据错误:当技能数值表因策划填写出现手误,导致玩家战斗异常时,可在不停机的情况下及时更新内存中的数值表。

在更新代码时,要格外保护非代码数据,尽量避免重新载入存盘数据。例如,登陆用户列表保存在内存中,当用户登录模块出现 Bug 需要更新代码时,更新后用户列表应依然有效,确保在线用户不受影响。

常见方案

Lua 的 module 机制

在 Lua 中,require 函数会阻止重新加载相同的模块。当需要更新系统时,需卸载自己编写的模块。具体做法是:将 package.loaded 里对应模块名的值设置为 nil(这样能保证下次 require 时重新加载一次),同时把全局表中对应的模块表置为 nil

为保证数据不被更新过程重置,需将数据记录在专用的全局表下,并用 local 去引用它。在初始化这些数据时,首先要检查它们是否已被初始化。

不过,这种方法存在一些问题。例如,在 Module B 中使用 local shortFunc = ModuleA.longFunc,在更新 ModuleA 后,shortFunc 无法更新,需要在之前加上 local shortFunc = nil,操作较为麻烦。而且,此方法用到了 module,但在 Lua 5.2 中 module 机制已被弃用,Lua 建议用户自己实现更简单的方法。

loadfile 机制

loadfile 的使用基础

通过 loadfile 可以模拟 module 机制,并且能根据业务需求灵活加入更多的热更新需求。以下是基于 Lua 5.2 版本的 loadfile 实现代码动态加载模块的示例:

-- 代码动态加载模块 v0.1
-- 基于 Lua 5.2 版本的 loadfile
-- 模块管理表,类似于 package.loaded
local Modules = {}

-- 通过 loadfile 将新代码载入到 newModule
function Load(pathname, modulename)
local newModule = {}
-- _ENV 被设置为空表,载入文件的访问空间被限制在模块内部
local func, err = loadfile(pathname, "bt", newModule)
if not func then
print("ERROR: " .. err)
else
func()
Modules[modulename] = newModule
end
end

下面是测试模块 Game.lua 的代码:

-- 模拟一个游戏定时活动玩法逻辑
-- 活动中获奖用户记录下来,活动结束后发奖
-- 此代码有逻辑错误
-- 活动中获奖用户列表
rewardedUser = {}
-- 活动开始结束时间设置
local starttime = {hour = 10, min = 0, sec = 0}
local endtime = {hour = 12, min = 0, sec = 0}

-- 记录活动中中奖用户
function RecordReward(charId)
rewardedUser[char] = true -- 报错,char = nil
-- 更新后的正确代码
-- rewardedUser[charId] = true
end

测试驱动模块 Main.lua 的代码如下:

require("Modules.lua")
GAME = Load("Game.lua")
GAME.RecordReward(1001) -- 报错
-- 更新错误以后重新加载
GAME = Load("Game.lua")
GAME.RecordReward(1001) -- rewardedUser = { [1001] = true}
local data = GAME.rewardedUser
local func = GAME.RecordReward
-- local 导致当前 GAME.RecordReward、GAME.rewardedUser 引用被当作 upvalue 绑定在 closure 里
GAME = Load("Game.lua")
-- GAME.rewardedUser = {} 被重新载入的代码所取代, { [1001] = true} 数据丢失,导致用户无法领奖,业务逻辑发生异常。
GAME.RecordReward(1002) -- rewardedUser = { [1002] = true}
-- data 引用的是保存在 upvalue 中的旧引用,值为{ [1001] = true} ,与当前 GAME.RecordReward 不同步了,出现逻辑异常
func()
-- 仍然报错,因为其引用的是保存在 upvalue 中的旧引用,而不是更新后的 GAME.RecordReward

通过上述代码实验,我们发现了以下几个问题:

  • Game.lua 不能访问 _G 空间。
  • 需要持久化的内容没有得到保存。
  • upvalue 导致热更新失效。

不过,我们也注意到,通过 loadfile 加载的代码是以 table 形式引用的,这为解决上述问题提供了思路。

loadfile 的使用进阶

以下是优化后的代码动态加载模块:

-- 代码动态加载模块 v0.1
-- 基于 Lua 5.2 版本的 loadfile
-- 模块管理表,类似于 package.loaded
local Modules = {}

-- 参数:
-- string pathname 加载模块文件名,含路径
-- string name 模块名,一般和 pathname 一致即可
-- boolean reload 是否强制更新重载
-- 返回:
-- table module 模块,如果加载失败返回 nil
-- string err 如果 module 为 nil,返回错误信息
function Load(pathname, moduleName, reload)
moduleName = moduleName or pathname
local oldModule = Modules[moduleName]
if not oldModule then -- 第一次加载模块,全新加载
local newModule = {}
-- 通过 metatable 机制允许模块环境访问 _G
setmetatable(newModule, {__index = _G})
local func, err = loadfile(pathname, "bt", newModule)
if not func then
print("ERROR: " .. err)
return nil, err
end
func()
Modules[moduleName] = newModule
return newModule
else -- 重复加载,不需要更新时直接返回缓存
if not reload then
return oldModule
else
-- 先缓存更新前模块内的 table 数据
local oldCache = {}
for k, v in pairs(oldModule) do
if type(v) == "table" then
oldCache[k] = v
end
oldModule[k] = nil
end
-- 原模块直接作为新的环境使用
local newModule = oldModule
-- 原模块被完全更新
local func, err = loadfile(pathname, "bt", newModule)
if not func then
print("ERROR: " .. err)
return nil, err
end
func()
-- 恢复 table 数据,既保持原有数据,也保持了其他模块的既有引用
-- 因为此引用机制只能作用于 table,函数 upvalue 依然得不到更新
for k, v in pairs(oldCache) do
-- 将 metatable 换成新的即可实现函数段更新
local mt = getmetatable(newModule[k])
if mt then setmetatable(v, mt) end
-- 对于已存在的 table,数据段保持不变
newModule[k] = v
end
return newModule
end
end
end

小结

  • 数据保持方面:此方法可实现模块内非 local table 的数据保持以及外部对其引用的保持,且不需要业务模块进行特殊处理。但模块内 local table 数据不能保持,建议用来保存固定数据,如数值表、活动时间等。
  • 函数引用方面:其他模块通过 local func = MODULE.Func 的形式引用模块函数无法得到更新,建议不使用此形式。或者在每个引用之前加上 local func = nil,强制脚本每次执行时去更新引用。
  • 扩展功能方面:在此基础上还可扩展模块的初始化和 unload 方法,以实现特殊逻辑需求。

作者信息

menghao

menghao

共发布了 3994 篇文章