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