入门CocosCreator大概一年,由于项目需要,要用到热更新,由于之前没接触过,于是根据官方文档把热更新走了一遍后,其中遇到各种问题,大大小小的坑也爬了不少,于是把它记录下来也方便自己以后查看,希望对需要的人也有点帮助。
1.搭建好cocos打包环境(可自行百度) 2.热更环境(这里使用的是cocos官方商店插件,自己可以去商店下载安装即可,安装完成后重启Cocos,这里也不做过多介绍) 3. 下载官方热更范列教程 接下来进入正题。
一.我们先打开官方教程,对照官方文档,了解一下大致流程,发现热更流程大致分为下面四点: 1.基于原生打包目录中的 res 和 src 目录生成本地 Manifest 文件。 2.创建一个热更新组件来负责热更新逻辑。 3.游戏发布后,若需要更新版本,则生成一套远程版本资源,包含 res 目录、src 目录和 Manifest 文件,将远程版本部署到服务端。 4.当热更新组件检测到服务端 Manifest 版本不一致时,就会开始热更新
具体请参考官方地址 热更新官方案列
下面详细介绍更新步骤 1.先构建 2.在项目下面打开热更新工具(前面在商店下载安装的热更新工具)
填写好对应的信息,点击生成即可。 3.如果第一次出包,则找到刚生成的热更新资源(在工程文件根目录下会生成packVersion,刚生成的资源包就在这个文件夹下),解压,然后发现四个文件,sre和res主要是资源文件和代码,project.manifest和version.mainfast 将project.manifest和version.manifest拷贝到工程目录下覆盖之前的(官方的mainfast直接写在了hotupdate.js里面,我们当然不能这样做,于是把他放在我的工程目录下),我目前工程放在这里,然后在重新构建。 4.在build文件夹下找到main.js文件
打开main.Js,在开头加上红色框里面的代码(不加的话会导致热更完下次打开游戏还是之前的资源)这里帮你写好了,直接复制即可(这里有一个问题注意,官方if (cc.sys.isNative) {}我按照这样写,android启动游戏会黑屏,然后改成if (jsb) {})
if (jsb) { var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths'); if (hotUpdateSearchPaths) { jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths)); } }这里是hotupdate.js代码
cc.Class({ extends: cc.Component, properties: { panel: UpdatePanel, manifestUrl: { type: cc.Asset, default: null }, updateUI: cc.Node, _updating: false, _canRetry: false, _storagePath: '' }, checkCb: function (event) { cc.log('Code: ' + event.getEventCode()); switch (event.getEventCode()) { case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: this.panel.info.string = "No local manifest file found, hot update skipped."; break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: this.panel.info.string = "Fail to download manifest file, hot update skipped."; break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: this.panel.info.string = "Already up to date with the latest remote version."; break; case jsb.EventAssetsManager.NEW_VERSION_FOUND: this.panel.info.string = 'New version found, please try to update.'; this.panel.checkBtn.active = false; this.panel.fileProgress.progress = 0; this.panel.byteProgress.progress = 0; 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: this.panel.info.string = 'No local manifest file found, hot update skipped.'; failed = true; break; case jsb.EventAssetsManager.UPDATE_PROGRESSION: this.panel.byteProgress.progress = event.getPercent(); this.panel.fileProgress.progress = event.getPercentByFile(); this.panel.fileLabel.string = event.getDownloadedFiles() + ' / ' + event.getTotalFiles(); this.panel.byteLabel.string = event.getDownloadedBytes() + ' / ' + event.getTotalBytes(); var msg = event.getMessage(); if (msg) { this.panel.info.string = 'Updated file: ' + msg; // cc.log(event.getPercent()/100 + '% : ' + msg); } break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: this.panel.info.string = 'Fail to download manifest file, hot update skipped.'; failed = true; break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: this.panel.info.string = 'Already up to date with the latest remote version.'; failed = true; break; case jsb.EventAssetsManager.UPDATE_FINISHED: this.panel.info.string = 'Update finished. ' + event.getMessage(); needRestart = true; break; case jsb.EventAssetsManager.UPDATE_FAILED: this.panel.info.string = 'Update failed. ' + event.getMessage(); this.panel.retryBtn.active = true; this._updating = false; this._canRetry = true; break; case jsb.EventAssetsManager.ERROR_UPDATING: this.panel.info.string = 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage(); break; case jsb.EventAssetsManager.ERROR_DECOMPRESS: this.panel.info.string = 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(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(); } }, loadCustomManifest: function () { if (this._am.getState() === jsb.AssetsManager.State.UNINITED) { var manifest = new jsb.Manifest(customManifestStr, this._storagePath); this._am.loadLocalManifest(manifest, this._storagePath); this.panel.info.string = 'Using custom manifest'; } }, retry: function () { if (!this._updating && this._canRetry) { this.panel.retryBtn.active = false; this._canRetry = false; this.panel.info.string = 'Retry failed Assets...'; this._am.downloadFailedAssets(); } }, checkUpdate: function () { if (this._updating) { this.panel.info.string = '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()) { this.panel.info.string = 'Failed to load local manifest ...'; return; } this._am.setEventCallback(this.checkCb.bind(this)); this._am.checkUpdate(); this._updating = true; }, hotUpdate: function () { 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.panel.updateBtn.active = false; 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() : '/') + 'blackjack-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); 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) { panel.info.string = "Verification passed : " + relativePath; return true; } else { panel.info.string = "Verification passed : " + relativePath + ' (' + expectedMD5 + ')'; return true; } }); this.panel.info.string = '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); this.panel.info.string = "Max concurrent tasks count have been limited to 2"; } this.panel.fileProgress.progress = 0; this.panel.byteProgress.progress = 0; }, onDestroy: function () { if (this._updateListener) { this._am.setEventCallback(null); this._updateListener = null; } } });当然按照这个流程热更新是没有问题得,但是我们当时有一个需求是需要根据服务器传过来得地址去动态选择热更新地址,那这个需求就不能满足我们了,于是又去各种查资料,发现只要能动态改变project.mainfast里面得地址就可以达到这个效果。于是就有两种情况。 ① 用户从未进行过热更新,这个时候App内的.manifest文件只有一份,我们只需要修改这个.manifest文件即可。 ②用户在安装该App期间使用过热更新,这时候App内部就会存在两份project.manifest文件了,此时我们要修改的project.manifest、version.manifest文件位于当初我们在项目中指定的热更新目录位置。如我的项目是: 这个目录根据自己写的实际情况来,然后就是修改project.mainfast的值(这里copy一下别人的代码)
这是连接地址
modifyAppLoadUrlForManifestFile(newAppHotUpdateUrl, localManifestPath, resultCallback) { if (jsb.fileUtils.isFileExist(jsb.fileUtils.getWritablePath() + 'remoteAssets/project.manifest')) { console.log("有下载的manifest文件"); let storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remoteAssets'); console.log("StoragePath for remote asset : ", storagePath); let loadManifest = jsb.fileUtils.getStringFromFile(storagePath + '/project.manifest'); let manifestObject = JSON.parse(loadManifest); manifestObject.packageUrl = "远程服务器地址'; manifestObject.remoteManifestUrl = "远程服务器地址" + 'project.manifest'; manifestObject.remoteVersionUrl = "远程服务器地址" + 'version.manifest'; let afterString = JSON.stringify(manifestObject); let isWritten = jsb.fileUtils.writeStringToFile(afterString, storagePath + '/project.manifest'); let initializedManifestPath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remoteAssets'); if (!jsb.fileUtils.isDirectoryExist(initializedManifestPath)) jsb.fileUtils.createDirectory(initializedManifestPath); console.log("storagePath==", initializedManifestPath); console.log("没有下载的manifest文件", newAppHotUpdateUrl); //修改原始manifest文件 let originManifestPath = localManifestPath; let originManifest = jsb.fileUtils.getStringFromFile(originManifestPath); let originManifestObject = JSON.parse(originManifest); originManifestObject.packageUrl = newAppHotUpdateUrl; originManifestObject.remoteManifestUrl = originManifestObject.packageUrl + 'project.manifest'; originManifestObject.remoteVersionUrl = originManifestObject.packageUrl + 'version.manifest'; let afterString = JSON.stringify(originManifestObject); let isWritten = jsb.fileUtils.writeStringToFile(afterString, initializedManifestPath + '/project.manifest'); console.log("Written Status : ", isWritten); } },修改完之后,在把这个project.mainfast转成mainfast文件,接下来就可以根据呢服务器传过来的地址动态修改热更地址了。 还有一个问题就是更新下载的资源是根据你远程project.mainfast的地址下载的。本地的project.mainfast主要用于下载远程project.mainfast,然后和远程比对MD5的值,不同的就下载。 到这里热更基本上流程已经走完了。 但是最后我们又遇到一个问题,就是热更过后,设置优先搜索路径之后(前面在main.js中加入的代码)会把你的热更路径设为优先搜索路径,下次遇到大版本更新的话,还是会优先从这个路径进行搜索,然后就会出现问题。所以,当遇到大版本更新的的时候,我们需要清除自己热更路径下资源。想要彻底清理一次本地的热更新缓存,那么怎么做呢,后来发现可以记录当前的游戏版本,检查 cc.sys.localStorage 中保存的版本是否匹配,如果不匹配则可以做一次清理操作:
// 之前版本保存在 local Storage 中的版本号,如果没有认为是旧版本 var previousVersion = parseFloat( cc.sys.localStorage.getItem('currentVersion') ); // game.currentVersion 为该版本的常量 if (previousVersion < game.currentVersion) { // 热更新的储存路径,如果旧版本中有多个,可能需要记录在列表中,全部清理 jsb.fileUtils.removeDirectory(storagePath); }好了,到这里基本上就结束了。(第一次发博客,如果发现有什么说的不对的地方,欢迎指正)