cocos creator 2.3.1 熱更新

一、掛載熱更新腳本

場景Canvas裏面增加腳本HotUpdate.js,對應檢查更新和更新兩個按鈕的功能。

cc.Class({
    extends: cc.Component,

    properties: {
        manifestUrl: {
            type: cc.Asset,
            default: null
        },
        updateUI:{
            default:null,
            type:cc.Node,
        },
        _updating: false,
        _canRetry: false,
        _storagePath: '',

        progressBar:{
            default:null,
            type:cc.ProgressBar,
        },
    },

    checkCb: function (event) {
        cc.log('Code: ' + event.getEventCode());
        switch (event.getEventCode())
        {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                cc.log("No local manifest file found, hot update skipped.") ;
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                cc.log("Fail to download manifest file, hot update skipped.")
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log("Already up to date with the latest remote version.")
                break;
            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                cc.log('New version found, please try to update. (' + this._am.getTotalBytes() + ')')
                break;
            default:
                return;
        }
        
        this._am.setEventCallback(null);
        this._checkListener = null;
        this._updating = false;
    },

    updateCb: function (event) {
        var needRestart = false;
        var failed = false;
        switch (event.getEventCode())
        {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                cc.log('No local manifest file found, hot update skipped.')
                failed = true;
                break;
            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                this.show();
                // cc.log("大小百分比:",event.getPercent())
                // cc.log("文件百分比:",event.getPercentByFile())

                // cc.log("文件進度:",event.getDownloadedFiles() + ' / ' + event.getTotalFiles())
                // cc.log("大小進度:",event.getDownloadedBytes() + ' / ' + event.getTotalBytes())

                var title = this.updateUI.getChildByName("title").getComponent(cc.Label)
                title.string = parseInt(event.getDownloadedBytes()) + ' / ' + parseInt(event.getTotalBytes());
                var progressText = this.updateUI.getChildByName("progressText").getComponent(cc.Label);

                progressText.string = event.getPercent().toFixed(2)*100+"%";

                // var progressBar = this.updateUI.getChildByName("progress")

                this.progressBar.progress = event.getPercent();
                cc.log("showPercent = ",(event.getPercent()).toFixed(1),event.getPercent())

                var msg = event.getMessage();
                if (msg) {
                    cc.log(event.getPercent()/100 + '% : ' + msg);
                }
                break;
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                cc.log('Fail to download manifest file, hot update skipped.')
                failed = true;
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log('Already up to date with the latest remote version.')
                failed = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FINISHED:
                cc.log('Update finished. ' + event.getMessage())
                needRestart = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FAILED:
                cc.log('Update failed. ' + event.getMessage())
                this._updating = false;
                this._canRetry = true;
                break;
            case jsb.EventAssetsManager.ERROR_UPDATING:
                cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage())
                break;
            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                cc.log(event.getMessage())
                break;
            default:
                break;
        }

        if (failed) {
            this._am.setEventCallback(null);
            this._updateListener = null;
            this._updating = false;
        }

        if (needRestart) {
            this._am.setEventCallback(null);
            this._updateListener = null;
            // Prepend the manifest's search path
            var searchPaths = jsb.fileUtils.getSearchPaths();
            var newPaths = this._am.getLocalManifest().getSearchPaths();
            console.log("newPaths:",JSON.stringify(newPaths));
            Array.prototype.unshift.apply(searchPaths, newPaths);
            // This value will be retrieved and appended to the default search path during game startup,
            // please refer to samples/js-tests/main.js for detailed usage.
            // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
            cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
            jsb.fileUtils.setSearchPaths(searchPaths);

            cc.audioEngine.stopAll();
            cc.game.restart();
            cc.log("重啓遊戲")
        }
    },
    
    retry: function () {
        if (!this._updating && this._canRetry) {
            this._canRetry = false;
            cc.log('Retry failed Assets...');
            this._am.downloadFailedAssets();
        }
    },
    
    checkUpdate: function () {
        if (!cc.sys.isNative){
            return;
        }
        if (this._updating) {
            cc.log('Checking or updating ...')
            return;
        }
        if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
            // Resolve md5 url
            var url = this.manifestUrl.nativeUrl;
            if (cc.loader.md5Pipe) {
                url = cc.loader.md5Pipe.transformURL(url);
            }
            this._am.loadLocalManifest(url);
        }
        if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
            cc.log('Failed to load local manifest ...')
            return;
        }
        this._am.setEventCallback(this.checkCb.bind(this));

        this._am.checkUpdate();
        this._updating = true;
    },

    hotUpdate: function () {
        if (!cc.sys.isNative){
            return;
        }
        if (this._am && !this._updating) {
            this._am.setEventCallback(this.updateCb.bind(this));

            if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
                // Resolve md5 url
                var url = this.manifestUrl.nativeUrl;
                if (cc.loader.md5Pipe) {
                    url = cc.loader.md5Pipe.transformURL(url);
                }
                this._am.loadLocalManifest(url);
            }

            this._failCount = 0;
            this._am.update();
            this._updating = true;
        }
    },
    
    show: function () {
        if (this.updateUI.active === false) {
            this.updateUI.active = true;
        }
    },

    // use this for initialization
    onLoad: function () {
        // Hot update is only available in Native build
        if (!cc.sys.isNative) {
            return;
        }
        this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'myCatchFish-assets');
        cc.log('Storage path for remote asset : ' + this._storagePath);

        // Setup your own version compare handler, versionA and B is versions in string
        // if the return value greater than 0, versionA is greater than B,
        // if the return value equals 0, versionA equals to B,
        // if the return value smaller than 0, versionA is smaller than B.
        this.versionCompareHandle = function (versionA, versionB) {
            cc.log("JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB);
            var vA = versionA.split('.');
            var vB = versionB.split('.');
            for (var i = 0; i < vA.length; ++i) {
                var a = parseInt(vA[i]);
                var b = parseInt(vB[i] || 0);
                if (a === b) {
                    continue;
                }
                else {
                    return a - b;
                }
            }
            if (vB.length > vA.length) {
                return -1;
            }
            else {
                return 0;
            }
        };

        // Init with empty manifest url for testing custom manifest
        this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);

        var panel = this.panel;
        // Setup the verification callback, but we don't have md5 check function yet, so only print some message
        // Return true if the verification passed, otherwise return false
        this._am.setVerifyCallback(function (path, asset) {
            // When asset is compressed, we don't need to check its md5, because zip file have been deleted.
            var compressed = asset.compressed;
            // Retrieve the correct md5 value.
            var expectedMD5 = asset.md5;
            // asset.path is relative path and path is absolute.
            var relativePath = asset.path;
            // The size of asset file, but this value could be absent.
            var size = asset.size;
            if (compressed) {
                cc.log("Verification passed : " + relativePath)
                return true;
            }
            else {
                cc,log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')')
                return true;
            }
        });

        cc.log('Hot update is ready, please check or directly update.')

        if (cc.sys.os === cc.sys.OS_ANDROID) {
            // Some Android device may slow down the download process when concurrent tasks is too much.
            // The value may not be accurate, please do more test and find what's most suitable for your game.
            this._am.setMaxConcurrentTask(2);
            cc.log("Max concurrent tasks count have been limited to 2")
        }
        
    },

    onDestroy: function () {
        if (this._updateListener) {
            this._am.setEventCallback(null);
            this._updateListener = null;
        }
    }
});

