关于cocos2dx客户端程序的自动更新解决方案

转载自:帘卷西风的专栏(http://blog.csdn.net/ljxfblog)

随着手机游戏的不断发展,游戏包也越来越大,手机网络游戏已经超过100M了,对于玩家来说,如果每次更新都要重新下载,那简直是灾难。而且如果上IOS平台,每次重新发包都要审核,劳神费力。所以当前的主流手游都开始提供自动更新的功能,在不改动C++代码的前提下,使用lua或者js进行业务逻辑开发,然后自动更新脚本和资源,方便玩家也方便研发者。

以前做端游的时候,自动更新是一个大工程,不仅要能更新资源和脚本,还要更新dll文件等,后期甚至要支持P2P,手游目前基本上都使用http方式。cocos2dx也提供了一个基础功能类AssetsManager,但是不太完善,只支持单包下载,版本控制基本没有。因此我决定在AssetsManager的基础上扩展一下这个功能。

先明确一下需求,自动更新需要做些什么?鉴于手游打包的方式,我们需要能够实现多版本增量更新游戏资源和脚本。明确设计思路,首先,服务器端,我们要要有一个版本计划,每一个版本和上一个版本之间的变化内容,打成一个zip包,并为之分配一个版本,然后将所有版本的信息放到http服务器上。然后,客户端程序启动的时候我们都需要读取服务器所有的版本信息,并与客户端版本进行比较,大于本地版本的都是需要下载的内容,将下载信息缓存起来,然后依次下载并解压,然后再正式进入游戏。

好了,我们先设计一下版本信息的格式吧!大家可以看看。

1. http://203.195.148.180:8080/ts_update/ 1 1001 scene.zip
2. //格式为:文件包目录(http://203.195.148.180:8080/ts_update/) 总版本数量(1)
3. //版本号1(1001) 版本文件1(scene.zip) ... 版本号n(1001) 版本文件n(scene.zip)
我们现在开始改造AssetsManager,首先定义下载任务的结构。

 

 

01. struct UpdateItem
02. {
03. int version;
04. std::string zipPath;
05. std::string zipUrl;
06.  
07. UpdateItem(int v, std::string p, std::string u) : version(v), zipPath(p), zipUrl(u) {}
08. };
09. std::deque<UpdateItem> _versionUrls;

 

然后改造bool checkUpdate(),这里把服务器的版本内容解析出来,放到一个队列_versionUrls里面。

 

01. bool UpdateEngine::checkUpdate()
02. {
03. if (_versionFileUrl.size() == 0return false;
04.  
05. _curl = curl_easy_init();
06. if (!_curl)
07. {
08. CCLOG("can not init curl");
09. return false;
10. }
11. _version.clear();
12.  
13. CURLcode res;
14. curl_easy_setopt(_curl, CURLOPT_URL, _versionFileUrl.c_str());
15. curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, 0L);
16. curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, getVersionCode);
17. curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &_version);
18. if (_connectionTimeout) curl_easy_setopt(_curl, CURLOPT_CONNECTTIMEOUT, _connectionTimeout);
19. curl_easy_setopt(_curl, CURLOPT_NOSIGNAL, 1L);
20. curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_LIMIT, LOW_SPEED_LIMIT);
21. curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_TIME, LOW_SPEED_TIME);
22. res = curl_easy_perform(_curl);
23.  
24. if (res != 0)
25. {
26. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{
27. if (this->_delegate)
28. this->_delegate->onError(ErrorCode::NETWORK);
29. });
30. CCLOG("can not get version file content, error code is %d", res);
31. return false;
32. }
33.  
34. int localVer = getVersion();
35. StringBuffer buff(_version);
36.  
37. int version;
38. short versionCnt;
39. string versionUrl, pathUrl;
40. buff >> pathUrl >> versionCnt;
41. for (short i = 0; i < versionCnt; ++i)
42. {
43. buff >> version >> versionUrl;
44. if (version > localVer)
45. {
46. _versionUrls.push_back(UpdateItem(version, pathUrl, versionUrl));
47. }
48. }
49. if (_versionUrls.size() <= 0)
50. {
51. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{
52. if (this->_delegate)
53. this->_delegate->onError(ErrorCode::NO_NEW_VERSION);
54. });
55. CCLOG("there is not new version");
56. return false;
57. }
58. CCLOG("there is %d new version!", _versionUrls.size());
59.  
60. //设置下载目录,不存在则创建目录
61. _downloadPath = FileUtils::getInstance()->getWritablePath();
62. _downloadPath += "download_temp/";
63. createDirectory(_downloadPath.c_str());
64. return true;
65. }
其次,改造void downloadAndUncompress(),把版本队里里面的任务取出来,下载解压,然后写本地版本号,直到版本队列为空。
01. void UpdateEngine::downloadAndUncompress()
02. {
03. while(_versionUrls.size() > 0)
04. {
05. //取出当前第一个需要下载的url
06. UpdateItem item = _versionUrls.front();
07. _packageUrl = item.zipPath + item.zipUrl;
08. char downVersion[32];
09. sprintf(downVersion, "%d", item.version);
10. _version = downVersion;
11.  
12. //通知文件下载
13. std::string zipUrl = item.zipUrl;
14. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this, zipUrl]{
15. if (this->_delegate)
16. this->_delegate->onDownload(zipUrl);
17. });
18.  
19. //开始下载,下载失败退出
20. if (!downLoad())
21. {
22. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{
23. if (this->_delegate)
24. this->_delegate->onError(ErrorCode::UNDOWNED);
25. });
26. break;
27. }
28.  
29. //通知文件压缩
30. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this, zipUrl]{
31. if (this->_delegate)
32. this->_delegate->onUncompress(zipUrl);
33. });
34.  
35. //解压下载的zip文件
36. string outFileName = _downloadPath + TEMP_PACKAGE_FILE_NAME;
37. if (!uncompress(outFileName))
38. {
39. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{
40. if (this->_delegate)
41. this->_delegate->onError(ErrorCode::UNCOMPRESS);
42. });
43. break;
44. }
45. //解压成功,任务出队列,写本地版本号
46. _versionUrls.pop_front();
47. Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{           
48. //写本地版本号
49. UserDefault::getInstance()->setStringForKey("localVersion", _version);
50. UserDefault::getInstance()->flush();
51.  
52. //删除本次下载的文件
53. string zipfileName = this->_downloadPath + TEMP_PACKAGE_FILE_NAME;
54. if (remove(zipfileName.c_str()) != 0)
55. {
56. CCLOG("can not remove downloaded zip file %s", zipfileName.c_str());
57. }
58. //如果更新任务已经完成,通知更新成功
59. if(_versionUrls.size() <= 0 && this->_delegate)
60. this->_delegate->onSuccess();
61. });
62. }
63.  
64. curl_easy_cleanup(_curl);
65. _isDownloading = false;
66. }
再次,对lua进行支持,原来的方案是写了一个脚本代理类,但是写lua的中间代码比较麻烦,我采用了比较简单的方式,通常自动更新是全局的,所以自动更新的信息,我通过调用lua全局函数方式来处理。

 

 

