这几天由于项目组需要一直在琢磨一个功能,就是如何在unity编辑器下不需要重启游戏就能让lua文件改动后立刻生效。如果能够实现这个功能,那会大幅提高开发效率。查了一圈,网上的结果都不太满意,要么只有理论没有源码,要么有源码但是考虑的情况过于简单。所以自己打算写博客告诉大家,我是怎么实现的,并且提供完整源码。github工程地址 使用的unity2019.3.0 + xlua。改成其他lua也是可以用,只要在传入luaEnv的时候做相应改动就可以了。


这个功能大体分为两大步:

  • 检测哪些lua文件发生变化
  • 重新加载lua模块,保留数据,替换函数(因为我们做了函数、数据分离,所以我这个工程目前只考虑替换函数)
    其中重载lua模块还要考虑以下问题:
  • 其它模块缓存了旧模块的函数的处理
  • upvalue值的处理
  • 需要更新的模块的元表的处理
  • 对于正在运行的函数的处理,比如update

内容有点多,一篇文章应该塞不下,所以这第一篇先讲第一步,怎么检测哪些lua文件发生变化。 涉及到工程里两个类:

  • DirectoryWatcher,检测文件变化
  • LuaFileWatcher,处理检测文件发生变化后该做什么事情

DirectoryWatcher
public class DirectoryWatcher
{
    public DirectoryWatcher(string dirPath, FileSystemEventHandler handler)
    {
        Debug.Log("create directory watcher");
        CreateWatch(dirPath, handler);
    }

    void CreateWatch(string dirPath, FileSystemEventHandler handler)
    {
        if (!Directory.Exists(dirPath)) return;

        var watcher = new FileSystemWatcher();
        watcher.IncludeSubdirectories = true; //includeSubdirectories;
        watcher.Path = dirPath;
        watcher.NotifyFilter = NotifyFilters.LastWrite;
        watcher.Filter = "*。lua";
        watcher.Changed += handler;
        watcher.EnableRaisingEvents = true;
        watcher.InternalBufferSize = 10240;
    }
}

这里涉及到一个C#的系统类FileSystemWatcher

FileSystemWatcher

监控指定文件或目录的文件的创建、删除、改动、重命名等活动。可以动态地定义需要监控的文件类型及文件属性改动的类型。

  • IncludeSubdirectories 是否包含子文件。
  • Path 目标路径。
  • NotifyFilter 设置文件的哪些属性的变动会触发 Changed事件。这里设置成了当文件内容发生变化时会触发。
  • Filter 设置筛选字符串,用于确定在目录中监视哪些类型的文件。这里只需要筛选 .lua后缀文件即可。
  • Changed 文件发生改变时的监听事件,需要一个FileSystemEventHandler 类型的委托。除了Changed外还可以监听RenamedDeletedCreated
  • EnableRaisingEvents 设置是否开始监控,默认为false
  • InternalBufferSize 能够监听的改动大小。如果监听事件没有触发,请把这个值设得大一点。
    还有一些其他属性,详细看MSDN关于FileSystemWatcher

LuaFileWatcher
public  class LuaFileWatcher
{
    //private static ReloadDelegate ReloadFunction;
    
    private static HashSet<string> _changedFiles = new HashSet<string>();
    
    public static void CreateLuaFileWatcher(LuaEnv luaEnv)
    {
        var scriptPath = Path.Combine(Application.dataPath, "LuaScripts");
        var directoryWatcher =
            new DirectoryWatcher(scriptPath, new FileSystemEventHandler(LuaFileOnChanged));
        //ReloadFunction = luaEnv.Global.Get<ReloadDelegate>("hotfix");
        EditorApplication.update -= Reload;
        EditorApplication.update += Reload;
    }

    private static void LuaFileOnChanged(object obj, FileSystemEventArgs args)
    {
        var fullPath = args.FullPath;
        var luaFolderName = "LuaScripts";
        var requirePath = fullPath.Replace(".lua", "");
        var luaScriptIndex = requirePath.IndexOf(luaFolderName) + luaFolderName.Length + 1;
        requirePath = requirePath.Substring(luaScriptIndex);
        requirePath = requirePath.Replace('\\','.');
        _changedFiles.Add(requirePath);
    }

    private static void Reload()
    {
        if (EditorApplication.isPlaying == false)
        {
            return;
        }
        if (_changedFiles.Count == 0)
        {
            return;
        }

        foreach (var file in _changedFiles)
        {
            //ReloadFunction(file);
            Debug.Log("Reload:" + file);
        }
        _changedFiles.Clear();
    }
}

LuaFileWatcher做了以下几件事情:

  • 将文件路径转化为lua里调用require函数需要的参数
    之前提到的FileSystemEventHandler的委托有两个参数,一个是object即对应的文件,另一个参数FileSystemEventArgs包含了文件的数据,其中有FullPath即文件路径。是文件的完整路径,如 “F:\Git\LuaRuntimeHotfix\Assets\LuaScripts\NewDirectory1\Test.lua”。需要转化成 “NewDirectory1.Test” 。"LuaScripts"是我的工程里的lua文件夹名称。
    这一步是LuaFileOnChanged这个函数的主要内容。
  • 将改动的文件记录下来,并在主线程中对这些文件进行重载
    为什么需要记录,原因是因为FileSystemWatcher是多线程的。每新建一个FileSystemWatcher都相当于开了一个新线程。 如果不拿一个列表记录,直接在多线程下重载lua模块,极其容易导致unity崩溃!!! 这个我不知道是unity的限制还是lua的限制,总之这个崩溃问题困扰了我好几天,最后才发现原因。
    解决办法也很简单,拿一个列表去存文件路径,再到主线程下处理,让EditorApplication.update绑定处理函数,这样unity编辑器每刷新一次就会调用处理函数。
    需要注意的一点是,EditorApplication.update即使unity编辑器没有在运行模式也会跑,所以需要加入下面代码判断编辑器是不是在运行模式:
if (EditorApplication.isPlaying == false)
	 {
	     return;
	 }

这里我使用HashSet 这个数据结构,因为HashSet不会存储重复元素,如果用List还要考虑列表里可能有重复元素的情况。
这一步是Reload这个函数的主要内容,当然这个函数里还需要调用lua端的方法,这部分内容下篇文章再说,今天先做到能够打印出需要修改的全部模块路径。


关于第一步检测哪些lua文件发生变化的代码就讲解到这里,剩下的内容下一篇文章进行讲解。

系列文章:
【Lua运行时热重载功能实现①】检测Lua文件发生变化【Lua运行时热重载功能实现②】重载Lua模块、替换函数



知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264