對應界面顯示

二、準備舊版本apk

1.從官網熱更新範例裏面下載version_generator.js腳本,需要修改的地方爲,主要設置version參數爲舊版本號

修改路徑

上面的src和res對應的是jsb-link發佈路徑下面的目錄

打開終端,cd到version_generator.js目錄下面,運行命令(在這之前需要安裝node環境)

node version_generator.js

也可以不修改version_generator.js文件,直接命令行傳遞參數

node version_generator.js -v 1.0.0 -u http://your-server-address/hot-update/remote-assets/ -s native/package/ -d assets/

運行成功終端顯示

然後我們在assets目錄下面會多出兩個文件

現在,可以構建發佈Android工程了,cocos creator裏面【項目】-> 【構建發佈】,選擇發佈平臺爲Android,記得不要勾選MD5Cache,否則熱更新失效,【構建】

注意,構建完成後,jsb-link目錄下面的main.js會刷新,這時候要在main.js第一行增加搜索路徑設置的邏輯和更新中斷修復代碼,否則,熱更新成功後,退出遊戲重新進入遊戲會恢復舊版本的代碼

// 在 main.js 的開頭添加如下代碼
(function () {
    if (typeof window.jsb === 'object') {
        var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
        if (hotUpdateSearchPaths) {
            var paths = JSON.parse(hotUpdateSearchPaths);
            jsb.fileUtils.setSearchPaths(paths);

            var fileList = [];
            var storagePath = paths[0] || '';
            var tempPath = storagePath + '_temp/';
            var baseOffset = tempPath.length;

            if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
                jsb.fileUtils.listFilesRecursively(tempPath, fileList);
                fileList.forEach(srcPath => {
                    var relativePath = srcPath.substr(baseOffset);
                    var dstPath = storagePath + relativePath;

                    if (srcPath[srcPath.length] == '/') {
                        cc.fileUtils.createDirectory(dstPath)
                    }
                    else {
                        if (cc.fileUtils.isFileExist(dstPath)) {
                            cc.fileUtils.removeFile(dstPath)
                        }
                        cc.fileUtils.renameFile(srcPath, dstPath);
                    }
                })
                cc.fileUtils.removeDirectory(tempPath);
            }
        }
    }
})();

