美文网首页程序员
Unity SLua加载lua文件的简捷方案

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

作者: blueuee | 来源:发表于2018-01-30 10:41 被阅读0次

    作为一个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
    

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

    相关文章

      网友评论

        本文标题:Unity SLua加载lua文件的简捷方案

        本文链接:https://www.haomeiwen.com/subject/vhyqzxtx.html