栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 游戏开发 > Unity3D

unity lua热重载,编辑器下检查lua文件的变化,前端自动热更lua代码

Unity3D 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

unity lua热重载,编辑器下检查lua文件的变化,前端自动热更lua代码

FileSystemWatcher这里主要用到的一个c#系统类

https://docs.microsoft.com/zh-cn/dotnet/api/system.io.filesystemwatcher?view=net-6.0

watcher = new FileSystemWatcher();
watcher.IncludeSubdirectories = true;
watcher.Path = dirPath;
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.Filter = "*.lua";
watcher.Changed += new FileSystemEventHandler(LuaFileOnChanged);
watcher.EnableRaisingEvents = true;
watcher.InternalBufferSize = 10240;

用于 FileSystemWatcher 监视指定目录中的更改。 可以监视指定目录的文件和子目录中的更改。 可以创建组件来监视本地计算机、网络驱动器或远程计算机上的文件。

FileSystemWatcher 以下是使用到的属性

IncludeSubdirectories

获取或设置一个值,该值指示是否应监视指定路径中的子目录。

Path

获取或设置要监视的目录的路径。

NotifyFilter

获取或设置要监视的更改的类型。

NotifyFilters.LastWrite也是c#系统类,代表上一次有任何内容写入到

Filter

获取或设置用于确定在目录中监视哪些文件的筛选器字符串。

Filter筛选lua文件

Changed

当更改指定 Path 中的文件和目录时发生。

watcher.Changed += handler;hander是回调函数
EnableRaisingEvents

获取或设置一个值,该值指示是否启用此组件。

InternalBufferSize

获取或设置内部缓冲区的大小(以字节为单位)。

其中回调函数LuaFileOnChanged如下

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

FileSystemEventHandler 委托 (System.IO) | Microsoft DocsFileSystemEventHandler 委托

public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

 带两个参数,

参数

Object

事件源。

e

FileSystemEventArgs

包含事件数据的 FileSystemEventArgs。

 FileSystemEventArgs里面有着文件的完全路径

将改动的文件记录下来,并在主线程中对这些文件进行重载 为什么需要记录,原因是因为FileSystemWatcher是多线程的。每新建一个FileSystemWatcher都相当于开了一个新线程。 如果不拿一个列表记录,直接在多线程下重载lua模块,极其容易导致unity崩溃!!! 

使用EditorApplication.update重载,使用这个需要注意,这个在不是运行模式下也会执行,所以需要跳出if (EditorApplication.isPlaying == false)
            return;

EditorApplication.update -= Reload;
EditorApplication.update += Reload;
private static LuaFunction ReloadFunction;
luaState.Require("Base.Main");
ReloadFunction = luaState.GetFunction("hotfix");

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

        foreach (var item in _changeFiles)
        {
            ReloadFunction.Call(item);
        }
        _changeFiles.Clear();
    }

重载的话调用lua中的的Main.lua中的hotfix方法

function hotfix(fileName)
	if BATTLE_EDITOR then
		return
	end

	if not package.loaded[fileName] then 
		return 
	end
	print("hotfix call. file:", fileName)
	--置空之前加载的
	local oldModule = package.loaded[fileName]
	package.loaded[fileName] = nil 

	--重新加载
	local ok, err = pcall(require, fileName)
	if not ok then 
		package.loaded[fileName] = oldModule
		warning("重新加载失败,再尝试一下. error: %s", err)
		return
	end

	local newModule = package.loaded[fileName]
	if type(newModule) ~= "table" or type(oldModule) ~= "table" then 
		warning("hotfix fail. 热更失败. %s还是旧文件", fileName)
		return 
	end

	--更新旧module
	if newModule.__classid then --require 时,luaObject会生成一个新的id.
		newModule.__classid = oldModule.__classid
	end

	local showingPanel = false
	local isUI = not not (newModule.InternalShow and newModule.OnCreate)
	if isUI then --如果是UI,检测一下参数变化..
		showingPanel = OnHotfixUI(newModule, oldModule)
	end

	if showingPanel then 
		UIManager:Instance():HidePanel(oldModule)
	end

	local updateTables = {}
	HotfixStackNum = 0
	UpdateTable(newModule, oldModule, updateTables, HotfixStackNum)

	if oldModule.OnReload then 
		oldModule:OnReload()
	end

	package.loaded[fileName] = oldModule
	if isUI then --重新加载得了..不然OnCreate改不到..
		package.loaded[fileName] = nil
	end
	print("relaod Finish, 更新成功了")
	if showingPanel then 
		UIManager:Instance():ShowPanel(require(fileName), safeunpack(showingPanel._cacheParams or {}))
		--UI直接重新require,不管那些旧函数了..
	end
