Unity運行時Lua熱更新

項目開發中,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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章