前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于cocos2dx客户端程序的自动更新解决方案

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

作者头像
帘卷西风
发布2018-08-03 15:42:41
1K0
发布2018-08-03 15:42:41
举报

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

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

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

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

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

代码语言:javascript
复制
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,首先定义下载任务的结构。

代码语言:javascript
复制
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里面。

代码语言:javascript
复制
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(),把版本队里里面的任务取出来,下载解压,然后写本地版本号,直到版本队列为空。

代码语言:javascript
复制
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全局函数方式来处理。

代码语言:javascript
复制
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文件的目的。

代码语言:javascript
复制
class UpdateEngine : public Node
{
public:
	static UpdateEngine* create(const char* versionFileUrl, const char* storagePath);
   
	virtual void update();
};

       好了,主要代码和思路以及给出来了,现在我们看看如何使用吧!

代码语言:javascript
复制
--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自动更新源码

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2014年07月10日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档