Cocos Creator 热更新(前辈的经验+当前版本的结合)

  • Post author:
  • Post category:其他


苦海无涯,特此记录。

先吐槽一下。官方的文档要么就是古董,要么就是分散。对于我这个新上手的人来说,上来cocosCreator版本就是2.1.2了,而参考文档还是1.X的版本、几年前的说明。


1、准备工作

1、cocos Creator 版本 V2.1.2

2、Android 原生打包环境–>自行百度(主要是sdk,nkd,ant)

3、cocosCreator 热更新插件 《

热更新manifest生成工具

4、hfs网络文件服务器 2.3(自行下载、安装。主要用做本地简单服务器的搭建)


2、预备知识

1、阅读一下官方文档


https://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html


https://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html

读完之后,是不是就知道更新大致需要的文档了

2、从官方文档中给出的大致过程



3、开始热更新

新建一个cocosCreator 项目。

1、开始还是先搭建一个简单的服务器

1、在硬盘中创建一个目录,用来热更新的目录

2、打开hfs.exe,将刚刚的目录拖入hfs左侧空白就会自动生成

这样就简单的生成了资源更新服务器了。

2、生成Manifest文档和更新资源包(官方是代码生成的,这里我使用的cocosCreator商店的工具)

->1、创建  version_generator.js 文件 (官方代码仓库也有)

var fs = require('fs');
var path = require('path');
var crypto = require('crypto');

var manifest = {
	//服务器上资源文件存放路径(src,res的路径)
    packageUrl: 'http://192.168.1.120:8080/hotUpdate/',
    remoteManifestUrl: 'http://192.168.1.120:8080/hotUpdate/project.manifest',
    remoteVersionUrl: 'http://192.168.1.120:8080/hotUpdate/version.manifest',
    version: '1.0.2',
    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)).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');
});

*****该文件主要作用是生成资源的更新信息,将创建的

version_generator.js 文档

放在主工程目录下

–>2、打开cocosCreator热更新工具

设置当前版本号,

第一次打包版本号可以就从1.0.0开始,

资源服务器url  对应服务器搭建的目录我这里是

http://192.168.1.120:8080/hotUpdate/

build资源路径就是我们在构建的时候生成的build文件模板

构建的时候,我选择的是default,所有在热更工具中build目录也是这个目录。

完成后,点击生成,再打开目录

已经生成版本文件。里面就是版本更新文件,解压后可以看到有四项文件(Manifest文件,res,src)

到这里,需要打包用的初始Manifest文件就已经生成了。

可以将这两个*.manifest文件拷贝到assets目录下

接下来就是创建热更新组件来负责这个更新逻辑了。

打开前面新建的项目,新建一个场景,一个更新逻辑js

1、创建场景,和HotUpdate.js

//HotUpdate.js
/**
 * 负责热更新逻辑的组件
 */
