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方法,实现特殊逻辑需求
  • 相关阅读:
    最近开始学习python,学习到了关于web的内容。
    redis requires Ruby version >= 2.2.2问题
    xml中CDATA包含问题
    pl/sql中文乱码问题解决
    关于window.open()中文传值乱码问题的解决方法
    关于maven中央仓库jar包不存在问题
    201571030332 扎西平措 《面向对象程序设计Java》第八周学习总结
    扎西平措 201571030332 《面向对象程序设计(java)课程学习进度条》
    扎西平措 201571030332 《面向对象程序设计 (JAVA)》第二周 学习总结
    扎西平措 201571030332《面向对象程序设计 Java 》第一周学习总结
  • 原文地址:https://www.cnblogs.com/byfei/p/3112355.html
Copyright © 2011-2022 走看看