作為一個Unity新手,在打包lua代碼的時候,遇到幾個棘手的問題:
- Resource.Load無法加載.lua格式的文件
- lua中的require/dofile/loadfile無法使用橘蜜,android中無法使用file read
講解決方案前蚤认,先附上兩個git倉庫及糾正一個網(wǎng)上文章的錯誤:
lua: https://github.com/lua/lua
slua: https://github.com/pangweiwei/slua
網(wǎng)上有關require順序描述的文章:http://blog.chinaunix.net/uid-552961-id-2736410.html
里面的require順序有誤稳摄,不是先從package.loaded中加載室囊,具體順序,請參考此文章秘噪,建議遇到問題狸吞,先看官網(wǎng)及源碼,最后再google, baidu指煎。
對于第一個問題蹋偏,有非常多種做法,這邊選用的是打包時至壤,將一個個lua文件copy到Resources/LuaScripts目錄(按照原有的目錄結構copy威始,項目本身的Lua代碼在Assets同層的LuaScripts目錄),并修改后綴為.txt像街,因為項目出demo比較急黎棠,做在了unity菜單上晋渺,簡單在項目editor中增加了生成->打包用Lua文件
一項,后面需要統(tǒng)一使用python腳本編寫脓斩,以方便自動出包木西,下面是參考代碼:
[MenuItem("XXXProj/生成/打包用lua文件")]
public static void OnMenu_GenTarLuaFiles()
{
// Env.luaScriptDFirectoryName 為lua腳本目錄名, 項目中定義為LuaScripts
// Env.luaScriptPath 為lua腳本目錄,在editor為全路徑随静,如:d:\project\program\client\LuaScripts, 在device上為Application.streamingAssets\LuaScripts
// Join方法為項目特有方法八千,仿python的join設計,方便進行路徑拼接
var targetRootDir = FileUtil.Join(Application.dataPath, "Resources", Env.luaScriptDirectoryName);
if (!Directory.Exists(targetRootDir))
Directory.CreateDirectory(targetRootDir);
var origDirInfo = new DirectoryInfo(Env.luaScriptPath);
FileUtil.ForeachFiles(origDirInfo, (dInfo) => // 項目特有方法燎猛,方便目錄遍歷
{
if (dInfo.Name.StartsWith("."))
return false;
return true;
},
(fInfo) =>
{
if (fInfo.Name.StartsWith("."))
return;
var inlPath = fInfo.FullName.Substring(Env.luaScriptPath.Length);
var inlDir = FileUtil.DirName(inlPath);
var targetDir = FileUtil.Join(targetRootDir, inlDir);
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
var targetPath = FileUtil.Join(targetRootDir, FileUtil.SplitExt(inlPath)[0] + ".txt");
File.Copy(fInfo.FullName, targetPath, true);
});
在生成后恋捆,進行打包,這樣可以在device中通過Resource.Load進行l(wèi)ua文件的讀入扛门。
對于第二個問題鸠信,之前沒有細看slua對package.searchers/package.loaders的修改部分代碼纵寝,想了非常多不太靠譜的方案论寨,后面再細看slua中的LuaState.init代碼的時候,發(fā)覺已經(jīng)做了這些工作了爽茴,只需要為你的LuaState設置一個loaderdelegate就OK葬凳,最終幾行代碼解決了lua require支持及c#中dofile/loadfile的問題,說解決方案前室奏,需要了解一下lua中的require機制火焰,直接看lua53中的loadlib.c中的ll_require代碼:
static int ll_require (lua_State *L) {
const char *name = luaL_checkstring(L, 1);
lua_settop(L, 1); /* LOADED table will be at index 2 */
lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_getfield(L, 2, name); /* LOADED[name] */
if (lua_toboolean(L, -1)) /* is it there? */
return 1; /* package is already loaded */
/* else must load package */
lua_pop(L, 1); /* remove 'getfield' result */
findloader(L, name);
lua_pushstring(L, name); /* pass name as argument to module loader */
lua_insert(L, -2); /* name is 1st argument (before search data) */
lua_call(L, 2, 1); /* run loader to load module */
if (!lua_isnil(L, -1)) /* non-nil return? */
lua_setfield(L, 2, name); /* LOADED[name] = returned value */
if (lua_getfield(L, 2, name) == LUA_TNIL) { /* module set no value? */
lua_pushboolean(L, 1); /* use true as result */
lua_pushvalue(L, -1); /* extra copy to be returned */
lua_setfield(L, 2, name); /* LOADED[name] = true */
}
return 1;
}
可以看到,在5.3中的require實現(xiàn)胧沫,已經(jīng)非常清爽(5.1的也看過昌简,代碼寫得比較啰嗦),簡單概括就是:
- 確認package.loaded中是否已經(jīng)加載過此模塊绒怨,如果有纯赎,直接返回
- 如果沒有,通過findloader()查找到模塊對應的loader
- call loader(對于c就是調(diào)用entry function南蹂,lua即執(zhí)行chunk)犬金,將結果保存在package.loaded[name]中,如果call loader返回nil六剥,直接填充true晚顷,返回call loader結果
其中findloader看了一下實現(xiàn),里面會逐個取得package.searchers中的searcher進行l(wèi)oader search疗疟,打到后该默,返回loader,具體可以看loadlib.c中的實現(xiàn)策彤,5.1栓袖、5.2則是從package.loaders取得searcher顿膨,所以從命名來看,還是5.3合理叽赊。
說到這里恋沃,大家應該清楚怎么去做自己的require了,只需要在package.searchers中增加自己的searcher即可必指,SLua也是這么做的囊咏,看LuaState.init中的部分代碼:
pushcsfunction(L, dofile);
LuaDLL.lua_setglobal(L, "dofile");
pushcsfunction(L, loadfile);
LuaDLL.lua_setglobal(L, "loadfile");
pushcsfunction(L, loader);
int loaderFunc = LuaDLL.lua_gettop(L);
LuaDLL.lua_getglobal(L, "package");
#if LUA_5_3
LuaDLL.lua_getfield(L, -1, "searchers");
#else
LuaDLL.lua_getfield(L, -1, "loaders");
#endif
int loaderTable = LuaDLL.lua_gettop(L);
// Shift table elements right
for (int e = LuaDLL.lua_rawlen(L, loaderTable) + 1; e > 2; e--)
{
LuaDLL.lua_rawgeti(L, loaderTable, e - 1);
LuaDLL.lua_rawseti(L, loaderTable, e);
}
LuaDLL.lua_pushvalue(L, loaderFunc);
LuaDLL.lua_rawseti(L, loaderTable, 2);
LuaDLL.lua_settop(L, 0);
slua已經(jīng)將loadfile, dofile都已經(jīng)替換成了自己的loadfile, dofile,內(nèi)部統(tǒng)一調(diào)用loader進行l(wèi)ua file load塔橡,同時將自己的loader插入到了package.searchers中的2位置上梅割,5.3的loader順序是(看loadlib.c中的createsearchertable()):{searcher_preload, searcher_Lua, searcher_C, searcher_Croot, NULL};
,放在preload searcher前是合理的葛家,preload需要先被執(zhí)行户辞,這個是內(nèi)部庫searcher實現(xiàn),它應該是第一個被調(diào)用的癞谒。
看LuaState中的的loadfile/dofile實現(xiàn)底燎,都只會進行l(wèi)oader調(diào)用,也就確保了在lua中的"loadfile", "dofile"最終調(diào)用回了LuaState中的loader弹砚。
LuaState.loader調(diào)用的是LuaState.loadFile()方法双仍,在最終的loadFile實現(xiàn)中:我們可以看到,SLua會先確認有沒有自定義的LoadDelegate桌吃,如果有就調(diào)用朱沃,沒有就進行通用處理,魔法就在這里了茅诱,我們實現(xiàn)一下自己的loader delegate即可逗物,簡單來說,require流程變成了下面這樣:
- require "a.b.c"
- 調(diào)用c實現(xiàn):ll_require()
- 逐個調(diào)用package.searchers中的searcher瑟俭,找到loader
- 到LuaState.loader()翎卓,static方法
- 到LuaState.loadFile()
- 確認有沒有l(wèi)oader delegate,有就調(diào)用尔当,得到byte[]莲祸,沒有則進行通用lua文件加載策略,得到byte[]
- 返回LuaState.loader()椭迎,對byte[]進行l(wèi)uaL_loadbuffer锐帜,得到chunk返回
- 回到ll_require()中,ll_require()執(zhí)行這個chunk畜号,并將此chunk執(zhí)行結果緩存到package.loaded[name]中缴阎,如果為nil,則存入true
- 返回chunk執(zhí)行結果給調(diào)用require "a.b.c"處
- 完成
整個流程弄清楚之后简软,我們只需要編寫一個loader即可蛮拔,參考的Loader代碼:
// 代碼從項目的LuaEngine中摘取述暂,內(nèi)部方法統(tǒng)一"_"加大駝峰格式
// _scriptRootPath為LuaEngine中緩存的腳本根目錄路徑,在Editor下為全路徑建炫,在Device模式下為"LuaScripts"
// require的格式都為"a.b.c"畦韭,所以需要先進行replace操作,所有"."統(tǒng)一replace成"/"
// 在editor模式下肛跌,簡單進行File.ReadAllBytes即可艺配,后綴還是.lua,在device模式下衍慎,進行資源加載转唉,資源已經(jīng)在上面有說,會copy到Resources/LuaScripts目錄中
// lua文件加載這邊可以在loading時先加載到cache中稳捆,這樣游戲運行中赠法,require可以快一點,也不會出現(xiàn)卡頓乔夯,但lua文件cache在游戲后期也有可能高達十幾MB砖织,這個就交給大家權衡啦
private byte[] _LoaderDelegate(string fn)
{
Log.Dbg<LuaMgr>("Load lua file:{0}", fn);
return _LoadLuaBytes(fn);
}
private byte[] _LoadLuaBytes(string fn)
{
string assetPath = FileUtil.Join(_scriptRootPath, fn.Replace('.', '/'));
#if UNITY_EDITOR
return File.ReadAllBytes(assetPath + ".lua");
#else
var textAsset = ResMgr.LoadRes<TextAsset>(assetPath);
return textAsset != null ? textAsset.bytes : null;
#endif
占用工作時間寫的筆記,比較亂驯嘱,將就看啦镶苞。