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

来源:互联网 发布:unity3d连接sql数据库 编辑:程序博客网 时间:2024/06/06 13:00

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


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


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


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

1
2
3
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,首先定义下载任务的结构。

1
2
3
4
5
6
7
8
9
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里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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(),把版本队里里面的任务取出来,下载解压,然后写本地版本号,直到版本队列为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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全局函数方式来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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, intint> 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文件的目的。

1
2
3
4
5
6
7
class UpdateEngine : public Node  
{  
public:  
    static UpdateEngine* create(const char* versionFileUrl, const char* storagePath);  
      
    virtual void update();  
};


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
--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

0 0
原创粉丝点击