zoukankan      html  css  js  c++  java
  • Cocos Creator热更新

    一,添加热更新需要的文件

    1. 在项目根目录添加 version_generator.js 文件


    version_generator.js 内容如下:

    /**
     * 此模块用于热更新工程清单文件的生成
     */
    
    var fs = require('fs');
    var path = require('path');
    var crypto = require('crypto');
    
    var manifest = {
        //服务器上资源文件存放路径(src,res的路径)
        packageUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/',
        //服务器上project.manifest路径
        remoteManifestUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/project.manifest',
        //服务器上version.manifest路径
        remoteVersionUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/version.manifest',
        version: '1.0.0',
        assets: {},
        searchPaths: []
    };
    
    //生成的manifest文件存放目录
    var dest = 'assets/';
    //项目构建后资源的目录
    var src = 'build/jsb-link/';
    
    /**
     * 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');
    });
    

    注意:以下几个地方,你可能需要根据自己的需求修改,本文 第三节第一点 会指出各个参数对应的情况。

    2. 添加热更新组件,并挂在热更新脚本
    image.png


    这里我简单新建了一个helloWorld工程
    添加了两个button,check用于检测是否有更新,update用于更新资源;
    在canvas上挂载HotUpdate脚本,注意:这时ManifestUrl为空。
    给button check添加点击事件,绑定checkForUpdate()
    给button update添加点击事件,绑定hotUpdate()

    HotUpdate.js
    /**
     * 负责热更新逻辑的组件
     */
    cc.Class({
        extends: cc.Component,
    
        properties: {
            manifestUrl: cc.RawAsset,  //本地project.manifest资源清单文件
            _updating: false,
            _canRetry: false,
            _storagePath: ''
        },
    
        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.hotUpdate();
                    break;
                default:
                    return;
            }
    
            cc.eventManager.removeListener(this._checkListener);
            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:
                    cc.log(event.getPercent());
                    cc.log(event.getPercentByFile());
                    cc.log(event.getDownloadedFiles() + ' / ' + event.getTotalFiles());
                    cc.log(event.getDownloadedBytes() + ' / ' + event.getTotalBytes());
    
                    var msg = event.getMessage();
                    if (msg) {
                        cc.log('Updated file: ' + 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) {
                cc.eventManager.removeListener(this._updateListener);
                this._updateListener = null;
                this._updating = 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(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();
            }
        },
    
    
        retry: function () {
            if (!this._updating && this._canRetry) {
                this._canRetry = false;
    
                cc.log('Retry failed Assets...');
                this._am.downloadFailedAssets();
            }
        },
    
        checkForUpdate: function () {
            cc.log("start checking...");
            if (this._updating) {
                cc.log('Checking or updating ...');
                return;
            }
            if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
                this._am.loadLocalManifest(this.manifestUrl);
                cc.log(this.manifestUrl);
            }
            if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
                cc.log('Failed to load local manifest ...');
                return;
            }
            this._checkListener = new jsb.EventListenerAssetsManager(this._am, this.checkCb.bind(this));
            cc.eventManager.addListener(this._checkListener, 1);
            
            this._am.checkUpdate();
    
            this._updating = true;
        },
    
        hotUpdate: function () {
            if (this._am) {
                this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
                cc.eventManager.addListener(this._updateListener, 1);
    
                if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
                    this._am.loadLocalManifest(this.manifestUrl);
                }
    
                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() : '/') + 'xiaoming-remote-asset');
            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);
            if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
                this._am.retain();
            }
            // 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");
            }
            // this.checkUpdate();
        },
    
        onDestroy: function () {
            if (this._updateListener) {
                cc.eventManager.removeListener(this._updateListener);
                this._updateListener = null;
            }
            if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
                this._am.release();
            }
        }
    });
    
    
    3. 添加点击事件
    4. 构建项目,生成原生资源文件

    注意: 我们选择的模板是 "default" ,发布路径为 "./build" ,发布后的项目资源相对路径为:build/jsb-default

    5. 修改main.js(可省略)

    2018/08/23测试发现,构建项目生成的main.js中,已包含判断,不用修改,也可以成功

    根据官方文档提示,每次构建项目后,都需要修改main.js,那我们就直接复制官方demo根目录main.js的内容覆盖原有内容。

    main.js 内容如下:

    main.js
    
    (function () {
    
        if (window.cc && cc.sys.isNative) {
            var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
            if (hotUpdateSearchPaths) {
                jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
            }
        }
    
        'use strict';
    
        function boot () {
    
            var settings = window._CCSettings;
            window._CCSettings = undefined;
    
            if ( !settings.debug ) {
                // retrieve minified raw assets
                var rawAssets = settings.rawAssets;
                var assetTypes = settings.assetTypes;
                for (var mount in rawAssets) {
                    var entries = rawAssets[mount];
                    for (var uuid in entries) {
                        var entry = entries[uuid];
                        var type = entry[1];
                        if (typeof type === 'number') {
                            entry[1] = assetTypes[type];
                        }
                    }
                }
            }
    
            // init engine
            var canvas;
    
            if (cc.sys.isBrowser) {
                canvas = document.getElementById('GameCanvas');
            }
    
            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.view.resizeWithBrowserSize(true);
                // UC browser on many android devices have performance issue with retina display
                if (cc.sys.os !== cc.sys.OS_ANDROID || cc.sys.browserType !== cc.sys.BROWSER_TYPE_UC) {
                    cc.view.enableRetina(true);
                }
                //cc.view.setDesignResolutionSize(settings.designWidth, settings.designHeight, cc.ResolutionPolicy.SHOW_ALL);
    
                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);
                    }
                    // qq, wechat, baidu
                    cc.view.enableAutoFullScreen(
                        cc.sys.browserType !== cc.sys.BROWSER_TYPE_BAIDU &&
                        cc.sys.browserType !== cc.sys.BROWSER_TYPE_WECHAT &&
                        cc.sys.browserType !== cc.sys.BROWSER_TYPE_MOBILE_QQ
                    );
                }
    
                // init assets
                cc.AssetLibrary.init({
                    libraryPath: 'res/import',
                    rawAssetsBase: 'res/raw-',
                    rawAssets: settings.rawAssets,
                    packedAssets: settings.packedAssets
                });
    
                var launchScene = settings.launchScene;
    
                // load scene
                if (cc.runtime) {
                    cc.director.setRuntimeLaunchScene(launchScene);
                }
                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;
    
                        // play game
                        // cc.game.resume();
    
                        console.log('Success to load scene: ' + launchScene);
                    }
                );
            };
    
            // jsList
            var jsList = settings.jsList;
            var bundledScript = settings.debug ? 'project.dev.js' : 'project.js';
            if (jsList) {
                jsList.push(bundledScript);
            }
            else {
                jsList = [bundledScript];
            }
    
            // anysdk scripts
            if (cc.sys.isNative && cc.sys.isMobile) {
                jsList = jsList.concat(['jsb_anysdk.js', 'jsb_anysdk_constants.js']);
            }
    
            jsList = jsList.map(function (x) { return 'src/' + x; });
    
            var option = {
                // width,
                //height: height,
                id: 'GameCanvas',
                scenes: settings.scenes,
                debugMode: settings.debug ? cc.DebugMode.INFO : cc.DebugMode.ERROR,
                showFPS: settings.debug,
                frameRate: 60,
                jsList: jsList,
                groupList: settings.groupList,
                collisionMatrix: settings.collisionMatrix,
                renderMode: 0
            };
    
            cc.game.run(option, onStart);
        }
    
        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);
                boot();
            };
            cocos2d.addEventListener('load', engineLoaded, false);
            document.body.appendChild(cocos2d);
        }
        else if (window.jsb) {
            require('src/settings.js');
            require('src/jsb_polyfill.js');
    
            boot();
        }
    
    })();
    

    二,服务器搭建(仅局域网内可访问)

    1. 编写服务器脚本
    server.py
    
    import SimpleHTTPServer
    import SocketServer
    
    PORT = 8000
    
    Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
    
    httpd = SocketServer.TCPServer(("", PORT), Handler)
    
    print "serving at port", PORT
    httpd.serve_forever()
    
    2. 启动服务

      打开命令行
      切换目录到 server.py 所在目录下
      输入下方命令,启动服务:

    python -m server 8000
    


      启动完成后,服务器的地址即: http://192.168.200.117:8000
      对应的根目录即为server.py所在的目录下,我们在服务器根目录下,新建HotUpdate文件夹,用于存储新版本资源,通过网页访问如下:

      详细参见官方文档给出的地址:https://docs.python.org/2/library/simplehttpserver.html

    三,生成旧版清单文件

    1. 修改version_generator.js

    packageUrl:服务器存放资源文件(src res)的路径
    remoteManifestUrl:服务器存放资源清单文件(project.manifest)的路径
    remoteVersionUrl:服务器存放version.manifest的路径
    dest:要生成的manifest文件存放路径
    src:项目构建后的资源目录

    2. 根据构建后的资源目录,执行version_generator.js,生成manifest清单文件

      打开cmd,切换到当前项目根目录下,执行下方命令:

    //官方给出的命令格式
    >node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
    
    //我的命令
    >node version_generator.js -v 1.0.0 -u http://192.168.200.117:8000/HotUpdate/ -s build/jsb-default/ -d assets
    
    //由于我们version_generator文件中,都配置好了参数
    //因此可以简单调用以下命令即可
    >node version_generator.js
    
    • -v: 指定 Manifest 文件的主版本号。
    • -u: 指定服务器远程包的地址,这个地址需要和最初发布版本中 Manifest 文件的远程包地址一致,否则无法检测到更新。
    • -s: 本地原生打包版本的目录相对路径。
    • -d: 保存 Manifest 文件的地址。

      生成的Manifest文件目录,如下:

    PS:如果version_generator.js中的配置都正确,特别是版本号,可以直接执行 node version_generator.js

    3. 并绑定到热更新脚本上

      如果指定的Manifest文件生成的目录不在assets下,则需将project.manifest复制到assets目录下

      并将project.manifest绑定到HotUpdate.js热更新脚本上

    4. 打包旧版本

      构建项目->编译
      在真机上运行build/jsb-default/simulator目录下的apk
      1.0.0版本运行如下:

    三,生成新版本

    1. 更改代码,更改version_generator.js中的版本号

      修改logo图片,1.0.1版本,本地运行结果,如下:

    2. 构建项目 && 重新生成资源清单文件

      构建项目:此步骤和生成旧版本中一样,这里就不截图啦。
      重新生成资源清单文件:修改version_generator.js中的版本号后,可以直接调用以下命令:

    >node version_generator.js
    
    3. 将manifest文件以及src,res拷贝到服务器
    4. 运行旧版本

      运行结果,点击 检测更新 后,很快app重启,logo变成了head.png。
    成功!

    由于这里检测到新版本后,就开始自动更新。

    你可以修改这部分逻辑,检测到有新版后,弹窗提示是否需要更新。

    后面再继续研究 大厅+子游戏的模式,以及不重启加载子游戏方案...

    来源:https://www.jianshu.com/p/094cd0e95e55

  • 相关阅读:
    冲刺第七,八天(5月27,28日)
    作业4 阅读《构建之法》 第5.5 第6 第7章
    用户模拟+spec
    第二阶段
    第一次Spring总结
    小组互评和自评
    SPRINT四则运算(第二天)
    开始第一段SPRINT
    四则运算APP
    四则运算 测试与封装 (完善) 5.2 5.3
  • 原文地址:https://www.cnblogs.com/gao88/p/11632626.html
Copyright © 2011-2022 走看看