編譯->運行(需要先打開模擬器纔會自動安裝運行apk),至此,舊版本apk完成。

三、生成新版本熱更新文件,部署到服務器上

1、修改version_generator.js裏面的版本號(1.0.1)

2、修改工程內容,(可以往resource文件夾裏面增加一些圖片文件,增加場景,然後在新場景中防入新增的圖片)

3、運行node version_generator.js命令,得到新的version.manifest和project.manifest兩個文件,cocos creator【構建】->【編譯】,得到新版本的jsb-link目錄

注意:這裏構建完也要像舊版本工程那樣在main.js第一行增加搜索路徑設置的邏輯和更新中斷修復代碼

4、添加熱更新包

      新建熱更新目錄remote-assets,將jsb-link目錄下的res和src,還有上面生成的兩個manifest文件複製到remote-assets下

5.搭建下載服務器(可以用python搭建簡易服務器和nodejs+express或者其他服務器),這裏用nodejs+express搭建一個簡單服務器,將熱更新目錄放到服務器上的hot-update/remote-assets目錄下面

cd到根目錄,運行nmp start命令,服務器啓動,我們驗證一下服務器下載是否成功,請求一下version_generator.js裏面配置的遠程資源路徑

http://192.168.0.103:3000/hot-update/remote-assets/project.manifest

請求結果:

說明服務器資源可以正常下載

四、打開舊版本測試更新

點擊檢查更新按鈕,日誌顯示發現新版本,點擊更新按鈕,彈出更新窗口更新,更新完畢重啓遊戲進入新版本界面

 

五、構建完自動更新main.js文件

1.在工程packages目錄下面增加 hot-update 編輯器插件

main.js

'use strict';

var Fs = require("fire-fs");
var Path = require("fire-path");

var inject_script = `
(function () {
    if (typeof window.jsb === 'object') {
        var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
        if (hotUpdateSearchPaths) {
            var paths = JSON.parse(hotUpdateSearchPaths);
            jsb.fileUtils.setSearchPaths(paths);

            var fileList = [];
            var storagePath = paths[0] || '';
            var tempPath = storagePath + '_temp/';
            var baseOffset = tempPath.length;

            if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
                jsb.fileUtils.listFilesRecursively(tempPath, fileList);
                fileList.forEach(srcPath => {
                    var relativePath = srcPath.substr(baseOffset);
                    var dstPath = storagePath + relativePath;

                    if (srcPath[srcPath.length] == '/') {
                        cc.fileUtils.createDirectory(dstPath)
                    }
                    else {
                        if (cc.fileUtils.isFileExist(dstPath)) {
                            cc.fileUtils.removeFile(dstPath)
                        }
                        cc.fileUtils.renameFile(srcPath, dstPath);
                    }
                })
                cc.fileUtils.removeDirectory(tempPath);
            }
        }
    }
})();
`;

module.exports = {
    load: function () {
        // 當 package 被正確加載的時候執行
    },

    unload: function () {
        // 當 package 被正確卸載的時候執行
    },

    messages: {
        'editor:build-finished': function (event, target) {
            var root = Path.normalize(target.dest);
            var url = Path.join(root, "main.js");
            Fs.readFile(url, "utf8", function (err, data) {
                if (err) {
                    throw err;
                }

                var newStr = inject_script + data;
                Fs.writeFile(url, newStr, function (error) {
                    if (err) {
                        throw err;
                    }
                    Editor.log("SearchPath updated in built main.js for hot update");
                });
            });
        }
    }
};

 

package.json

{
  "name": "hot-update",
  "version": "0.0.1",
  "description": "用於熱更新插件",
  "author": "Cocos Creator",
  "main": "main.js"
}

這樣就不用每次構建都要手動插入一段增加搜索路徑設置的邏輯和更新中斷修復代碼了。

 

<完>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章