CocosCreator热更新(v1.10.2)

作者: Dane_404 | 来源:发表于2019-04-23 12:32 被阅读43次

    creator生成的项目是纯粹的数据驱动而非代码驱动,一切皆为数据,包括脚本、图片、音频、动画等,而数据驱动需要一个入口,就是main.js,而setting.js描述了整个资源的配置文件,我们自己编写的脚本,在编译后统一存放在了project.js中。热更新根据这个原理,在服务器存放游戏资源,通过服务端与本地的manifest进行对比,把差异文件下载到某个文件夹里面,在入口文件main.js设置搜索路径为更新的文件夹,这样达到热更新的目的。

    新建Hall工程

    VersionTip用来提示更新,UpdateTip用来显示更新进度,FinishTip用来提示完成更新并重启。


    image.png

    热更新脚本:

    onst hotResDir = "AllGame/Hall"; //更新的资源目录
    
    export class HotUpdateUtil {
    
        private static am;
        private static isUpdating = false;
        private static checkUpdateListener;
        private static updateListener;
        private static manifestUrl;
    
        private static checkNewVersionListener: Function;
        private static updateProgressListener: Function;
        private static updateErrorListener: Function;
        private static updateFinishListener: Function;
    
        public static init(manifestUrl: cc.Asset) {
    
            if (!cc.sys.isNative) return;
    
            this.manifestUrl = manifestUrl;
            let storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + hotResDir);
            this.am = new jsb.AssetsManager("", storagePath, this.versionCompareHandle);
    
            if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
                this.am.retain();
            }
    
            this.am.setVerifyCallback(function (path, asset) {
    
                var compressed = asset.compressed;
                var expectedMD5 = asset.md5;
                var relativePath = asset.path;
                var size = asset.size;
                if (compressed) {
                    cc.log("Verification passed : " + relativePath);
                    return true;
                }
                else {
                    cc.log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
                    return true;
                }
            });
    
            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");
            }
        }
    
        //版本对比
        private static versionCompareHandle(versionA, versionB): number {
    
            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;
            }
    
        }
    
        public static checkUpdate(checkNewVersionCallback?:Function) {
    
            if (this.isUpdating) {
                cc.log("正在更新中...");
                return;
            }
    
            this.checkNewVersionListener = checkNewVersionCallback;
    
            if (this.am.getState() === jsb.AssetsManager.State.UNINITED) {
                var url = this.manifestUrl;
                cc.log(url);
                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.checkUpdateListener = new jsb.EventListenerAssetsManager(this.am, this.checkCallback.bind(this));
            cc.eventManager.addListener(this.checkUpdateListener, 1);
    
            this.am.checkUpdate();
            this.isUpdating = true;
        }
    
        private static checkCallback(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.');
                    if (this.checkNewVersionListener) {
                        this.checkNewVersionListener();
                    }
                    break;
                default:
                    return;
            }
            cc.eventManager.removeListener(this.checkUpdateListener);
            this.checkUpdateListener = null;
            this.isUpdating = false;
        }
    
        public static update(updateProgressListener?:Function,updateErrorListener?:Function,updateFinishListener?:Function) {
    
            if (this.am && !this.isUpdating) {
    
                this.updateProgressListener = updateProgressListener;
                this.updateErrorListener = updateErrorListener;
                this.updateFinishListener = updateFinishListener;
    
                this.updateListener = new jsb.EventListenerAssetsManager(this.am, this.updateCallback.bind(this));
                cc.eventManager.addListener(this.updateListener, 1);
    
                if (this.am.getState() === jsb.AssetsManager.State.UNINITED) {
                    // Resolve md5 url
                    var url = this.manifestUrl;
                    if (cc.loader.md5Pipe) {
                        url = cc.loader.md5Pipe.transformURL(url);
                    }
                    this.am.loadLocalManifest(url);
                }
    
                this.am.update();
                this.isUpdating = true;
            }
        }
    
    
        private static updateCallback(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:
                  
                    if(this.updateProgressListener){
                        this.updateProgressListener(event.getDownloadedBytes(),event.getTotalBytes());
                    }
                    break;
                case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                    cc.log('Fail to download manifest file, hot update skipped.');
                    if(this.updateErrorListener){
                        this.updateFinishListener('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;
                    if(this.updateFinishListener){
                        this.updateFinishListener();
                    }
                    break;
                case jsb.EventAssetsManager.UPDATE_FAILED:
                    cc.log('Update failed. ' + event.getMessage());
                    if(this.updateErrorListener){
                        this.updateErrorListener('Update failed. ' + event.getMessage());
                    }
                    this.isUpdating = false;
                    break;
                case jsb.EventAssetsManager.ERROR_UPDATING:
                    cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
                    if(this.updateErrorListener){
                        this.updateErrorListener('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
                    }
                    break;
                case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                    cc.log(event.getMessage());
                    break;
                default:
                    break;
            }
    
            if (failed) {
                cc.eventManager.removeListener(this.updateListener);
                this.updateListener = null;
                this.isUpdating = false;
            }
    
            if (needRestart) {
                cc.eventManager.removeListener(this.updateListener);
                this.updateListener = null;
                // Prepend the manifest's search path
                var searchPaths = jsb.fileUtils.getSearchPaths();
                var newPaths = this.am.getLocalManifest().getSearchPaths();
                cc.log(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);
         
            }
        }
    }
    

    使用脚本

    import { HotUpdateUtil } from "./HotUpdateUtil";
    
    const {ccclass, property} = cc._decorator;
    
    @ccclass
    export default class HotUpdate extends cc.Component {
    
       @property(cc.Asset)
       manifest:cc.Asset = null;
       @property(cc.Node)
       newVersionTip:cc.Node = null;
       @property(cc.Node)
       updateTip:cc.Node = null;
       @property(cc.Node)
       finishUpdateTip:cc.Node = null;
    
        onLoad(){
            HotUpdateUtil.init(this.manifest);
        }  
    
        start(){
            HotUpdateUtil.checkUpdate(()=>{
                this.newVersionTip.active = true;
            });
        }
    
        private updateVersion(){
            this.updateTip.active = true;
            HotUpdateUtil.update((progress)=>{
                let temp = ~~(progress * 100);
                this.updateTip.getComponentInChildren(cc.Label).string = "下载中" + temp + "%";
            },null,()=>{
                this.finishUpdateTip.active = true;
            });
        }
    
        private restart(){
            cc.game.restart();
        }
    
    }
    

    生成manifest文件

    首先,一开始HotUpdate的manifest是空的:

    image.png
    然后,开始构建项目,不要勾选MD5 Cache,以Android为例,模板为default,构建完成后,在项目的根目录下放文件version_generator.js,里面的路径根据需要修改:
    /**
     * 此模块用于热更新工程清单文件的生成
     */
    
    var fs = require('fs');
    var path = require('path');
    var crypto = require('crypto');
    
    var manifest = {
        //服务器上资源文件存放路径(src,res的路径)
        packageUrl: 'http://192.168.0.136:8000',
        //服务器上project.manifest路径
        remoteManifestUrl: 'http://192.168.0.136:8000/project.manifest',
        //服务器上version.manifest路径
        remoteVersionUrl: 'http://192.168.0.136:8000/version.manifest',
        version: '1.0.0',
        assets: {},
        searchPaths: []
    };
    
    //生成的manifest文件存放目录
    var dest = 'assets/';
    //项目构建后资源的目录
    var src = 'build/jsb-default/';
    
    /**
     * node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
     */
    // Parse arguments
    var i = 2;
    while ( i < process.argv.length) {
        var arg = process.argv[i];
    
        switch (arg) {
        case '--url' :
        case '-u' :
            var url = process.argv[i+1];
            manifest.packageUrl = url;
            manifest.remoteManifestUrl = url + 'project.manifest';
            manifest.remoteVersionUrl = url + 'version.manifest';
            i += 2;
            break;
        case '--version' :
        case '-v' :
            manifest.version = process.argv[i+1];
            i += 2;
            break;
        case '--src' :
        case '-s' :
            src = process.argv[i+1];
            i += 2;
            break;
        case '--dest' :
        case '-d' :
            dest = process.argv[i+1];
            i += 2;
            break;
        default :
            i++;
            break;
        }
    }
    
    
    function readDir (dir, obj) {
        var stat = fs.statSync(dir);
        if (!stat.isDirectory()) {
            return;
        }
        var subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative;
        for (var i = 0; i < subpaths.length; ++i) {
            if (subpaths[i][0] === '.') {
                continue;
            }
            subpath = path.join(dir, subpaths[i]);
            stat = fs.statSync(subpath);
            if (stat.isDirectory()) {
                readDir(subpath, obj);
            }
            else if (stat.isFile()) {
                // Size in Bytes
                size = stat['size'];
                md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'binary')).digest('hex');
                compressed = path.extname(subpath).toLowerCase() === '.zip';
    
                relative = path.relative(src, subpath);
                relative = relative.replace(/\\/g, '/');
                relative = encodeURI(relative);
                obj[relative] = {
                    'size' : size,
                    'md5' : md5
                };
                if (compressed) {
                    obj[relative].compressed = true;
                }
            }
        }
    }
    
    var mkdirSync = function (path) {
        try {
            fs.mkdirSync(path);
        } catch(e) {
            if ( e.code != 'EEXIST' ) throw e;
        }
    }
    
    // Iterate res and src folder
    readDir(path.join(src, 'src'), manifest.assets);
    readDir(path.join(src, 'res'), manifest.assets);
    
    var destManifest = path.join(dest, 'project.manifest');
    var destVersion = path.join(dest, 'version.manifest');
    
    mkdirSync(dest);
    
    fs.writeFile(destManifest, JSON.stringify(manifest), (err) => {
      if (err) throw err;
      console.log('Manifest successfully generated');
    });
    
    delete manifest.assets;
    delete manifest.searchPaths;
    fs.writeFile(destVersion, JSON.stringify(manifest), (err) => {
      if (err) throw err;
      console.log('Version successfully generated');
    });
    

    接着cd到工程目录下,执行 node version_generator.js,在assets会自动生成了两个文件,project. manifest和version. manifest,把project. manifest拖给HotUpdate:

    image.png
    然后构建项目,构建成功后,修改mian.js:
    (function () {
         //添加这段
         if ( cc.sys.isNative) {
            var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');  //这个key对于HotUpdateUtil
            if (hotUpdateSearchPaths) {
                jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
            }
        }
        'use strict';
        //-------------
        function boot () {
    
            var settings = window._CCSettings;
            window._CCSettings = undefined;
    
            if ( !settings.debug ) {
                var uuids = settings.uuids;
    
                var rawAssets = settings.rawAssets;
                var assetTypes = settings.assetTypes;
                var realRawAssets = settings.rawAssets = {};
                for (var mount in rawAssets) {
                    var entries = rawAssets[mount];
                    var realEntries = realRawAssets[mount] = {};
                    for (var id in entries) {
                        var entry = entries[id];
                        var type = entry[1];
                        // retrieve minified raw asset
                        if (typeof type === 'number') {
                            entry[1] = assetTypes[type];
                        }
                        // retrieve uuid
                        realEntries[uuids[id] || id] = entry;
                    }
                }
    
                var scenes = settings.scenes;
                for (var i = 0; i < scenes.length; ++i) {
                    var scene = scenes[i];
                    if (typeof scene.uuid === 'number') {
                        scene.uuid = uuids[scene.uuid];
                    }
                }
    
                var packedAssets = settings.packedAssets;
                for (var packId in packedAssets) {
                    var packedIds = packedAssets[packId];
                    for (var j = 0; j < packedIds.length; ++j) {
                        if (typeof packedIds[j] === 'number') {
                            packedIds[j] = uuids[packedIds[j]];
                        }
                    }
                }
            }
    
            // init engine
            var canvas;
    
            if (cc.sys.isBrowser) {
                canvas = document.getElementById('GameCanvas');
            }
    
            if (false) {
                var ORIENTATIONS = {
                    'portrait': 1,
                    'landscape left': 2,
                    'landscape right': 3
                };
                BK.Director.screenMode = ORIENTATIONS[settings.orientation];
                initAdapter();
            }
    
            function setLoadingDisplay () {
                // Loading splash scene
                var splash = document.getElementById('splash');
                var progressBar = splash.querySelector('.progress-bar span');
                cc.loader.onProgress = function (completedCount, totalCount, item) {
                    var percent = 100 * completedCount / totalCount;
                    if (progressBar) {
                        progressBar.style.width = percent.toFixed(2) + '%';
                    }
                };
                splash.style.display = 'block';
                progressBar.style.width = '0%';
    
                cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, function () {
                    splash.style.display = 'none';
                });
            }
    
            var onStart = function () {
                cc.loader.downloader._subpackages = settings.subpackages;
    
                if (false) {
                    BK.Script.loadlib();
                }
    
                cc.view.resizeWithBrowserSize(true);
    
                if (!false && !false) {
                    if (cc.sys.isBrowser) {
                        setLoadingDisplay();
                    }
    
                    if (cc.sys.isMobile) {
                        if (settings.orientation === 'landscape') {
                            cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
                        }
                        else if (settings.orientation === 'portrait') {
                            cc.view.setOrientation(cc.macro.ORIENTATION_PORTRAIT);
                        }
                        cc.view.enableAutoFullScreen([
                            cc.sys.BROWSER_TYPE_BAIDU,
                            cc.sys.BROWSER_TYPE_WECHAT,
                            cc.sys.BROWSER_TYPE_MOBILE_QQ,
                            cc.sys.BROWSER_TYPE_MIUI,
                        ].indexOf(cc.sys.browserType) < 0);
                    }
    
                    // Limit downloading max concurrent task to 2,
                    // more tasks simultaneously may cause performance draw back on some android system / browsers.
                    // You can adjust the number based on your own test result, you have to set it before any loading process to take effect.
                    if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_ANDROID) {
                        cc.macro.DOWNLOAD_MAX_CONCURRENT = 2;
                    }
                }
    
                // init assets
                cc.AssetLibrary.init({
                    libraryPath: 'res/import',
                    rawAssetsBase: 'res/raw-',
                    rawAssets: settings.rawAssets,
                    packedAssets: settings.packedAssets,
                    md5AssetsMap: settings.md5AssetsMap
                });
    
                if (false) {
                    cc.Pipeline.Downloader.PackDownloader._doPreload("WECHAT_SUBDOMAIN", settings.WECHAT_SUBDOMAIN_DATA);
                }
    
                var launchScene = settings.launchScene;
    
                // load scene
                cc.director.loadScene(launchScene, null,
                    function () {
                        if (cc.sys.isBrowser) {
                            // show canvas
                            canvas.style.visibility = '';
                            var div = document.getElementById('GameDiv');
                            if (div) {
                                div.style.backgroundImage = '';
                            }
                        }
                        cc.loader.onProgress = null;
                        console.log('Success to load scene: ' + launchScene);
                    }
                );
            };
    
            // jsList
            var jsList = settings.jsList;
    
            if (!false) {
                var bundledScript = settings.debug ? 'src/project.dev.js' : 'src/project.js';
                if (jsList) {
                    jsList = jsList.map(function (x) {
                        return 'src/' + x;
                    });
                    jsList.push(bundledScript);
                }
                else {
                    jsList = [bundledScript];
                }
            }
    
            // anysdk scripts
            if (cc.sys.isNative && cc.sys.isMobile) {
    //            jsList = jsList.concat(['src/anysdk/jsb_anysdk.js', 'src/anysdk/jsb_anysdk_constants.js']);
            }
    
            var option = {
                //width: width,
                //height: height,
                id: 'GameCanvas',
                scenes: settings.scenes,
                debugMode: settings.debug ? cc.DebugMode.INFO : cc.DebugMode.ERROR,
                showFPS: (!false && !false) && settings.debug,
                frameRate: 60,
                jsList: jsList,
                groupList: settings.groupList,
                collisionMatrix: settings.collisionMatrix,
                renderMode: 0
            }
    
            cc.game.run(option, onStart);
        }
    
        if (false) {
            BK.Script.loadlib('GameRes://libs/qqplay-adapter.js');
            BK.Script.loadlib('GameRes://src/settings.js');
            BK.Script.loadlib();
            BK.Script.loadlib('GameRes://libs/qqplay-downloader.js');
            qqPlayDownloader.REMOTE_SERVER_ROOT = "";
            var prevPipe = cc.loader.md5Pipe || cc.loader.assetLoader;
            cc.loader.insertPipeAfter(prevPipe, qqPlayDownloader);
            // <plugin script code>
            boot();
            return;
        }
    
        if (false) {
            require(window._CCSettings.debug ? 'cocos2d-js.js' : 'cocos2d-js-min.js');
            require('./libs/weapp-adapter/engine/index.js');
            var prevPipe = cc.loader.md5Pipe || cc.loader.assetLoader;
            cc.loader.insertPipeAfter(prevPipe, wxDownloader);
            boot();
            return;
        }
    
        if (window.jsb) {
            require('src/settings.js');
            require('src/jsb_polyfill.js');
            boot();
            return;
        }
    
        if (window.document) {
            var splash = document.getElementById('splash');
            splash.style.display = 'block';
    
            var cocos2d = document.createElement('script');
            cocos2d.async = true;
            cocos2d.src = window._CCSettings.debug ? 'cocos2d-js.js' : 'cocos2d-js-min.js';
    
            var engineLoaded = function () {
                document.body.removeChild(cocos2d);
                cocos2d.removeEventListener('load', engineLoaded, false);
                if (typeof VConsole !== 'undefined') {
                    window.vConsole = new VConsole();
                }
                boot();
            };
            cocos2d.addEventListener('load', engineLoaded, false);
            document.body.appendChild(cocos2d);
        }
    
    })();
    

    修改好main.js后,就可以编辑项目安装到手机上了,接着修改项目,换个图片,保存然后构建,不用选MD5 Cache,构建成功后,修改version_generator.js版本号改为1.0.1,然后执行 node version_generator.js,然后把构建后的src、res和生成的project.manifest、version.manifest放在服务端,比如:
    通过mac模拟服务器 python -m SimpleHTTPServer 8000


    image.png

    启动好后,就可以打开App安装测试了。

    相关文章

      网友评论

        本文标题:CocosCreator热更新(v1.10.2)

        本文链接:https://www.haomeiwen.com/subject/tlvmgqtx.html