首页 > 代码库 > 关于cocos2dx客户端程序的自动更新解决方案
关于cocos2dx客户端程序的自动更新解决方案
转载请注明出处:帘卷西风的专栏(http://blog.csdn.net/ljxfblog)
随着手机游戏的不断发展,游戏包也越来越大,手机网络游戏已经超过100M了,对于玩家来说,如果每次更新都要重新下载,那简直是灾难。而且如果上IOS平台,每次重新发包都要审核,劳神费力。所以当前的主流手游都开始提供自动更新的功能,在不改动C++代码的前提下,使用lua或者js进行业务逻辑开发,然后自动更新脚本和资源,方便玩家也方便研发者。
以前做端游的时候,自动更新是一个大工程,不仅要能更新资源和脚本,还要更新dll文件等,后期甚至要支持P2P,手游目前基本上都使用http方式。cocos2dx也提供了一个基础功能类AssetsManager,但是不太完善,只支持单包下载,版本控制基本没有。因此我决定在AssetsManager的基础上扩展一下这个功能。
先明确一下需求,自动更新需要做些什么?鉴于手游打包的方式,我们需要能够实现多版本增量更新游戏资源和脚本。明确设计思路,首先,服务器端,我们要要有一个版本计划,每一个版本和上一个版本之间的变化内容,打成一个zip包,并为之分配一个版本,然后将所有版本的信息放到http服务器上。然后,客户端程序启动的时候我们都需要读取服务器所有的版本信息,并与客户端版本进行比较,大于本地版本的都是需要下载的内容,将下载信息缓存起来,然后依次下载并解压,然后再正式进入游戏。
好了,我们先设计一下版本信息的格式吧!大家可以看看。
http://203.195.148.180:8080/ts_update/ 1 1001 scene.zip //格式为:文件包目录(http://203.195.148.180:8080/ts_update/) 总版本数量(1) //版本号1(1001) 版本文件1(scene.zip) ... 版本号n(1001) 版本文件n(scene.zip)我们现在开始改造AssetsManager,首先定义下载任务的结构。
struct UpdateItem { int version; std::string zipPath; std::string zipUrl; UpdateItem(int v, std::string p, std::string u) : version(v), zipPath(p), zipUrl(u) {} }; std::deque<UpdateItem> _versionUrls;
然后改造bool checkUpdate(),这里把服务器的版本内容解析出来,放到一个队列_versionUrls里面。
bool UpdateEngine::checkUpdate() { if (_versionFileUrl.size() == 0) return false; _curl = curl_easy_init(); if (!_curl) { CCLOG("can not init curl"); return false; } _version.clear(); CURLcode res; curl_easy_setopt(_curl, CURLOPT_URL, _versionFileUrl.c_str()); curl_easy_setopt(_curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, getVersionCode); curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &_version); if (_connectionTimeout) curl_easy_setopt(_curl, CURLOPT_CONNECTTIMEOUT, _connectionTimeout); curl_easy_setopt(_curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_LIMIT, LOW_SPEED_LIMIT); curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_TIME, LOW_SPEED_TIME); res = curl_easy_perform(_curl); if (res != 0) { Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{ if (this->_delegate) this->_delegate->onError(ErrorCode::NETWORK); }); CCLOG("can not get version file content, error code is %d", res); return false; } int localVer = getVersion(); StringBuffer buff(_version); int version; short versionCnt; string versionUrl, pathUrl; buff >> pathUrl >> versionCnt; for (short i = 0; i < versionCnt; ++i) { buff >> version >> versionUrl; if (version > localVer) { _versionUrls.push_back(UpdateItem(version, pathUrl, versionUrl)); } } if (_versionUrls.size() <= 0) { Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{ if (this->_delegate) this->_delegate->onError(ErrorCode::NO_NEW_VERSION); }); CCLOG("there is not new version"); return false; } CCLOG("there is %d new version!", _versionUrls.size()); //设置下载目录,不存在则创建目录 _downloadPath = FileUtils::getInstance()->getWritablePath(); _downloadPath += "download_temp/"; createDirectory(_downloadPath.c_str()); return true; }其次,改造void downloadAndUncompress(),把版本队里里面的任务取出来,下载解压,然后写本地版本号,直到版本队列为空。
void UpdateEngine::downloadAndUncompress() { while(_versionUrls.size() > 0) { //取出当前第一个需要下载的url UpdateItem item = _versionUrls.front(); _packageUrl = item.zipPath + item.zipUrl; char downVersion[32]; sprintf(downVersion, "%d", item.version); _version = downVersion; //通知文件下载 std::string zipUrl = item.zipUrl; Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this, zipUrl]{ if (this->_delegate) this->_delegate->onDownload(zipUrl); }); //开始下载,下载失败退出 if (!downLoad()) { Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{ if (this->_delegate) this->_delegate->onError(ErrorCode::UNDOWNED); }); break; } //通知文件压缩 Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this, zipUrl]{ if (this->_delegate) this->_delegate->onUncompress(zipUrl); }); //解压下载的zip文件 string outFileName = _downloadPath + TEMP_PACKAGE_FILE_NAME; if (!uncompress(outFileName)) { Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{ if (this->_delegate) this->_delegate->onError(ErrorCode::UNCOMPRESS); }); break; } //解压成功,任务出队列,写本地版本号 _versionUrls.pop_front(); Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{ //写本地版本号 UserDefault::getInstance()->setStringForKey("localVersion", _version); UserDefault::getInstance()->flush(); //删除本次下载的文件 string zipfileName = this->_downloadPath + TEMP_PACKAGE_FILE_NAME; if (remove(zipfileName.c_str()) != 0) { CCLOG("can not remove downloaded zip file %s", zipfileName.c_str()); } //如果更新任务已经完成,通知更新成功 if(_versionUrls.size() <= 0 && this->_delegate) this->_delegate->onSuccess(); }); } curl_easy_cleanup(_curl); _isDownloading = false; }再次,对lua进行支持,原来的方案是写了一个脚本代理类,但是写lua的中间代码比较麻烦,我采用了比较简单的方式,通常自动更新是全局的,所以自动更新的信息,我通过调用lua全局函数方式来处理。
void UpdateEngineDelegate::onError(ErrorCode errorCode) { auto engine = LuaEngine::getInstance(); lua_State* pluaState = engine->getLuaStack()->getLuaState(); static LuaFunctor<Type_Null, int> selfonError(pluaState, "UpdateLayer.onError"); if (!selfonError(LUA_NOREF, nil, errorCode)) { log("UpdateLayer.onError failed! Because: %s", selfonError.getLastError()); } } void UpdateEngineDelegate::onProgress(int percent, int type /* = 1 */) { auto engine = LuaEngine::getInstance(); lua_State* pluaState = engine->getLuaStack()->getLuaState(); static LuaFunctor<Type_Null, int, int> selfonProgress(pluaState, "UpdateLayer.onProgress"); if (!selfonProgress(LUA_NOREF, nil, percent, type)) { log("UpdateLayer.onProgress failed! Because: %s", selfonProgress.getLastError()); } } void UpdateEngineDelegate::onSuccess() { auto engine = LuaEngine::getInstance(); lua_State* pluaState = engine->getLuaStack()->getLuaState(); static LuaFunctor<Type_Null> selfonSuccess(pluaState, "UpdateLayer.onSuccess"); if (!selfonSuccess(LUA_NOREF, nil)) { log("UpdateLayer.onSuccess failed! Because: %s", selfonSuccess.getLastError()); } } void UpdateEngineDelegate::onDownload(string packUrl) { auto engine = LuaEngine::getInstance(); lua_State* pluaState = engine->getLuaStack()->getLuaState(); static LuaFunctor<Type_Null, string> selfonDownload(pluaState, "UpdateLayer.onDownload"); if (!selfonDownload(LUA_NOREF, nil, packUrl)) { log("UpdateLayer.onDownload failed! Because: %s", selfonDownload.getLastError()); } } void UpdateEngineDelegate::onUncompress(string packUrl) { auto engine = LuaEngine::getInstance(); lua_State* pluaState = engine->getLuaStack()->getLuaState(); static LuaFunctor<Type_Null, string> selfonUncompress(pluaState, "UpdateLayer.onUncompress"); if (!selfonUncompress(LUA_NOREF, nil, packUrl)) { log("UpdateLayer.onUncompress failed! Because: %s", selfonUncompress.getLastError()); } }最后把UpdateEngine使用PKG方式暴露给lua使用,这个lua文件是app里面调用的第一个lua文件,里面没有任何游戏内容相关,游戏内容都从main.lua开始加载,达到更新完毕后在加载其他lua文件的目的。
class UpdateEngine : public Node { public: static UpdateEngine* create(const char* versionFileUrl, const char* storagePath); virtual void update(); };好了,主要代码和思路以及给出来了,现在我们看看如何使用吧!
--update.lua require "Cocos2d" local timer_local = nil --自动更新界面 UpdateLayer = {} local function showUpdate() if timer_local then cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local) timer_local = nil end local layer = cc.Layer:create() local sceneGame = cc.Scene:create() local winSize = cc.Director:getInstance():getWinSize() local bg_list = { "update/loading_bg_1.jpg", "update/loading_bg_2.jpg", "update/loading_bg_3.jpg", } local imageName = bg_list[math.random(3)] local bgSprite = cc.Sprite:create(imageName) bgSprite:setPosition(cc.p(winSize.width / 2, winSize.height / 2)) layer:addChild(bgSprite) --进度条背景 local loadingbg = cc.Sprite:create("update/loading_bd.png") loadingbg:setPosition(cc.p(winSize.width / 2, winSize.height / 2 - 40)) layer:addChild(loadingbg) --进度条 UpdateLayer._loadingBar = ccui.LoadingBar:create("update/loading.png", 0) UpdateLayer._loadingBar:setSize(cc.size(880, 20)) UpdateLayer._loadingBar:setPosition(cc.p(winSize.width / 2, winSize.height / 2 - 40)) layer:addChild(UpdateLayer._loadingBar) --提示信息 UpdateLayer._labelNotice = cc.LabelTTF:create("", "res/fonts/DFYuanW7-GB2312.ttf", 25) UpdateLayer._labelNotice:setPosition(cc.p(winSize.width / 2, winSize.height / 2)) layer:addChild(UpdateLayer._labelNotice) --动画切换场景 sceneGame:addChild(layer) local transScene = cc.TransitionFade:create(1.5, sceneGame, cc.c3b(0,0,0)) cc.Director:getInstance():replaceScene(transScene) --初始化更新引擎 local path = cc.FileUtils:getInstance():getWritablePath() .. "temp/" UpdateLayer._updateEngine = UpdateEngine:create("http://203.195.148.180:8080/ts_update/version", path) UpdateLayer._updateEngine:retain() --启动定时器等待界面动画完成后开始更新 local function startUpdate() UpdateLayer._loadingBar:setPercent(1) UpdateLayer._updateEngine:update() cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local) timer_local = nil end UpdateLayer._loadingBar:setPercent(0) UpdateLayer._labelNotice:setString(strg2u("正在检查新版本,请稍等")) timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(startUpdate, 1.5, false) end --显示提示界面 local function showNotice() if timer_local then cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local) timer_local = nil end local layer = cc.Layer:create() local sceneGame = cc.Scene:create() local winSize = cc.Director:getInstance():getWinSize() local notice = cc.Sprite:create("update/notice.png") notice:setPosition(cc.p(winSize.width/2, winSize.height/2)); layer:addChild(notice) sceneGame:addChild(layer) local transScene = cc.TransitionFade:create(1.5, sceneGame, cc.c3b(0,0,0)) cc.Director:getInstance():replaceScene(transScene) timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(showUpdate, 2.6, false) end --显示logo界面 local function showLogo() local sceneGame = cc.Scene:create() local winSize = cc.Director:getInstance():getWinSize() local layer = cc.LayerColor:create(cc.c4b(128, 128, 128, 255), winSize.width, winSize.height) local logo1 = cc.Sprite:create("update/logo1.png") local logo2 = cc.Sprite:create("update/logo2.png") local logo3 = cc.Sprite:create("update/logo3.png") logo3:setPosition(cc.p(winSize.width / 2, winSize.height / 2)) logo2:setPosition(cc.p(winSize.width - logo2:getContentSize().width / 2, logo2:getContentSize().height / 2)) logo1:setPosition(cc.p(winSize.width - logo1:getContentSize().width / 2, logo2:getContentSize().height + logo1:getContentSize().height / 2)) layer:addChild(logo1) layer:addChild(logo2) layer:addChild(logo3) sceneGame:addChild(layer) cc.Director:getInstance():runWithScene(sceneGame) timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(showNotice, 1, false) end --更新主函数 function update() collectgarbage("collect") -- avoid memory leak collectgarbage("setpause", 100) collectgarbage("setstepmul", 5000) math.randomseed(os.time()) math.random(os.time()) math.random(os.time()) math.random(os.time()) --显示logoo界面 showLogo() end --c++更新信息回调 local ErrorCode = { NETWORK = 0, CREATE_FILE = 1, NO_NEW_VERSION = 2, UNDOWNED = 3, UNCOMPRESS = 4, } local function finishUpdate() UpdateLayer.percent = 0 local function addPercent() if UpdateLayer.percent < 200 then UpdateLayer.percent = UpdateLayer.percent + 2 if UpdateLayer.percent < 100 then UpdateLayer._loadingBar:setPercent(UpdateLayer.percent) elseif UpdateLayer.percent <= 100 then UpdateLayer._loadingBar:setPercent(UpdateLayer.percent) UpdateLayer._labelNotice:setString(strg2u("当前版本已经最新,无需更新")) elseif UpdateLayer.percent >= 200 then cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local) timer_local = nil --进入游戏界面 UpdateLayer = nil require "src.main" end end end timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(addPercent, 0.05, false) end function UpdateLayer.onError(errorCode) if errorCode == ErrorCode.NO_NEW_VERSION then finishUpdate() elseif errorCode == ErrorCode.NETWORK then UpdateLayer._labelNotice:setString(strg2u("获取服务器版本失败,请检查您的网络")) elseif errorCode == ErrorCode.UNDOWNED then UpdateLayer._labelNotice:setString(strg2u("下载文件失败,请检查您的网络")) elseif errorCode == ErrorCode.UNCOMPRESS then UpdateLayer._labelNotice:setString(strg2u("解压文件失败,请关闭程序重新更新")) end end function UpdateLayer.onProgress(percent) local progress = string.format("正在下载文件:%s(%d%%)", UpdateLayer._downfile, percent) print(strg2u(progress)) UpdateLayer._labelNotice:setString(strg2u(progress)) UpdateLayer._loadingBar:setPercent(percent) end function UpdateLayer.onSuccess() UpdateLayer._labelNotice:setString(strg2u("自动更新完毕")) local function updateSuccess() cc.Director:getInstance():getScheduler():unscheduleScriptEntry(timer_local) timer_local = nil --进入游戏界面 UpdateLayer = nil require "src.main" end timer_local = cc.Director:getInstance():getScheduler():scheduleScriptFunc(updateSuccess, 2, false) end function UpdateLayer.onDownload(str) UpdateLayer._downfile = str local downfile = string.format("正在下载文件:%s(0%%)", str) print(strg2u(downfile)) UpdateLayer._labelNotice:setString(strg2u(downfile)) end function UpdateLayer.onUncompress(str) local uncompress = string.format("正在解压文件:%s", str) print(strg2u(uncompress)) UpdateLayer._labelNotice:setString(strg2u(uncompress)) end -- for CCLuaEngine traceback function __G__TRACKBACK__(msg) print("----------------------------------------") print("LUA ERROR: " .. tostring(msg) .. "\n") print(debug.traceback()) print("----------------------------------------") end xpcall(update, __G__TRACKBACK__)最后说明一点,需要把下载解压的目录加到文件搜索的最前面,保证cocos2dx优先加载解压的lua文件和资源。
最最最后,我把我改造的自动更新系统代码分享给大家吧,有什么问题大家可以咨询我!
cocos2dx自动更新源码