Lua 腳本重啟 機制


不管是 現在開發中的游戲服務端, 還是近期love2D 開發的前端, 都使用 Lua 做腳本引擎, 需要涉及到 腳本的修改和重啟. 第一種方法是 寫個封裝函數, 里面進行對所有 lua 腳本文件的 require() 操作, 這就要求 :

1.對每個支持重新加載的文件進行

package.loaded[ filename]  = nil
require( filename)

2.文件加載要保持一定的順序, 以免造成資源的錯亂.

就當前使用 love2D 前端來看, 其實只有一個 "啟動"文件: main.lua, 並在其內進行 各個子功能腳本的 require 加載.如果在 重新加載時, 自動按照 main.lua 提供的

require(...) 順序進行自動加載就好了, 並且無需像上面的針對每個文件編寫:

function reload_files()
    require( f1)
    require( f2)
    ...

end

整體目標有幾個:
1.無需靜態維護一個重新加載的文件, 或函數, 進行編寫 各個腳本文件的 require() 進行重新加載;

2.能夠按照當前文件中各個文件的 順序進行加載, 即如果

--main.lua

require( "config")
require( "function")
require( "globals")
require( "gameplayer")
require( "scene") 

NeedReset = true
...

這種順序編寫main.lua( 或其他文件), 都盡量保持 config > function > globals > gameplayer > scene 的順序進行重新加載;
3.能夠避免 "已被重新記載的文件" 再次被重新加載;

4.能夠避免 嵌套遞歸加載;

5.能夠對 外部庫進行識別, 即 

require( "bit")

是在加載 "位操作"的 庫 bit.dll , 而不是 bit.lua, 不應該進行 嵌套加載;
6.能夠識別某些 "禁止重新加載"的文件, 例如:

-- global.lua

require( "skill_cfg")
require( "effect_cfg")

g_object_list = {}

global.lua 文件本身不能被 多次require(), 不然 g_object_list 全局變量會被重置, 但又能夠不會影響 skill_cfg 和 effect_cfg 的重新加載;

7.應該要支持 "后序" 方式進行加載, 記載加載 main.lua 過程中, 應該現在遞歸加載完 子腳本文件:

require( "config")
require( "function")
require( "globals")
require( "gameplayer")
require( "scene")

然后在進行 加載 main.lua 的后序內容:

NeedReset = true
...

8.能夠 識別 文件中的 require(...) 行.

 

大概這 8 點目標 和要求, 但對於第7點, 有個問題:

假設 重新加載 的 遞歸函數為

function recursive_reload( filename)

   package.loaded[ filename] = nil  
   require( filename ) end

並且main.lua 的內容簡單有如:

--main.lua

require( "config") 
require( "function") 
require( "globals") 
require( "gameplayer") 
require( "scene")

NeedReset = true

 

在 觸發重新加載的 入口中:

function main_reloader()  
    recursive_reload( "mian" ) 
end

調用 main_reloader() 進行重新加載的過程 展開將會如:

--先遞歸地使用 recursive_reload() 重新加載子文件 

package.loaded[ 'config'] = nil 
require( 'config')

package.loaded[ 'function'] = nil 
require( 'function')

package.loaded[ 'globals'] = nil 
require( 'globals')

package.loaded[ 'gameplayer'] = nil 
require( 'gameplayer')

package.loaded[ 'scene'] = nil 
require( 'scene')

--再最后加載 main.lua 
package.loaded[ 'main'] = nil 
require( 'main') --但就在這個操作中, 還會涉及到嵌套的:  
require( "config")  
require( "function")  
require( "globals")  
require( "gameplayer")  
require( "scene")  

NeedReset = true
 
         

 

 
        

這 5 個 文件不就會被 多次 require() 了嗎? 雖然 完整的 recursive_reload() 能夠防止 "顯示的" 重復require(),  但是不能禁止 "隱式的" require() 其實, 就算第二次的 "隱式" requre() 確實會調用, 但不會重新加載 實際的物理文件, 見於 lua 開發手冊上:

