Cocos Creator熱更新

一,添加熱更新須要的文件

1. 在項目根目錄添加 version_generator.js 文件


version_generator.js 內容以下:css

/** * 此模塊用於熱更新工程清單文件的生成 */ 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'); }); 

注意:如下幾個地方,你可能須要根據本身的需求修改,本文 第三節第一點 會指出各個參數對應的狀況。html

2. 添加熱更新組件,並掛在熱更新腳本
image.png


這裏我簡單新建了一個helloWorld工程
添加了兩個button,check用於檢測是否有更新,update用於更新資源;
在canvas上掛載HotUpdate腳本,注意:這時ManifestUrl爲空。
給button check添加點擊事件,綁定checkForUpdate()
給button update添加點擊事件,綁定hotUpdate()node

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-defaultpython

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

2018/08/23測試發現,構建項目生成的main.js中,已包含判斷,不用修改,也能夠成功android

根據官方文檔提示,每次構建項目後,都須要修改main.js,那咱們就直接複製官方demo根目錄main.js的內容覆蓋原有內容。canvas

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: 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 所在目錄下
  輸入下方命令,啓動服務:app

python -m server 8000


  啓動完成後,服務器的地址即: http://192.168.200.117:8000
  對應的根目錄即爲server.py所在的目錄下,咱們在服務器根目錄下,新建HotUpdate文件夾,用於存儲新版本資源,經過網頁訪問以下:async

  詳細參見官方文檔給出的地址: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

相關文章
相關標籤/搜索