zoukankan      html  css  js  c++  java
  • Lua游戏脚本热更新机制

    Lua游戏脚本热更新机制

    设计要点

    • 能够在服务器运行期间更新程序逻辑代码以实现修正程序Bug、修改游戏数据的目的。游戏框架代码的热更新暂不考虑。例如:
      • 某个业务处理函数逻辑有Bug,服务器运营期间发现,在不停机情况下及时更新代码修复。
      • 技能数值表策划填写出现手误错误,导致玩家战斗异常,在不停机情况下及时更新内存中的数值表。
    • 更新代码时要保护非代码数据,尽量不用重新载入存盘数据。例如:
      • 登陆用户列表保存在内存中,用户登录模块出现bug需要跟新代码,此段代码更新后,用户列表依然有效,在线用户不受影响。

    常见方案

    lua的module机制

    require 会阻止你重新加载相同的模块。当需要更新系统的时候,卸载掉自己编写的模块。方法是:把 package.loaded 里对应模块名下设置为 nil (这样可以保证下次 require 重新加载一次)并把全局表中的对应的模块表置 nil 。

    同时把数据记录在专用的全局表下,并用 local 去引用它。初始化这些数据的时候,首先应该检查他们是否被初始化过了。这样来保证数据不被更新过程重置。

    Module B中 local shortFunc = ModuleA.longFunc 在更新ModuleA后更新不到,需在之前加上local shortFunc = nil。比较麻烦

    此方法用到了module,在Lua5.2中已经弃用module机制,Lua建议用户自己实现更简单的方法。

    loadfile机制

    通过loadfile来模拟module机制,可以灵活的根据业务需求加入更多的热更新需求。

    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
    

    loadfile的使用进阶

    通过上面的代码实验,我们发现以下几个问题

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

    同时我们也可以看到,通过loadfile加载的代码,是以table形式引用的,这就为我们解决上述问题打开了思路。

    -------------------------
    --代码动态加载模块 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方法,实现特殊逻辑需求
  • 相关阅读:
    Android开发之Sqlite的使用
    ZOJ 3607 Lazier Salesgirl
    ZOJ 3769 Diablo III
    ZOJ 2856 Happy Life
    Ural 1119 Metro
    Ural 1146 Maximum Sum
    HDU 1003 Max Sum
    HDU 1160 FatMouse's Speed
    Ural 1073 Square Country
    Ural 1260 Nudnik Photographer
  • 原文地址:https://www.cnblogs.com/byfei/p/3112355.html
Copyright © 2011-2022 走看看