01. void UpdateEngineDelegate::onError(ErrorCode errorCode)
02. {
03. auto engine = LuaEngine::getInstance();
04. lua_State* pluaState = engine->getLuaStack()->getLuaState();
05. static LuaFunctor<Type_Null, int> selfonError(pluaState, "UpdateLayer.onError");
06. if (!selfonError(LUA_NOREF, nil, errorCode))
07. {
08. log("UpdateLayer.onError failed! Because: %s", selfonError.getLastError());
09. }
10. }
11.  
12. void UpdateEngineDelegate::onProgress(int percent, int type /* = 1 */)
13. {
14. auto engine = LuaEngine::getInstance();
15. lua_State* pluaState = engine->getLuaStack()->getLuaState();
16. static LuaFunctor<Type_Null, intint> selfonProgress(pluaState, "UpdateLayer.onProgress");
17. if (!selfonProgress(LUA_NOREF, nil, percent, type))
18. {
19. log("UpdateLayer.onProgress failed! Because: %s", selfonProgress.getLastError());
20. }
21. }
22.  
23. void UpdateEngineDelegate::onSuccess()
24. {
25. auto engine = LuaEngine::getInstance();
26. lua_State* pluaState = engine->getLuaStack()->getLuaState();
27. static LuaFunctor<Type_Null> selfonSuccess(pluaState, "UpdateLayer.onSuccess");
28. if (!selfonSuccess(LUA_NOREF, nil))
29. {
30. log("UpdateLayer.onSuccess failed! Because: %s", selfonSuccess.getLastError());
31. }
32. }
33.  
34. void UpdateEngineDelegate::onDownload(string packUrl)
35. {
36. auto engine = LuaEngine::getInstance();
37. lua_State* pluaState = engine->getLuaStack()->getLuaState();
38. static LuaFunctor<Type_Null, string> selfonDownload(pluaState, "UpdateLayer.onDownload");
39. if (!selfonDownload(LUA_NOREF, nil, packUrl))
40. {
41. log("UpdateLayer.onDownload failed! Because: %s", selfonDownload.getLastError());
42. }
43. }
44.  
45. void UpdateEngineDelegate::onUncompress(string packUrl)
46. {
47. auto engine = LuaEngine::getInstance();
48. lua_State* pluaState = engine->getLuaStack()->getLuaState();
49. static LuaFunctor<Type_Null, string> selfonUncompress(pluaState, "UpdateLayer.onUncompress");
50. if (!selfonUncompress(LUA_NOREF, nil, packUrl))
51. {
52. log("UpdateLayer.onUncompress failed! Because: %s", selfonUncompress.getLastError());
53. }
54. }
最后把UpdateEngine使用PKG方式暴露给lua使用,这个lua文件是app里面调用的第一个lua文件,里面没有任何游戏内容相关,游戏内容都从main.lua开始加载,达到更新完毕后在加载其他lua文件的目的。
1. class UpdateEngine : public Node
2. {
3. public:
4. static UpdateEngine* create(const char* versionFileUrl, const char* storagePath);
5.  
6. virtual void update();
7. };
好了,主要代码和思路以及给出来了,现在我们看看如何使用吧!

 

 

