关于Cocos2d-x客户端程序的自动更新解决方案
来源:互联网 发布:unity3d连接sql数据库 编辑:程序博客网 时间:2024/06/06 13:00
随着手机游戏的不断发展,游戏包也越来越大,手机网络游戏已经超过100M了,对于玩家来说,如果每次更新都要重新下载,那简直是灾难。而且如果上iOS平台,每次重新发包都要审核,劳神费力。所以当前的主流手游都开始提供自动更新的功能,在不改动C++代码的前提下,使用Lua或者js进行业务逻辑开发,然后自动更新脚本和资源,方便玩家也方便研发者。
做端游时,自动更新是一个大工程,不仅要能更新资源和脚本,还要更新dll文件等,后期甚至要支持P2P,手游目前基本上都使用http方式。Cocos2d-x也提供了一个基础功能类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__)
最后说明一点,需要把下载解压的目录加到文件搜索的最前面,保证Cocos2d-x优先加载解压的Lua文件和资源。
最最最后,我把我改造的自动更新系统代码分享给大家吧,有什么问题大家可以咨询我!
Cocos2d-x 自动更新源码.rar
注意:移植过去的时候出现了2个坑,一个是缓存没有释放
需要加fflush();
bool UpdateEngine::downLoad(std::string outFileName)
{
//const string outFileNameTmp = _storagePath + TEMP_PACKAGE_FILE_NAME;
FILE* fp = fopen(outFileName.c_str(), "wb");
if (!fp)
{
Director::getInstance()->getScheduler()->performFunctionInCocosThread([&, this]{
if (this->_delegate)
this->_delegate->onError(ErrorCode::CREATE_FILE);
});
CCLOG("can not create temp zip file %s", outFileName.c_str());
return false;
}
//ø™ º¥”∑˛ŒÒ∆˜œ¬‘ÿ
_curl = curl_easy_init();
CURLcode res;
curl_easy_setopt(_curl, CURLOPT_URL, _packageUrl.c_str());
curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, downLoadPackage);
curl_easy_setopt(_curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(_curl, CURLOPT_NOPROGRESS, false);
curl_easy_setopt(_curl, CURLOPT_PROGRESSFUNCTION, updateProgressFunc);
curl_easy_setopt(_curl, CURLOPT_PROGRESSDATA, this);
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("error when download package %s", _packageUrl.c_str());
fclose(fp);
return false;
}
curl_easy_cleanup(_curl);
CCLOG("succeed downloading package %s", _packageUrl.c_str());
fflush(fp);
fclose(fp);
return true;
}
另外一个坑是
void UpdateEngine::update()
{
if (_isDownloading) return;
_isDownloading = true;
if (_versionFileUrl.size() == 0)
{
CCLOG("no version file url");
_isDownloading = false;
return;
}
//检查是否有新版本下载,如果有就更新任务
if (!checkUpdate())
{
_isDownloading = false;
curl_easy_cleanup(_curl);
return;
}
//开始下载新任务
auto t = std::thread(&UpdateEngine::downloadAndUncompress, this);
//t.detach();
t.join();
//downloadAndUncompress();
}
t.detach();改为 t.join();
来源网址:http://blog.csdn.net/ljxfblog/article/details/37649739
- 关于Cocos2d-x客户端程序的自动更新解决方案
- 关于Cocos2d-x客户端程序的自动更新解决方案
- 关于cocos2dx客户端程序的自动更新解决方案
- 关于cocos2dx客户端程序的自动更新解决方案
- 关于cocos2dx客户端程序的自动更新解决方案
- 关于cocos2dx客户端程序的自动更新解决方案
- Cocos2d-x 3.2编译生成Android程序出错的解决方案
- Cocos2d-x 3.2编译生成Android程序出错的解决方案
- VS2012关于cocos2d-x的解决方案加载失败
- 客户端程序自动更新(升级)的方式
- 关于cocos2d-x 手游lua文件加密的解决方案,cocos2d-x lua
- C# 实现客户端程序自动更新
- 基于cocos2d-x的游戏客户端优化
- 基于cocos2d-x的游戏客户端优化
- libpomelo的cocos2d-x客户端使用总结
- 基于cocos2d-x的游戏客户端优化
- 关于cocos2d-x 3.X的搭建和编译成Android平台程序的说明
- Cocos2d-x 3.2编译生成Android程序出错的解决方案:c++_static报错
- 统计字符串中字母个数(用treeMap)
- C语言总结之数据类型,sizeof,void*总结
- 一个c语言读写文件程序
- 广州恒大被淘汰了!
- Debian 使用帮助
- 关于Cocos2d-x客户端程序的自动更新解决方案
- 数组巧用——HDU 4970
- STL源码分析--deque
- 斐波那契算法分析
- 快速排序&希尔排序(shell)
- Android 拖拽自定义控件的原理与实现
- DOM创建并添加节点
- C语言变长参数
- static