Unity SLua加载lua文件的简捷方案

作为一个Unity新手,在打包lua代码的时候,遇到几个棘手的问题:

  1. Resource.Load无法加载.lua格式的文件
  2. 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的也看过,代码写得比较啰嗦),简单概括就是:

  1. 确认package.loaded中是否已经加载过此模块,如果有,直接返回
  2. 如果没有,通过findloader()查找到模块对应的loader
  3. 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流程变成了下面这样:

  1. require "a.b.c"
  2. 调用c实现:ll_require()
  3. 逐个调用package.searchers中的searcher,找到loader
  4. 到LuaState.loader(),static方法
  5. 到LuaState.loadFile()
  6. 确认有没有loader delegate,有就调用,得到byte[],没有则进行通用lua文件加载策略,得到byte[]
  7. 返回LuaState.loader(),对byte[]进行luaL_loadbuffer,得到chunk返回
  8. 回到ll_require()中,ll_require()执行这个chunk,并将此chunk执行结果缓存到package.loaded[name]中,如果为nil,则存入true
  9. 返回chunk执行结果给调用require "a.b.c"处
  10. 完成

整个流程弄清楚之后,我们只需要编写一个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

占用工作时间写的笔记,比较乱,将就看啦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,700评论 0 38
  • 第一篇 语言 第0章 序言 Lua仅让你用少量的代码解决关键问题。 Lua所提供的机制是C不擅长的:高级语言,动态...
    testfor阅读 2,638评论 1 7
  • 指令集 lua_capture_error_log lua_use_default_type lua_malloc...
    吃瓜的东阅读 11,954评论 0 2
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,335评论 0 17
  • 曾先生的书架上有很多书,那天我想挑一本来看看。又不知从哪本看起。他见我的选择困难症犯了,便随手递给我一本李尚龙的《...
    依米Ariel阅读 167评论 0 0