require (modname) Loads the given module. 

The function starts by looking into the package.loaded table to determine whether modname is already loaded. 
If it is, then require returns the value stored at package.loaded[modname].
Otherwise, it tries to find a loader for the module.

即是說, 只要曾經加載了 文件, 並在 package.loaded 內有記錄, 后序的 requre() 將會直接返回.

這 5 個 文件不就會被 多次 require() 了嗎? 雖然 完整的 recursive_reload() 能夠防止 "顯示的" 重復require(),  但是不能禁止 "隱式的" require() 其實, 就算第二次的 "隱式" requre() 確實會調用, 但不會重新加載 實際的物理文件, 見於 lua 開發手冊上:

require (modname) Loads the given module. The function starts by looking into the package.loaded table to determine whether modname is already loaded. If it is, then require returns the value stored at package.loaded[modname]. Otherwise, it tries to find a loader for the module.

即是說, 只要曾經加載了 文件, 並在 package.loaded 內有記錄, 后序的 requre() 將會直接返回.

 

具體運行效果:

只是具體的實現代碼:

 

-- 外部庫 登記
local package_list = {
    bit = true 
}

-- 全局性質類/或禁止重新加載的文件記錄
local ignored_file_list = {
    global = true ,
}

--已重新加載的文件記錄
local loaded_file_list = {}

--視圖排版控制
function leading_tag( indent )
    -- body
    if indent < 1 then
        return ''
    else
        return string.rep( '    |',  indent - 1  ) .. '    '
    end
end

--關鍵遞歸重新加載函數
--filename 文件名
--indent   遞歸深度, 用於控制排版顯示
function recursive_reload( filename, indent )
    -- body
    if package_list[ filename] then 
        --對於 外部庫, 只進行重新加載, 不做遞歸子文件
        --卸載舊文件
        package.loaded[ filename] = nil

        --裝載信文件
        require( filename )

        --標記"已被重新加載"
        loaded_file_list[ filename] = true

        print( leading_tag(indent) .. filename .. "... done" )
        return true
    end

    --普通文件
    --進行 "已被重新加載" 檢測
    if loaded_file_list[ filename] then 
        print( leading_tag(indent) .. filename .. "...already been reloaded IGNORED" )
        return true
    end

    --讀取當前文件內容, 以進行子文件遞歸重新加載
    local file, err = io.open( filename..".lua" )
    if file == nil then 
        print( string.format( "failed to reaload file(%s), with error:%s", filename, err or "unknown" ) )
        return false
    end

    print( leading_tag(indent) .. filename .. "..." )

    --讀取每一行
    for line in file:lines() do 
        
        --識別 require(...)行, 正則表達? 模式匹配? 並拾取文件名 到 subFileName
        line = string.gsub( line, '%s', '' )
        local subFileName = nil 
        local ret = string.gsub( line, '^require%("(.+)"%)', function ( s ) subFileName = s end )

        if subFileName then
            --進行遞歸 
            local success = recursive_reload( subFileName, indent + 1 )
            if not success then 
                print( string.format( "failed to reload sub file of (%s)", filename ) )
                return false 
            end

        end
        
    end    


    -- "后序" 處理當前文件...

    
    if ignored_file_list[ filename] then
        --忽略 "禁止被重新加載"的文件
        print( leading_tag(indent) .. filename .. "... IGNORED" )
        return true
    else

        --卸載舊文件
        package.loaded[ filename] = nil

        --裝載新文件
        require( filename )

        --設置"已被重新加載" 標記
        loaded_file_list[ filename] = true
        print( leading_tag(indent) .. filename .. "... done" )
        return true
    end
end

--主入口函數
function reload_script_files()
    
    print( "[reload_script_files...]")

    loaded_file_list = {}

    --本項目是以 main.lua 為主文件
    recursive_reload( "main", 0 )
    
    print( "[reload_script_files...done]")

    return "reload ok"
end

備注: 該機制只支持簡單文件目錄

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM