项目开发中,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