作为一个Unity新手,在打包lua代码的时候,遇到几个棘手的问题:
- Resource.Load无法加载.lua格式的文件
- lua中的require/dofile/loadfile无法使用,android中无法使用file read
讲解决方案前,先附上两个git仓库及纠正一个网上文章的错误:
lua: https://github.com/lua/lua
slua: https://github.com/pangweiwei/slua
网上有关require顺序描述的文章:http://blog.chinaunix.net/uid-552961-id-2736410.html
里面的require顺序有误,不是先从package.loaded中加载,具体顺序,请参考此文章,建议遇到问题,先看官网及源码,最后再google, baidu。
对于第一个问题,有非常多种做法,这边选用的是打包时,将一个个lua文件copy到Resources/LuaScripts目录(按照原有的目录结构copy,项目本身的Lua代码在Assets同层的LuaScripts目录),并修改后缀为.txt,因为项目出demo比较急,做在了unity菜单上,简单在项目editor中增加了生成->打包用Lua文件
一项,后面需要统一使用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进行lua文件的读入。
对于第二个问题,之前没有细看slua对package.searchers/package.loaders的修改部分代码,想了非常多不太靠谱的方案,后面再细看slua中的LuaState.init代码的时候,发觉已经做了这些工作了,只需要为你的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实现,已经非常清爽(5.1的也看过,代码写得比较啰嗦),简单概括就是:
- 确认package.loaded中是否已经加载过此模块,如果有,直接返回
- 如果没有,通过findloader()查找到模块对应的loader
- call loader(对于c就是调用entry function,lua即执行chunk),将结果保存在package.loaded[name]中,如果call loader返回nil,直接填充true,返回call loader结果
其中findloader看了一下实现,里面会逐个取得package.searchers中的searcher进行loader search,打到后,返回loader,具体可以看loadlib.c中的实现,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已经将loadfile, dofile都已经替换成了自己的loadfile, dofile,内部统一调用loader进行lua file load,同时将自己的loader插入到了package.searchers中的2位置上,5.3的loader顺序是(看loadlib.c中的createsearchertable()):{searcher_preload, searcher_Lua, searcher_C, searcher_Croot, NULL};
,放在preload searcher前是合理的,preload需要先被执行,这个是内部库searcher实现,它应该是第一个被调用的。
看LuaState中的的loadfile/dofile实现,都只会进行loader调用,也就确保了在lua中的"loadfile", "dofile"最终调用回了LuaState中的loader。
LuaState.loader调用的是LuaState.loadFile()方法,在最终的loadFile实现中: image.png我们可以看到,SLua会先确认有没有自定义的LoadDelegate,如果有就调用,没有就进行通用处理,魔法就在这里了,我们实现一下自己的loader delegate即可,简单来说,require流程变成了下面这样:
- require "a.b.c"
- 调用c实现:ll_require()
- 逐个调用package.searchers中的searcher,找到loader
- 到LuaState.loader(),static方法
- 到LuaState.loadFile()
- 确认有没有loader delegate,有就调用,得到byte[],没有则进行通用lua文件加载策略,得到byte[]
- 返回LuaState.loader(),对byte[]进行luaL_loadbuffer,得到chunk返回
- 回到ll_require()中,ll_require()执行这个chunk,并将此chunk执行结果缓存到package.loaded[name]中,如果为nil,则存入true
- 返回chunk执行结果给调用require "a.b.c"处
- 完成
整个流程弄清楚之后,我们只需要编写一个loader即可,参考的Loader代码:
// 代码从项目的LuaEngine中摘取,内部方法统一"_"加大驼峰格式
// _scriptRootPath为LuaEngine中缓存的脚本根目录路径,在Editor下为全路径,在Device模式下为"LuaScripts"
// require的格式都为"a.b.c",所以需要先进行replace操作,所有"."统一replace成"/"
// 在editor模式下,简单进行File.ReadAllBytes即可,后缀还是.lua,在device模式下,进行资源加载,资源已经在上面有说,会copy到Resources/LuaScripts目录中
// lua文件加载这边可以在loading时先加载到cache中,这样游戏运行中,require可以快一点,也不会出现卡顿,但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
占用工作时间写的笔记,比较乱,将就看啦。
网友评论