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

前端之家收集整理的这篇文章主要介绍了cocos2d-x+lua代码热加载(Hot Swap)的研究前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

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

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

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

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

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

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

  1. local listener = cc.EventListenerKeyboard:create();
  2.  
  3. listener:registerScriptHandler(function(keycode,evt)
  4. --print(keycode)
  5. if keycode == 138 then
  6. -- R重新加载代码
  7. reload_script_files();
  8.  
  9. -- 逻辑代码 重新加载所有的配置
  1. -- 逻辑代码 关闭并重新打开当前已打开的窗口
  1. end
  2. end,cc.Handler.EVENT_KEYBOARD_RELEASED);
  3.  
  4. local eventDispatcher = cc.Director:getInstance():getEventDispatcher();
  5. eventDispatcher:addEventListenerWithSceneGraPHPriority(listener,scene);

2、重新加载脚本的实现,这个会递归遍历这个脚本所有依赖的子脚本。所以一般情况下我们只需要加载一个main.lua就足够了。当然后面优化后就可以加载特定的文件而无需从main.lua一直遍历下去
  1. -- 外部库 登记
  2. local package_list = package_list or {
  3. bit = true,lfs = true,cjson = true,pb = true,socket = true,}
  4.  
  5. -- 全局性质类/或禁止重新加载的文件记录
  6. local ignored_file_list = ignored_file_list or {
  7. global = true,}
  8.  
  9. --已重新加载的文件记录
  10. local loaded_file_list = loaded_file_list or {}
  11.  
  12. --视图排版控制
  13. function leading_tag( indent )
  14. -- body
  15. if indent < 1 then
  16. return ''
  17. else
  18. return string.rep( ' |',indent - 1 ) .. ' '
  19. end
  20. end
  21.  
  22. --关键递归重新加载函数
  23. --filename 文件
  24. --indent 递归深度,用于控制排版显示
  25. function recursive_reload( filename,indent )
  26. -- body
  27. if package_list[ filename] then
  28. --对于 外部库,只进行重新加载,不做递归子文件
  29. --卸载旧文件
  30. package.loaded[ filename] = nil
  31.  
  32. --装载信文件
  33. require( filename )
  34.  
  35. --标记"已被重新加载"
  36. loaded_file_list[ filename] = true
  37.  
  38. --print( leading_tag(indent) .. filename .. "... done" )
  39. return true
  40. end
  41.  
  42. --普通文件
  43. --进行 "已被重新加载" 检测
  44. if loaded_file_list[ filename] then
  45. --print( leading_tag(indent) .. filename .. "...already been reloaded IGNORED" )
  46. return true
  47. end
  48.  
  49. local fullPath = cc.FileUtils:getInstance():fullPathForFilename(string.gsub(filename,'%.','/') .. '.lua');
  50. --print(fullPath)
  51. --读取当前文件内容,以进行子文件递归重新加载
  52. local file,err = io.open( fullPath )
  53. if file == nil then
  54. print( string.format( "Failed to reaload file(%s),with error:%s",fullPath,err or "unknown" ) )
  55. return false
  56. end
  57.  
  58. print( leading_tag(indent) .. filename)
  59.  
  60. -- 缓存文件内容,及时关闭文件,否则文件不可写入
  61. local data = {}
  62. local comment = false
  63. for line in file:lines() do
  64. line = string.trim(line);
  65. if string.find(line,'%-%-%[%[%-%-') ~= nil then
  66. comment = true;
  67. end
  68.  
  69. if comment and (string.find(line,'%]%]') ~= nil or string.find(line,'%-%-%]%]%-%-') ~= nil) then
  70. comment = false;
  71. end
  72.  
  73. -- 被注释掉的,和持有特殊标志的require文件不重新加载
  74. local linecomment = (line[1] == '-' and line[2] == '-')
  75. if not comment and not linecomment and string.find(line,'%-%- Ignore Reload') == nil then
  76. table.insert(data,line);
  77. end
  78. end
  79.  
  80. io.close(file)
  81.  
  82. local function getFileName(line)
  83. local begIndex = string.find(line,"'");
  84. local endIndex = string.find(line,"'",(begIndex or 1) + 1)
  85. if begIndex == nil or endIndex == nil then
  86. begIndex = string.find(line,'"');
  87. endIndex = string.find(line,'"',(begIndex or 1) + 1)
  88. end
  89.  
  90. if begIndex == nil or endIndex == nil then
  91. return nil;
  92. end
  93.  
  94. return string.sub(line,begIndex + 1,endIndex - 1)
  95. end
  96.  
  97. -- 先解析文件,加载里面的子文件
  98. for _,line in ipairs(data) do
  99. -- 去除空白符
  100. --line = string.gsub( line,'%s','' )
  101. local subFileName = nil
  102. if string.find(line,'require') ~= nil then
  103. subFileName = getFileName(line);
  104. elseif string.find(line,'import') ~= nil then
  105. -- TODO 兼容import 通过fullPath进行解析
  106. subFileName = nil
  107. end
  108.  
  109. if subFileName then
  110. --printInfo('file: %s subFile: %s',line,subFileName)
  111. --进行递归
  112. local success = recursive_reload( subFileName,indent + 1 )
  113. if not success then
  114. print( string.format( "Failed to reload sub file of (%s)",filename ) )
  115. return false
  116. end
  117.  
  118. end
  119. end
  120.  
  121.  
  122. -- "后序" 处理当前文件...
  123. if ignored_file_list[ filename] then
  124. --忽略 "禁止被重新加载"文件
  125. print( leading_tag(indent) .. filename .. "... IGNORED" )
  126. return true
  127. else
  128.  
  129. --卸载旧文件
  130. package.loaded[ filename] = nil
  131.  
  132. --装载新文件
  133. require( filename )
  134.  
  135. --设置"已被重新加载" 标记
  136. loaded_file_list[ filename] = true
  137. --print( leading_tag(indent) .. filename .. "... done" )
  138. return true
  139. end
  140. end
  141.  
  142. --主入口函数
  143. function reload_script_files()
  144. print( "[reload_script_files...]")
  145.  
  146. loaded_file_list = {}
  147.  
  148. --本项目是以 main.lua 为主文件
  149. recursive_reload( "MainController",0 )
  150. print( "[reload_script_files...done]")
  151.  
  152. return "reload ok"
  153. end

