淘先锋技术网

首页 1 2 3 4 5 6 7

项目开发中,Lua代码被require之后,如果出错,就需要重新打开项目。这样非常影响开发效率。因此,Lua中使用热更新,非常的必要。

由于实际开发项目中,运行环境复杂。因此,这里我对项目中 界面的代码做了重新加载,其实,一个游戏项目,系统的工作量,特别是界面的工作量,占了整个工作量的绝大部分。而对其他必要的类,进行了函数的重新加载,这样,就能够在运行时修改函数,添加日志,追踪错误。

整体思路是:当保存代码时,C#感知代码的修改,通知Lua进行代码更新。

实际在开发过程中,我是在登录到主场景之后,收集所有已经加载到游戏中的lua代码,因为这部分代码主要是数据和管理类,而界面代码基本都是在打开游戏界面的时候require的,因此,可以对这些后require的代码,直接替换。

新的项目开发中,感觉效率比之前快多了,可以在游戏运行中开发系统,也能直接使用修改后的prefab,避免了重启游戏的过程。

感知代码修改使用了 C#的 FileSystemWatcher类,lua的变化主要通过package.loaded获取和替换已经require的类和函数

using UnityEngine;
using System.IO;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// 监听文件改变
/// </summary>
public sealed class FileWatcher: MonoBehaviour
{
#if UNITY_EDITOR_WIN 
    class WatchInfo
    {
        FileSystemWatcher watcher;
        string fileter;
        List<string> changeFiles;
        System.Action<string> changeAction;
        string dirPath;

        public WatchInfo(string _dirPath, string _filter, System.Action<string> _changeAction)
        {
            watcher = new FileSystemWatcher();
            changeFiles = new List<string>();
            fileter = _filter;
            dirPath = _dirPath;
            changeAction = _changeAction;
            CreateWatcher(watcher,dirPath,changeFiles, fileter);
        }

        public void UpdateLs()
        {
            if (changeFiles.Count > 0 && null != changeAction)
            {
                foreach (var changeFile in changeFiles)
                {
                    changeAction(changeFile);
                }
                changeFiles.Clear();
            }
        }

        private void AddToLs(List<string> ls, string elem)
        {
            if (!ls.Contains(elem))
            {
                ls.Add(elem);
            }
        }
        private void CreateWatcher(FileSystemWatcher watcher, string path, List<string> changeLs, string fileFilter)
        {
            CreateWatcher(watcher, path, fileFilter, (object source, FileSystemEventArgs e) =>
            {
                AddToLs(changeLs, e.FullPath);
            }, (object source, RenamedEventArgs e) => {
                AddToLs(changeLs, e.FullPath);
            });
        }
        private void CreateWatcher(FileSystemWatcher watcher, string path, string fileFilter, FileSystemEventHandler onChanged, RenamedEventHandler onRenamed)
        {
            watcher.Path = Path.GetFullPath(path);

            // Watch for changes in LastAccess and LastWrite times, and
            // the renaming of files or directories.
            watcher.NotifyFilter = NotifyFilters.LastWrite;

            // Only watch text files.
            watcher.Filter = fileFilter;
            watcher.IncludeSubdirectories = true;

            // Add event handlers.
            watcher.Changed += onChanged;
            watcher.Created += onChanged;
            watcher.Deleted += onChanged;
            watcher.Renamed += onRenamed;

            // Begin watching.
            watcher.EnableRaisingEvents = true;
        }

    }
    private static FileWatcher _instance;

    private List<WatchInfo> watchInfos = new List<WatchInfo>();
    public static void Create(GameObject go)
    {
        _instance = go.AddComponent<FileWatcher>();
    }

    IEnumerator Start()
    {
        watchInfos.Add(new WatchInfo(GameSetting.codePath, "*.lua",(changeCode)=> {
            string s = changeCode.Replace(Path.GetFullPath(GameSetting.codePath + "src/"), "").Replace("\\", ".").Replace(".lua", "");
            Game.Client.Instance.OnMessage("codechange:" + s);
        }));
        yield return null;
    }

    void LateUpdate()
    {
        foreach (var watcher in watchInfos)
        {
            watcher.UpdateLs();
        }
    }

# endif
} 

Lua代码

local initLoadedLua = nil

local function setFuncUpValue(newFunc,oldFunc)
    local uvIndex = 1
    while true do
        local name, value = debug.getupvalue(oldFunc, uvIndex)
        if name == nil then
            break
        end
        debug.setupvalue(newFunc,uvIndex,value)
        uvIndex = uvIndex + 1
    end
end

local function onCodeChange(codePath)
    printyellow("OnCode Change:")
    if nil == initLoadedLua then
       return 
    end
    if package.loaded[codePath] ~= nil then
        printyellow("Modify:"..codePath)
        local oldCodeObj = package.loaded[codePath]
        package.loaded[codePath] = nil
        xpcall(function() 
            local newCodeObj = require(codePath)
            for k,v in pairs(newCodeObj) do
                if type(v) == "function" and oldCodeObj[k] ~= v then
                    setFuncUpValue(v,oldCodeObj[k])
                    oldCodeObj[k] =v
                end
            end
        end,function() logError(debug.traceback()) end)
        package.loaded[codePath] = oldCodeObj
       
    end
    local fileName = commons.utils.getfilename(codePath)
    for k,v in pairs(package.loaded) do
        if not initLoadedLua[k] then
            package.loaded[k] = nil
            printyellow(k)
        end
    end
    
end

local function getInitLoadedLua()
    if initLoadedLua == nil then
        initLoadedLua = {}
        for k,v in pairs(package.loaded) do
            initLoadedLua[k] = true
        end
    end 
end

知乎地址:https://zhuanlan.zhihu.com/p/91022294