x+lua代码热加载(Hot Swap)的研究

代码热加载跟自动更新无关,主要目的是在程序运行的时候动态的替换代码,从而实现不重启程序而更新代码的目的。最理想的情况当然是我修改完代码并保存,然后就可以直接在游戏中看到修改后的效果,这个在实际开发过程中会大大提高效率。 即便达不到理想情况,我们也希望可以实现部分热加载,从而简化操作。例如我们可以仅仅对配置文件、消息文件、界面文件实现热加载,这样策划更新数据后可以直接在游戏中看结果,而不需要重新打开客户端去跑任务。

热加载主要原理其实很简单,lua require文件都会缓存在package.loaded里面,当重新加载文件的时候,把这个置空,然后重新require对应文件就可以了。

实际应用中会有更多需要考虑的因素,所以完全的代码热加载很复杂(原理很简单,但是实现很复杂,需要关注的因素很多)。

CocosIDE展示了代码热加载的效果:编辑场景中图片的位置并保存,然后图片自动放置到新的位置上面了。 这个效果看着非常神奇,但是实际上并没有什么实用价值。因为它的热加载,其实就是重新require文件(基于上面提到的原理)的过程,这个过程中会重新require ‘main.lua’,从而整个游戏都会被重新启动。当我们只有一个简单的场景的时候,就可以实现看起来很完美的热加载。然而,由于实际游戏客户端项目会比这个复杂很多,我们会涉及到多场景、多界面、多状态的维护,,所以想实现没有Bug的热加载是很困难的。

现在只研究了一部分,初步可行,后期完善了会更加实用。

1、按R键重新加载所有的lua脚本。这个后面可以做很多优化。比如windows下检测文件变化,而不需要手动按键。只重新加载改变的文件而不是所有文件都遍历一遍。

local listener = cc.EventListenerKeyboard:create();listener:registerScriptHandler(function(keycode, evt)–print(keycode)if keycode == 138 then– 按R重新加载代码reload_script_files();– 逻辑代码 重新加载所有的配置– 逻辑代码 关闭并重新打开当前已打开的窗口endend, cc.Handler.EVENT_KEYBOARD_RELEASED);local eventDispatcher = cc.Director:getInstance():getEventDispatcher();eventDispatcher:addEventListenerWithSceneGraphPriority(listener, scene);2、重新加载脚本的实现,这个会递归遍历这个脚本所有依赖的子脚本。所以一般情况下我们只需要加载一个main.lua就足够了。当然后面优化后就可以加载特定的文件而无需从main.lua一直遍历下去

— 外部库 登记local package_list = package_list or {bit = true,lfs = true,cjson = true,pb = true,socket = true,}– 全局性质类/或禁止重新加载的文件记录local ignored_file_list = ignored_file_list or {global = true ,}–已重新加载的文件记录local loaded_file_list = loaded_file_list or {}–视图排版控制function leading_tag( indent )– bodyif indent < 1 thenreturn ''elsereturn string.rep( ' |', indent – 1 ) .. ' 'endend–关键递归重新加载函数–filename 文件名–indent 递归深度, 用于控制排版显示function recursive_reload( filename, indent )– bodyif package_list[ filename] then–对于 外部库, 只进行重新加载, 不做递归子文件–卸载旧文件package.loaded[ filename] = nil–装载信文件require( filename )–标记"已被重新加载"loaded_file_list[ filename] = true–print( leading_tag(indent) .. filename .. "… done" )return trueend–普通文件–进行 "已被重新加载" 检测if loaded_file_list[ filename] then–print( leading_tag(indent) .. filename .. "…already been reloaded IGNORED" )return trueendlocal fullPath = cc.FileUtils:getInstance():fullPathForFilename(string.gsub(filename, '%.', '/') .. '.lua');–print(fullPath)–读取当前文件内容, 以进行子文件递归重新加载local file, err = io.open( fullPath )if file == nil thenprint( string.format( "failed to reaload file(%s), with error:%s", fullPath, err or "unknown" ) )return falseendprint( leading_tag(indent) .. filename)– 缓存文件内容,及时关闭文件,否则文件不可写入local data = {}local comment = falsefor line in file:lines() doline = string.trim(line);if string.find(line, '%-%-%[%[%-%-') ~= nil thencomment = true;endif comment and (string.find(line, '%]%]') ~= nil or string.find(line, '%-%-%]%]%-%-') ~= nil) thencomment = false;end– 被注释掉的,和持有特殊标志的require文件不重新加载local linecomment = (line[1] == '-' and line[2] == '-')if not comment and not linecomment and string.find(line, '%-%- Ignore Reload') == nil thentable.insert(data, line);endendio.close(file)local function getFileName(line)local begIndex = string.find(line, "'");local endIndex = string.find(line, "'", (begIndex or 1) + 1)if begIndex == nil or endIndex == nil thenbegIndex = string.find(line, '"');endIndex = string.find(line, '"', (begIndex or 1) + 1)endif begIndex == nil or endIndex == nil thenreturn nil;endreturn string.sub(line, begIndex + 1, endIndex – 1)end– 先解析文件,加载里面的子文件for _,line in ipairs(data) do– 去除空白符–line = string.gsub( line, '%s', '' )local subFileName = nilif string.find(line, 'require') ~= nil thensubFileName = getFileName(line);elseif string.find(line, 'import') ~= nil then– TODO 兼容import 通过fullPath进行解析subFileName = nilendif subFileName then–printInfo('file: %ssubFile: %s', line, subFileName)–进行递归local success = recursive_reload( subFileName, indent + 1 )if not success thenprint( string.format( "failed to reload sub file of (%s)", filename ) )return falseendendend– "后序" 处理当前文件…if ignored_file_list[ filename] then–忽略 "禁止被重新加载"的文件print( leading_tag(indent) .. filename .. "… IGNORED" )return trueelse–卸载旧文件package.loaded[ filename] = nil–装载新文件require( filename )–设置"已被重新加载" 标记loaded_file_list[ filename] = true–print( leading_tag(indent) .. filename .. "… done" )return trueendend–主入口函数function reload_script_files()print( "[reload_script_files…]")loaded_file_list = {}–本项目是以 main.lua 为主文件recursive_reload( "MainController", 0 )print( "[reload_script_files…done]")return "reload ok"end3、具体逻辑层面的处理

lua的热加载主要麻烦的地方其实在逻辑层面的处理上面,一开始写代码的时候就要注意一些问题。比如:

收敛自己的脾气,偶尔要刻意沉默,

x+lua代码热加载(Hot Swap)的研究

相关文章:

你感兴趣的文章:

标签云: