不管是 現在開發中的游戲服務端, 還是近期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
備注: 該機制只支持簡單文件目錄