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

来源:互联网 发布:奥尼尔vs霍华德数据 编辑:程序博客网 时间:2024/05/23 21:36

转载请注明出处:帘卷西风的专栏(http://blog.csdn.net/ljxfblog)

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

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

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

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

[cpp] view plaincopy
  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,首先定义下载任务的结构。

[cpp] view plaincopy
  1. struct UpdateItem  
  2. {  
  3.     int version;  
  4.     std::string zipPath;  
  5.     std::string zipUrl;  
  6.   
  7.     UpdateItem(int v, std::string p, std::string u) : version(v), zipPath(p), zipUrl(u) {}  
  8. };  
  9. std::deque<UpdateItem> _versionUrls;  

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

[cpp] view plaincopy
  1. bool UpdateEngine::checkUpdate()  
  2. {  
  3.     if (_versionFileUrl.size() == 0) return false;  
  4.   
  5.     _curl = curl_easy_init();  
  6.     if (!_curl)  
  7.     {  
  8.         CCLOG("can not init curl");  
  9.         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(),把版本队里里面的任务取出来,下载解压,然后写本地版本号,直到版本队列为空。
[cpp] view plaincopy
  1. void UpdateEngine::downloadAndUncompress()  
  2. {  
  3.     while(_versionUrls.size() > 0)  
  4.     {  
  5.         //取出当前第一个需要下载的url  
  6.         UpdateItem item = _versionUrls.front();  
  7.         _packageUrl = item.zipPath + item.zipUrl;  
  8.         char downVersion[32];  
  9.         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全局函数方式来处理。

[cpp] view plaincopy
  1. void UpdateEngineDelegate::onError(ErrorCode errorCode)   
  2. {  
  3.     auto engine = LuaEngine::getInstance();  
  4.     lua_State* pluaState = engine->getLuaStack()->getLuaState();  
  5.     static LuaFunctor<Type_Null, int> selfonError(pluaState, "UpdateLayer.onError");  
  6.     if (!selfonError(LUA_NOREF, nil, errorCode))  
  7.     {  
  8.         log("UpdateLayer.onError failed! Because: %s", selfonError.getLastError());  
  9.     }  
  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文件的目的。
[cpp] view plaincopy
  1. class UpdateEngine : public Node  
  2. {  
  3. public:  
  4.     static UpdateEngine* create(const char* versionFileUrl, const char* storagePath);  
  5.      
  6.     virtual void update();  
  7. };  
       好了,主要代码和思路以及给出来了,现在我们看看如何使用吧!

[plain] view plaincopy
  1. --update.lua  
  2. require "Cocos2d"  
  3.   
  4. local timer_local = nil  
  5.   
  6. --自动更新界面  
  7. UpdateLayer = {}  
  8. local function showUpdate()  
  9.     if timer_local then  
  10.         cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)  
  11.         timer_local = nil  
  12.     end  
  13.   
  14.     local layer = cc.Layer:create()  
  15.     local sceneGame = cc.Scene:create()  
  16.     local winSize = cc.Director:getInstance():getWinSize()  
  17.   
  18.     local bg_list =   
  19.     {  
  20.         "update/loading_bg_1.jpg",  
  21.         "update/loading_bg_2.jpg",  
  22.         "update/loading_bg_3.jpg",  
  23.     }  
  24.     local imageName = bg_list[math.random(3)]  
  25.     local bgSprite = cc.Sprite:create(imageName)  
  26.     bgSprite:setPosition(cc.p(winSize.width / 2, winSize.height / 2))      
  27.     layer:addChild(bgSprite)  
  28.   
  29.     --进度条背景  
  30.     local loadingbg = cc.Sprite:create("update/loading_bd.png")  
  31.     loadingbg:setPosition(cc.p(winSize.width / 2, winSize.height / 2 - 40))  
  32.     layer:addChild(loadingbg)  
  33.            
  34.     --进度条  
  35.     UpdateLayer._loadingBar = ccui.LoadingBar:create("update/loading.png", 0)  
  36.     UpdateLayer._loadingBar:setSize(cc.size(880, 20))  
  37.     UpdateLayer._loadingBar:setPosition(cc.p(winSize.width / 2, winSize.height / 2 - 40))  
  38.     layer:addChild(UpdateLayer._loadingBar)     
  39.       
  40.     --提示信息  
  41.     UpdateLayer._labelNotice = cc.LabelTTF:create("", "res/fonts/DFYuanW7-GB2312.ttf", 25)  
  42.     UpdateLayer._labelNotice:setPosition(cc.p(winSize.width / 2, winSize.height / 2))  
  43.     layer:addChild(UpdateLayer._labelNotice)  
  44.   
  45.     --动画切换场景  
  46.     sceneGame:addChild(layer)  
  47.     local transScene = cc.TransitionFade:create(1.5, sceneGame, cc.c3b(0,0,0))  
  48.     cc.Director:getInstance():replaceScene(transScene)  
  49.   
  50.     --初始化更新引擎  
  51.     local path = cc.FileUtils:getInstance():getWritablePath() .. "temp/"  
  52.     UpdateLayer._updateEngine = UpdateEngine:create("http://203.195.148.180:8080/ts_update/version", path)  
  53.     UpdateLayer._updateEngine:retain()    
  54.       
  55.     --启动定时器等待界面动画完成后开始更新  
  56.     local function startUpdate()  
  57.         UpdateLayer._loadingBar:setPercent(1)  
  58.         UpdateLayer._updateEngine:update()   
  59.         cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)     
  60.         timer_local = nil  
  61.     end  
  62.     UpdateLayer._loadingBar:setPercent(0)  
  63.     UpdateLayer._labelNotice:setString(strg2u("正在检查新版本,请稍等"))  
  64.     timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(startUpdate, 1.5, false)  
  65. end  
  66.   
  67. --显示提示界面  
  68. local function showNotice()  
  69.     if timer_local then  
  70.         cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local)  
  71.         timer_local = nil  
  72.     end  
  73.     local layer = cc.Layer:create()  
  74.     local sceneGame = cc.Scene:create()  
  75.     local winSize = cc.Director:getInstance():getWinSize()  
  76.       
  77.     local notice = cc.Sprite:create("update/notice.png")      
  78.     notice:setPosition(cc.p(winSize.width/2, winSize.height/2));  
  79.       
  80.     layer:addChild(notice)  
  81.     sceneGame:addChild(layer)  
  82.   
  83.     local transScene = cc.TransitionFade:create(1.5, sceneGame, cc.c3b(0,0,0))  
  84.     cc.Director:getInstance():replaceScene(transScene)  
  85.   
  86.     timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(showUpdate, 2.6, false)  
  87. end  
  88.   
  89. --显示logo界面  
  90. local function showLogo()  
  91.     local sceneGame = cc.Scene:create()  
  92.     local winSize = cc.Director:getInstance():getWinSize()  
  93.     local layer = cc.LayerColor:create(cc.c4b(128, 128, 128, 255), winSize.width, winSize.height)     
  94.   
  95.     local logo1 = cc.Sprite:create("update/logo1.png")  
  96.     local logo2 = cc.Sprite:create("update/logo2.png")  
  97.     local logo3 = cc.Sprite:create("update/logo3.png")  
  98.   
  99.     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) .. "\n")  
  210.     print(debug.traceback())  
  211.     print("----------------------------------------")  
  212. end  
  213.   
  214. xpcall(update, __G__TRACKBACK__)  
0 0
原创粉丝点击