cc.Class({
    extends: cc.Component,

    properties: {
        manifestUrl: cc.RawAsset,  //本地project.manifest资源清单文件
        _updating: false,
        _canRetry: false,
        _storagePath: '',
        label: {
            default: null,
            type: cc.Label
        },
    },

    checkCb: function (event) {
        var self = this;
        cc.log('Code: ' + event.getEventCode());
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                self.label.string = '本地文件丢失';
                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.");
                self.label.string = '下载远程mainfest文件错误';
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log("Already up to date with the latest remote version.");
                self.label.string = '已经是最新版本';
                break;
            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                cc.log('New version found, please try to update.');
                self.label.string = '有新版本发现,请点击更新';
                //this.hotUpdate();  暂时去掉自动更新
                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...');
                self.label.string = '本地版本文件丢失,无法更新';
                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.');
                self.label.string = '下载远程版本文件失败';
                failed = true;
                break;
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log('Already up to date with the latest remote version.');
                self.label.string = '当前为最新版本';
                failed = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FINISHED:
                cc.log('Update finished. ' + event.getMessage());
                self.label.string = '更新完成. ' + event.getMessage();
                needRestart = true;
                break;
            case jsb.EventAssetsManager.UPDATE_FAILED:
                cc.log('Update failed. ' + event.getMessage());
                self.label.string = '更新失败. ' + event.getMessage();
                this._updating = false;
                this._canRetry = true;
                break;
            case jsb.EventAssetsManager.ERROR_UPDATING:
                cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
                self.label.string = '资源更新错误: ' + event.getAssetId() + ', ' + event.getMessage();
                break;
            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                cc.log(event.getMessage());
                self.label.string = event.getMessage();
                break;
            default:
                break;
        }

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

        if (needRestart) {
            //cc.eventManager.removeListener(this._updateListener);
            this._am.setEventCallback(null);
            //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));
        this._checkListener.setEventCallback(this.checkCb.bind(this));
        //cc.eventManager.addListener(this._checkListener, 1);
        
        this._am.checkUpdate();

        this._updating = true;
    }, */

    checkForUpdate:function(){
/*         if (this._updating) {
            cc.log('Checking or updating ...');
            return;
        } */

        //cc.log("加载更新配置文件");

        //this._am.loadLocalManifest(this.manifestUrl);
        //cc.log(this.manifestUrl);

        //this.tipLabel.string = '检查更新';
        //cc.log("start checking...");

        //var state = this._am.getState()

        //if (state=== jsb.AssetsManager.State.UNINITED) {

        // Resolve md5 url

        console.log('检查更新')

        this._am.setEventCallback(this.checkCb.bind(this));

        this._failCount = 0;

        this._am.checkUpdate();

        this._updating = true;

        // }

    },

    hotUpdate: function () {
        if (this._am && !this._updating) {
            //this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
            this._am.setEventCallback(this.updateCb.bind(this));
            //cc.eventManager.addListener(this._updateListener, 1);

            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;
        // }
    },

    changesence:function(){
        cc.log("改变场景");
        cc.director.loadScene("helloworld");
    },

    // use this for initialization
    onLoad: function () {
        var self = this;
        // Hot update is only available in Native build
        console.log("onloadUpdate");
        if (!cc.sys.isNative) {
            return;
        }

        this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + '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);
            self.label.string = "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);
        // 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;
            }
        }.bind(this));

        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._am.loadLocalManifest(this.manifestUrl);
        
        cc.log(this.manifestUrl);

        //检查更新
        this.checkUpdate()
    },

    checkUpdate:function() {
        console.log('检查更新')

        this._am.setEventCallback(this.checkCb.bind(this));

        this._failCount = 0;

        this._am.checkUpdate();

        this._updating = true;
    },

    onDestroy: function () {
        if (this._updateListener) {
            //cc.eventManager.removeListener(this._updateListener);
            this._am.setEventCallback(null);
            //this._updateListener = null;
        }
        //if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
        //    this._am.release();
        //}
    }
});

新建热更新场景,绑定脚本,创建元素三个按钮,一个标签。

将HotUpdate.js绑定到canvas上,将对应的节点添加到上去。

在场景文件中,创建三个按钮,一个标签。并分别绑定按钮事件。

其中切换场景就是查看热更新效果。

这个project就是我们在使用热更新工具生成出来的manifest文件

第二步完成


接下来,再一次构建项目。注意顺序

在目录build\jsb-default中找到main.js的开头处添加代码

if (jsb) { 
	var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
	if (hotUpdateSearchPaths) { 
		jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
	}
} 

这里你可能会疑惑,这个HotUpdateSearchPaths是什么,它是HotUpdate.js中updateCb函数中设置的对应的目录。

这个时候回到刚刚编译的界面,记得不要再构建了,直接编译出包。

等待中。。。

出包后,放到手机上安装。这个包就是当做旧版本包来使用。前面设置的版本是1.0.0


其实到这里,热更新已经完成了。测试一下。。注意顺序

在项目中,修改helloWord场景的元素。然后

构建

(这个时候,不是新包,不需要修改main.js 中的内容),然后打开热更新工具,修改版本号(提高一个版本),生成。将生成的资源拷贝到搭建的服务器热更目录下,解压。

这个时候再打开手机上的apk,就会发现提示新版本下载。


热更到此结束(罗里吧嗦的一大堆)!

如果还是没有成功。那就再检查一下

1、在version_generator.js文件中

2、在构建好的目录中

在该目录下的main.js中没有添加更新后的路径搜索代码

要还是崩。。。那就自己先看日志自己解决吧(其他的日志都在手机上会显示)。

最后提一下

以前很多前辈写的一写文章都是摸爬打滚出来的。很有参考价值。

但是,奈何敌不过CocosCreator自己的版本更新。

所以很多的bug都是没有试用新版本而已。及时关注他们的更新指南。

这是2.0的升级指南:

https://docs.cocos.com/creator/manual/zh/release-notes/upgrade-guide-v2.0.html

里面就改进了监听(主要是有个问题困扰我很久,最后看网友说去看升级指南。。。,然后就看到问题所在了。)

最后感谢这位博主

https://www.cnblogs.com/gao88/p/11632626.html

,给出了一个非常详细的过程。

最后指出一下,本博文只是简单的热更新。在代码中有很多屏蔽的地方,都是奔溃的。我只是让它实现了最基本的热更新。

还没有测试ios下,也没有太多的优化。比如进度条显示、文件显示。

后续再更新吧!

今天先到这里。



版权声明:本文为huanghuipost原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。