3、具体逻辑层面的处理

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

a、全局变量这样创建 test = test or {} 这样重新加载文件的时候就不会初始化全局变量了。同理,lua文件作用域内的函数调用也需要类似的判定防止重复运行。

b、重新加载配置和重新打开当前窗口都需要针对自己的逻辑特殊处理。

c、理论上我们希望的热加载是对函数实现的替换。所以肯定不会实时的反应修改,比如npc的位置不会因为重新加载脚本而实时改变,这个是我们加载场景的时候就创建好的,如果需要npc站在新的位置上,需要重新加载场景或者运行刷新npc位置的函数。 同理,窗口中控件的位置也不会实时改变,需要我们重新打开窗口。 不过如果我们可以通过代码自动执行相关的刷新操作,其实对最终用户来说是没有什么区别的。同样是可以达到所见即所得的效果

d、当我们重新加载脚本后,所有的脚本内容都会自动更新。但是注册给cocos2d-x的函数不会,估计是因为tolua已经缓存了对应的函数体。这个暂时想不到好的解决方法,因为即便我能够清空tolua中的缓存,也无法找到对应的lua中的新函数。 除非重新注册函数,而重新注册函数其实就相当于重新打开窗口这个过程。


4、实际应用

这里描述的是相对理想的情况,而且由于肯定是热加载功能为游戏框架服务,而不是反过来游戏框架去适应热加载。所以最终达不到真正理想的无缝加载。不过即便如此,通过上面三步操作也可以大大提高游戏开发效率。

当我们写了部分界面的功能后,运行程序,查看结果。发现界面光效位置有偏移,在脚本中修改光效的位置,保存。这个时候光效自动在新的位置出现(依赖于自动重新打开窗口或自动刷新窗口功能,如果没有这个功能,则需要手动点按钮打开窗口)。我们可以继续添加新的功能,比如给按钮绑定函数,保存一下,点击窗口中的按钮看看效果,发现函数实现有错误修改之,然后再保存,再点击按钮看下效果,运行正常,继续开发后续功能

猜你在找的Cocos2d-x相关文章