001. --update.lua
002. require "Cocos2d"
003.  
004. local timer_local = nil
005.  
006. --自动更新界面
007. UpdateLayer = {}
008. local function showUpdate()
009. if timer_local then
010. cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)
011. timer_local = nil
012. end
013.  
014. local layer = cc.Layer:create()
015. local sceneGame = cc.Scene:create()
016. local winSize = cc.Director:getInstance():getWinSize()
017.  
018. local bg_list =
019. {
020. "update/loading_bg_1.jpg",
021. "update/loading_bg_2.jpg",
022. "update/loading_bg_3.jpg",
023. }
024. local imageName = bg_list[math.random(3)]
025. local bgSprite = cc.Sprite:create(imageName)
026. bgSprite:setPosition(cc.p(winSize.width / 2, winSize.height / 2))   
027. layer:addChild(bgSprite)
028.  
029. --进度条背景
030. local loadingbg = cc.Sprite:create("update/loading_bd.png")
031. loadingbg:setPosition(cc.p(winSize.width / 2, winSize.height / 2 40))
032. layer:addChild(loadingbg)
033.  
034. --进度条
035. UpdateLayer._loadingBar = ccui.LoadingBar:create("update/loading.png"0)
036. UpdateLayer._loadingBar:setSize(cc.size(88020))
037. UpdateLayer._loadingBar:setPosition(cc.p(winSize.width / 2, winSize.height / 2 40))
038. layer:addChild(UpdateLayer._loadingBar)  
039.  
040. --提示信息
041. UpdateLayer._labelNotice = cc.LabelTTF:create("""res/fonts/DFYuanW7-GB2312.ttf"25)
042. UpdateLayer._labelNotice:setPosition(cc.p(winSize.width / 2, winSize.height / 2))
043. layer:addChild(UpdateLayer._labelNotice)
044.  
045. --动画切换场景
046. sceneGame:addChild(layer)
047. local transScene = cc.TransitionFade:create(1.5, sceneGame, cc.c3b(0,0,0))
048. cc.Director:getInstance():replaceScene(transScene)
049.  
050. --初始化更新引擎
051. local path = cc.FileUtils:getInstance():getWritablePath() .. "temp/"
052. UpdateLayer._updateEngine = UpdateEngine:create("http://203.195.148.180:8080/ts_update/version", path)
053. UpdateLayer._updateEngine:retain() 
054.  
055. --启动定时器等待界面动画完成后开始更新
056. local function startUpdate()
057. UpdateLayer._loadingBar:setPercent(1)
058. UpdateLayer._updateEngine:update()
059. cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)  
060. timer_local = nil
061. end
062. UpdateLayer._loadingBar:setPercent(0)
063. UpdateLayer._labelNotice:setString(strg2u("正在检查新版本,请稍等"))
064. timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(startUpdate, 1.5,false)
065. end
066.  
067. --显示提示界面
068. local function showNotice()
069. if timer_local then
070. cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)
071. timer_local = nil
072. end
073. local layer = cc.Layer:create()
074. local sceneGame = cc.Scene:create()
075. local winSize = cc.Director:getInstance():getWinSize()
076.  
077. local notice = cc.Sprite:create("update/notice.png")   
078. notice:setPosition(cc.p(winSize.width/2, winSize.height/2));
079.  
080. layer:addChild(notice)
081. sceneGame:addChild(layer)
082.  
083. local transScene = cc.TransitionFade:create(1.5, sceneGame, cc.c3b(0,0,0))
084. cc.Director:getInstance():replaceScene(transScene)
085.  
086. timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(showUpdate, 2.6,false)
087. end
088.  
089. --显示logo界面
090. local function showLogo()
091. local sceneGame = cc.Scene:create()
092. local winSize = cc.Director:getInstance():getWinSize()
093. local layer = cc.LayerColor:create(cc.c4b(128128128255), winSize.width, winSize.height)  
094.  
095. local logo1 = cc.Sprite:create("update/logo1.png")
096. local logo2 = cc.Sprite:create("update/logo2.png")
097. local logo3 = cc.Sprite:create("update/logo3.png")
098.  
099. logo3:setPosition(cc.p(winSize.width / 2, winSize.height / 2))
100. logo2:setPosition(cc.p(winSize.width - logo2:getContentSize().width / 2, logo2:getContentSize().height / 2))
101. logo1:setPosition(cc.p(winSize.width - logo1:getContentSize().width / 2, logo2:getContentSize().height + logo1:getContentSize().height / 2))
102.  
103. layer:addChild(logo1)
104. layer:addChild(logo2)
105. layer:addChild(logo3)
106.  
107. sceneGame:addChild(layer)
108. cc.Director:getInstance():runWithScene(sceneGame)  
109.  
110. timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(showNotice, 1,false)
111. end
112.  
113. --更新主函数
114. function update()
115. collectgarbage("collect")
116. -- avoid memory leak
117. collectgarbage("setpause"100)
118. collectgarbage("setstepmul"5000)
119. math.randomseed(os.time())
120. math.random(os.time())
121. math.random(os.time())
122. math.random(os.time()) 
123.  
124. --显示logoo界面
125. showLogo()
126. end
127.  
128. --c++更新信息回调
129. local ErrorCode =
130. {
131. NETWORK = 0,
132. CREATE_FILE = 1,
133. NO_NEW_VERSION = 2,
134. UNDOWNED = 3,
135. UNCOMPRESS = 4,
136. }
137.  
138. local function finishUpdate()
139. UpdateLayer.percent = 0   
140. local function addPercent()
141. if UpdateLayer.percent < 200 then
142. UpdateLayer.percent = UpdateLayer.percent + 2
143. if UpdateLayer.percent < 100 then
144. UpdateLayer._loadingBar:setPercent(UpdateLayer.percent)
145. elseif UpdateLayer.percent <= 100 then
146. UpdateLayer._loadingBar:setPercent(UpdateLayer.percent)
147. UpdateLayer._labelNotice:setString(strg2u("当前版本已经最新,无需更新"))
148. elseif UpdateLayer.percent >= 200 then
149. cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)
150. timer_local = nil
151.  
152. --进入游戏界面
153. UpdateLayer = nil
154. require "src.main"
155. end
156. end
157. end
158. timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(addPercent, 0.05,false)
159. end
160.  
161. function UpdateLayer.onError(errorCode)
162. if errorCode == ErrorCode.NO_NEW_VERSION then
163. finishUpdate()
164. elseif errorCode == ErrorCode.NETWORK then
165. UpdateLayer._labelNotice:setString(strg2u("获取服务器版本失败,请检查您的网络"))
166. elseif errorCode == ErrorCode.UNDOWNED then
167. UpdateLayer._labelNotice:setString(strg2u("下载文件失败,请检查您的网络"))
168. elseif errorCode == ErrorCode.UNCOMPRESS then
169. UpdateLayer._labelNotice:setString(strg2u("解压文件失败,请关闭程序重新更新"))
170. end
171. end
172.  
173. function UpdateLayer.onProgress(percent)
174. local progress = string.format("正在下载文件:%s(%d%%)", UpdateLayer._downfile, percent)
175. print(strg2u(progress))
176. UpdateLayer._labelNotice:setString(strg2u(progress))
177. UpdateLayer._loadingBar:setPercent(percent)
178. end
179.  
180. function UpdateLayer.onSuccess()
181. UpdateLayer._labelNotice:setString(strg2u("自动更新完毕"))
182. local function updateSuccess()    
183. cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local) 
184. timer_local = nil
185.  
186. --进入游戏界面
187. UpdateLayer = nil
188. require "src.main"
189. end
190. timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(updateSuccess, 2,false)
191. end
192.  
193. function UpdateLayer.onDownload(str)
194. UpdateLayer._downfile = str
195. local downfile = string.format("正在下载文件:%s(0%%)", str)
196. print(strg2u(downfile))
197. UpdateLayer._labelNotice:setString(strg2u(downfile))
198. end
199.  
200. function UpdateLayer.onUncompress(str)
201. local uncompress = string.format("正在解压文件:%s", str)
202. print(strg2u(uncompress))
203. UpdateLayer._labelNotice:setString(strg2u(uncompress))
204. end
205.  
206. -- for CCLuaEngine traceback
207. function __G__TRACKBACK__(msg)
208. print("----------------------------------------")
209. print("LUA ERROR: " .. tostring(msg) .. "
210. ")
211. print(debug.traceback())
212. print("----------------------------------------")
213. end
214.  
215. xpcall(update, __G__TRACKBACK__)
最后说明一点,需要把下载解压的目录加到文件搜索的最前面,保证cocos2dx优先加载解压的lua文件和资源。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值