根据上一篇(Cocos Creator热更新),可以看出以下几点:
- build-default目录下的main.js,为cocos creator项目的入口;
- 热更新一文中,放置在服务器上的,仅有资源,脚本,配置等,没有入口程序,因此本文中,我们需要创造一个入口程序。
1. 将大厅单独作为一个完整的项目,不同的子游戏,则为不同的项目
2. 然后要实现不同项目之间的互调,即大厅调子游戏,或者子游戏调大厅
3. 资源共享,共用的资源放在大厅项目中,并且子游戏中可以调用
1. 减小上架包的体积
2. 提高热更新的效率(打开指定子游戏,才会更新子游戏)
3. 降低项目的耦合性(如果不共享资源,子游戏完全可以随时抽取出来作为一个单独的包使用)
main.js的内容如下:
(function () {
'use strict';
if (window.jsb) {
/// 1.初始化资源Lib路径Root.
var subgameSearchPath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/')+'ALLGame/subgame/';
/// 2.subgame资源未映射,则初始化资源映射表,否则略过映射.
if(!cc.HallAndSubGameGlobal.subgameGlobal){
cc.HallAndSubGameGlobal.subgameGlobal = {};
/// 加载settings.js
require(subgameSearchPath + 'src/settings.js');
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
ps: 不用管src外部的main.js文件
这里的main.js内容和上面的内容一致
生成version.manifest 和 project.mainfest。这个在上一篇中已经讲过,就不细说了。
很明显,现在我们只是把子游戏生成了资源包,但是没有做任何热更新的操作。
接下来,就需要在大厅项目中,添加下载,更新的逻辑了。
负责下载,检测更新,更新子游戏的工具库文件内容如下:
const SubgameManager = {
_storagePath: [],
_getfiles: function(name, type, downloadCallback, finishCallback) {
this._storagePath[name] = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name);
this._downloadCallback = downloadCallback;
this._finishCallback = finishCallback;
this._fileName = name;
/// 替换该地址
var UIRLFILE = "http://192.168.200.117:8000/" + name + "/remote-assets";
var filees = this._storagePath[name] + '/project.manifest';
var customManifestStr = JSON.stringify({
'packageUrl': UIRLFILE,
'remoteManifestUrl': UIRLFILE + '/project.manifest',
'remoteVersionUrl': UIRLFILE + '/version.manifest',
'version': '0.0.1',
'assets': {},
'searchPaths': []
});
var versiOnCompareHandle= function(versionA, versionB) {
var vA = versionA.split('.');
var vB = versionB.split('.');
for (var i = 0; i vA.length) {
return -1;
} else {
return 0;
}
};
this._am = new jsb.AssetsManager('', this._storagePath[name], versionCompareHandle);
if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
this._am.retain();
}
this._am.setVerifyCallback(function(path, asset) {
var compressed = asset.compressed;
if (compressed) {
return true;
} else {
return true;
}
});
if (cc.sys.os === cc.sys.OS_ANDROID) {
this._am.setMaxConcurrentTask(2);
}
if (type === 1) {
this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._updateCb.bind(this));
} else if (type == 2) {
this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._checkCb.bind(this));
} else {
this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._needUpdate.bind(this));
}
cc.eventManager.addListener(this._updateListener, 1);
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
var manifest = new jsb.Manifest(customManifestStr, this._storagePath[name]);
this._am.loadLocalManifest(manifest, this._storagePath[name]);
}
if (type === 1) {
this._am.update();
this._failCount = 0;
} else {
this._am.checkUpdate();
}
this._updating = true;
cc.log('更新文件:' + filees);
},
// type = 1
_updateCb: function(event) {
var failed = false;
let self = this;
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
/*0 本地没有配置文件*/
cc.log('updateCb本地没有配置文件');
failed = true;
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
/*1下载配置文件错误*/
cc.log('updateCb下载配置文件错误');
failed = true;
break;
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
/*2 解析文件错误*/
cc.log('updateCb解析文件错误');
failed = true;
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
/*3发现新的更新*/
cc.log('updateCb发现新的更新');
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
/*4 已经是最新的*/
cc.log('updateCb已经是最新的');
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
/*5 最新进展 */
self._downloadCallback && self._downloadCallback(event.getPercentByFile());
break;
case jsb.EventAssetsManager.ASSET_UPDATED:
/*6需要更新*/
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
/*7更新错误*/
cc.log('updateCb更新错误');
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
/*8更新完成*/
self._finishCallback && self._finishCallback(true);
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
/*9更新失败*/
self._failCount++;
if (self._failCount
调用的过程如下:
1. 判断子游戏是否已下载
2. 已下载,判断是否需要更新
3.1 下载游戏
3.2 更新游戏
4. 进入子游戏
const SubgameManager = require('SubgameManager');
cc.Class({
extends: cc.Component,
properties: {
downloadBtn: {
default: null,
type: cc.Node
},
downloadLabel: {
default: null,
type: cc.Label
}
},
onLoad: function () {
const name = 'subgame';
//判断子游戏有没有下载
if (SubgameManager.isSubgameDownLoad(name)) {
//已下载,判断是否需要更新
SubgameManager.needUpdateSubgame(name, (success) => {
if (success) {
this.downloadLabel.string = "子游戏需要更新";
} else {
this.downloadLabel.string = "子游戏不需要更新";
}
}, () => {
cc.log('出错了');
});
} else {
this.downloadLabel.string = "子游戏未下载";
}
this.downloadBtn.on('click', () => {
//下载子游戏/更新子游戏
SubgameManager.downloadSubgame(name, (progress) => {
if (isNaN(progress)) {
progress = 0;
}
this.downloadLabel.string = "资源下载中 " + parseInt(progress * 100) + "%";
}, function(success) {
if (success) {
SubgameManager.enterSubgame('subgame');
} else {
cc.log('下载失败');
}
});
}, this);
},
});
说到这呢,就得提一下,
如果界面设计时,从大厅点击子游戏,中间有loading的界面的话,
loading界面就应该放在大厅的工程中了。
打开服务——>编译大厅目录——>安装运行
注意:
一定要生成原生apk,在真机(也可以是类似于夜神的模拟器啦)上运行测试。
结果:
1. 第一次,本地没有子游戏,提示“游戏未下载”,下载后,无需重启,可直接进入子游戏;
2. 修改version_generator.js中的版本号,将步骤二,再走一遍,能检测到更新,同样无需重启;
3. 在大厅中,使用cc.sys.localStorage存储的值,在子游戏中可以获取到;
本人的一点小思考:
在研究之前,想着一定要研究一下资源共享的问题;
现在想来,既然要将子游戏独立出一个项目,自然也期望以后子游戏可以作为一个单独的apk来运行,如果共用大厅的资源,以后想抽取出来,又是一项艰巨的任务。但是这样必然会造成一定的重复资源。具体取舍,等到项目后期再协调。