end

package.loaded记录了lua内部哪些模块被 require,我们这里先--置空之前加载的旧模块
    local oldModule = package.loaded[fileName]
    package.loaded[fileName] = nil 

然后用pcall的方式加载更改的当前文件   local ok, err = pcall(require, fileName)
    if not ok then 
        package.loaded[fileName] = oldModule
        warning("重新加载失败,再尝试一下. error: %s", err)
        return
    end

local newModule = package.loaded[fileName]
    if type(newModule) ~= "table" or type(oldModule) ~= "table" then 
        warning("hotfix fail. 热更失败. %s还是旧文件", fileName)
        return 
    end

一般如果只是重载,到这步就完了,

但是还有个问题就是lua文件会缓存数据,我们就模块的缓存数据不能丢失,所以还需要对旧模块进行处理,这里使用的是UpdateTable函数,里面是把旧模块的新函数和局部数据进行替换

表里的三种元素,数据,不做处理,

函数,UpdateFunc新的函数

表,嵌套UpdateTable处理

原表,嵌套UpdateTable处理

local function UpdateTable(newModule, oldModule, updateTables)
	if type(newModule) ~= "table" or type(oldModule) ~= "table" then 
		return 
	end
	HotfixStackNum = HotfixStackNum + 1
	if HotfixStackNum > 10000 then 
		print(debug.traceback(">>>>>>>>>>>>>>>>>>"))
		warning("stack overflow ????")
		return 
	end
	for k, v in pairs(newModule) do 
		local valueType = type(v)
		local oldValue = oldModule[k]

		if valueType == "function" then --替换func,以及其中的upvalue
			UpdateFunc(v, oldValue, k)
			oldModule[k] = v
		elseif valueType == "table" then 
			if not updateTables[v] then --防止互相引用引起死循环
				-- print(k, v, oldValue)
				updateTables[v] = true
				UpdateTable(v, oldValue, updateTables)
			end
		else
			oldModule[k] = v
		end
	end

	local oldMeta = debug.getmetatable(oldModule)
	local newMeta = debug.getmetatable(newModule)
	if type(oldMeta) == "table" and type(newMeta) == "table" then 
		if not updateTables[newMeta] then
			updateTables[newMeta] = true
			UpdateTable(newMeta, oldMeta, updateTables)
		end
	end
end
--更新旧模块的函数方法
local function UpdateFunc(newFunc, oldFunc, funcName)
	if type(oldFunc) ~= "function" then 
		return 
	end
	local newUpvalueMap = {}
	for i = 1, 10000 do 
		local name, value = debug.getupvalue(newFunc, i)
		if not name then 
			break 
		end
		newUpvalueMap[name] = value
	end

	for i = 1, 10000 do 
		local name, value = debug.getupvalue(oldFunc, i)
		if not name then 
			break 
		end
		local newValue = newUpvalueMap[name]
		if newValue then 
			local newTypeName = type(newValue)
			local oldTypeName = type(value)
			if newTypeName ~= oldTypeName and #name > 0 then --不知道为什么,函数里注释掉upvalue后,会产生一个name为空,value为false的字段.. 
				warning("注意, upvalue类型不一致。 函数名:%s, 变量名:%s, 之前类型为:%s, 当前类型为:%s", funcName, name, oldTypeName, newTypeName)
			end
			debug.setupvalue(oldFunc, i, newValue)			
		end
	end
end

最后再吧package.loaded[fileName] = oldModule指向旧模块,这样就是数据保留,函数代码更新的热重载了

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/900421.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号