Unity开发时自动热更新Lua代码

在游戏开发阶段中,经常需要小修小改,Unity提供了很方便的Scene窗口,让开发者的修改马上生效,而不需要重新启动游戏。
那么代码需要怎么样才能做到一修改就马上生效,并且不需要重启游戏呢?

下面结合Slua来说一下代码自动更新的方案(我使用的版本是slua-1.3.2,较旧)

思路:

  1. 获取Lua加载过哪些文件,记录下这些文件的文件名、全路径和最后修改时间
  2. 定时检测文件的最后修改时间,如果比记录的最后修改时间还要大,则表示在加载后发生过修改,然后重新加载它
代码:
using UnityEngine;
#if UNITY_EDITOR    
using UnityEditor;
#endif
using System.Collections;
using System.Collections.Generic;
using System.IO;
using SLua;
using System;

namespace XS
{
    public class ImmediateLuaScript : MonoBehaviour
    {
        public class ScriptInfo
        {
            public string strFileName;
            public string strFileFullPath;
            public System.DateTime dtLastWrite;
        }

        List<ScriptInfo> m_loadedScripts = new List<ScriptInfo>();
        int m_nNextScriptIndex = 0;

        private static ImmediateLuaScript _Instance;
        public static ImmediateLuaScript Instance
        {
            get
            {
                if (_Instance == null)
                {
                    GameObject resMgr = GameObject.Find("_ImmediateLuaScript");
                    if (resMgr == null)
                    {
                        resMgr = new GameObject("_ImmediateLuaScript");
                        GameObject.DontDestroyOnLoad(resMgr);
                    }

                    _Instance = resMgr.AddComponent<ImmediateLuaScript>();
                }
                return _Instance;
            }
        }

#if UNITY_EDITOR
       // 可以通过外部控制ImmediateLuaScriptInEditor,使自动更新暂停或恢复
       public static bool ImmediateLuaScriptInEditor = true;
       // 每帧检查文件数,可根据文件数量成正比
       public static int CheckFilesOnce = 5;

       ScriptInfo getScriptInfo(string strFileFullPath)
        {
            foreach (ScriptInfo info in m_loadedScripts)
            {   
                if(info.strFileFullPath == strFileFullPath)
                {
                    return info;
                }
            }
            return null;
        }

        ScriptInfo getOrCreateScriptInfo(string strFileName, string strFileFullPath)
        {
            ScriptInfo info = getScriptInfo(strFileFullPath);
            if (info != null) return info;

            info = new ScriptInfo();
            info.strFileName = strFileName;
            info.strFileFullPath = strFileFullPath;
            m_loadedScripts.Add(info);
            return info;
        }

        ScriptInfo getNextScriptInfo()
        {
            if (m_nNextScriptIndex >= m_loadedScripts.Count)
            {
                m_nNextScriptIndex = 0;
                return null;
            }

            return m_loadedScripts[m_nNextScriptIndex++];
        }

        string getFileFullPath(string strFileName)
        {
            string strFileFullPath;
            do
            {
                // 这里的代码目录路径属于自定义
                strFileFullPath = UnityEngine.Application.dataPath + "/../luaScript/" + strFileName + ".lua";
                if (System.IO.File.Exists(strFileFullPath))
                    break;
            } while (false);

            return strFileFullPath;
        }

        public void SetImmediateLuaScript(string strFileName)
        {
            if (!ImmediateLuaScriptInEditor) return;

            string strFileFullPath = getFileFullPath(strFileName);
            FileInfo fileInfo = new FileInfo(strFileFullPath);
            if(fileInfo.Exists)
            {
                ScriptInfo info = getOrCreateScriptInfo(strFileName, strFileFullPath);
                info.dtLastWrite = fileInfo.LastWriteTime;
            }
        }

        void checkScriptUpdate()
        {
            ScriptInfo info = getNextScriptInfo();
            if (info == null) return;

            FileInfo fileInfo = new FileInfo(info.strFileFullPath);
            if (info.dtLastWrite < fileInfo.LastWriteTime)
            {
                info.dtLastWrite = fileInfo.LastWriteTime;
                bool result = loadScriptFile(info.strFileName, info.strFileFullPath);
                if(!result)
                {
                    var L = LuaSvr.Instance.luaState.L;
                    string err = LuaDLL.lua_tostring(L, -1);
                    LuaDLL.lua_pop(L, 2);
                    throw new Exception(err);
                }
            }
        }

        void LateUpdate()
        {
            if (!ImmediateLuaScriptInEditor) return;
            for(int i=0;i<CheckFilesOnce; i++)
            {
                checkScriptUpdate();
            }
        }

        public static int newLoader(System.IntPtr L)
        {
            string fileName = LuaDLL.lua_tostring(L, 1);
            Instance.SetImmediateLuaScript(fileName);
            LuaObject.pushValue(L, true);
            LuaDLL.lua_pushnil(L);
            return 2;
        }

        // 注册Lua脚本loader
        public void AddLuaLoaders(System.IntPtr L)
        {
            LuaState.pushcsfunction(L, newLoader);
            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);
        }

        // 加载lua文件
        bool loadScriptFile(string strFileName, string strFileFullPath)
        {
            FileStream fs = new FileStream(strFileFullPath, FileMode.Open);
            long size = fs.Length;
            byte[] bytes = new byte[size];
            fs.Read(bytes, 0, bytes.Length);
            fs.Close();

            bytes = LuaState.CleanUTF8Bom(bytes);

            var L = LuaSvr.Instance.luaState.L;
            if (bytes != null)
            {
                var nOldTop = LuaDLL.lua_gettop(L);
                var err = LuaDLL.luaL_loadbuffer(L, bytes, bytes.Length, "@" + strFileFullPath);
                if (err != 0)
                {
                    string errstr = LuaDLL.lua_tostring(L, -1);
                    LuaDLL.lua_pop(L, 1);
                    LuaObject.error(L, errstr);
                    return false;
                }
                err = LuaDLL.lua_pcall(L, 0, 0, 0);
                if (err != 0)
                {
                    string errstr = LuaDLL.lua_tostring(L, -1);
                    LuaDLL.lua_pop(L, 1);
                    LuaObject.error(L, errstr);
                    return false;
                }
                LuaDLL.lua_settop(L, nOldTop);
                Debug.Log(" *** Reload Lua File Successful : " + strFileName);
                return true;
            }
            LuaObject.error(L, "Can't find {0}", strFileFullPath);
            return false;
        }

#endif

    }
}

使用:

在Slua启动时,在任何未加载Lua脚本前调用

    ImmediateLuaScript.Instance.AddLuaLoaders(luaState.L);

来完成注册

注意事项:

  1. 热更新Lua代码只是针对具体文件依次执行luaL_loadbuffer和lua_pcall,并不能更新代码中的闭包
  2. 代码中的局部变量会丢失,所以需要在代码书写时注意局部变量的保存(这里涉及Lua框架设计,如setfenv、module、_ENV,甚至面向对象)
  3. 可以通过增大CheckFilesOnce来加快检测速度,但是这样会影响到游戏运行时的性能
  4. 每次更新代码后会在后台输出 “ *** Reload Lua File Successful : FileName ”
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354