Cocos Creator大厅+子游戏模式

Stella981
• 阅读 877

一、前言

根据上一篇(Cocos Creator热更新),可以看出以下几点:

  • build-default目录下的main.js,为cocos creator项目的入口;
  • 热更新一文中,放置在服务器上的,仅有资源,脚本,配置等,没有入口程序,因此本文中,我们需要创造一个入口程序。
还是解释一下什么叫大厅+子游戏模式:

  1. 将大厅单独作为一个完整的项目,不同的子游戏,则为不同的项目
  2. 然后要实现不同项目之间的互调,即大厅调子游戏,或者子游戏调大厅
  3. 资源共享,共用的资源放在大厅项目中,并且子游戏中可以调用

这样做的好处:

  1. 减小上架包的体积
  2. 提高热更新的效率(打开指定子游戏,才会更新子游戏)
  3. 降低项目的耦合性(如果不共享资源,子游戏完全可以随时抽取出来作为一个单独的包使用)

二、修改子游戏

1. 添加version_generato.js
2. 构建项目
3. 在原生src下,添加 main.js 入口文件
3.1 每次构建完项目,拷贝main.js到原生目录的src中

  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 < scenes.length; ++i) { var scene = scenes[i]; if (typeof scene.uuid === 'number') { scene.uuid = uuids[scene.uuid]; } } var packedAssets = settings.packedAssets; for (var packId in packedAssets) { var packedIds = packedAssets[packId]; for (var j = 0; j < packedIds.length; ++j) { if (typeof packedIds[j] === 'number') { packedIds[j] = uuids[packedIds[j]]; } } } } /// 加载project.js var projectDir = 'src/project.js'; if ( settings.debug ) { projectDir = 'src/project.dev.js'; } require(subgameSearchPath + projectDir); /// 如果当前搜索路径没有subgame,则添加进去搜索路径。 var currentSearchPaths = jsb.fileUtils.getSearchPaths(); if(currentSearchPaths && currentSearchPaths.indexOf(subgameSearchPath) === -1){ jsb.fileUtils.addSearchPath(subgameSearchPath, true); console.log('subgame main.js 之前未添加,添加下subgameSearchPath' + currentSearchPaths); } cc.AssetLibrary.init({ libraryPath: 'res/import', rawAssetsBase: 'res/raw-', rawAssets: settings.rawAssets, packedAssets: settings.packedAssets, md5AssetsMap: settings.md5AssetsMap }); cc.HallAndSubGameGlobal.subgameGlobal.launchScene = settings.launchScene; /// 将subgame的场景添加到cc.game中,使得cc.director.loadScene可以从cc.game._sceneInfos查找到相关场景 for(var i = 0; i < settings.scenes.length; ++i){ cc.game._sceneInfos.push(settings.scenes[i]); } } /// 3.加载初始场景 var launchScene = cc.HallAndSubGameGlobal.subgameGlobal.launchScene; cc.director.loadScene(launchScene, null, function () { console.log('subgame main.js 成功加载初始场景' + launchScene); } ); } })(); 

ps: 不用管src外部的main.js文件

3.2 或者 添加build-templates目录,自动在每次构建项目后生成main.js文件

这里的main.js内容和上面的内容一致

4. 执行version_generator.js文件

  生成version.manifest 和 project.mainfest。这个在上一篇中已经讲过,就不细说了。

三、拷贝res,src,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; ++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; } }; 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 <= 3) { self._am.downloadFailedAssets(); cc.log(('updateCb更新失败' + this._failCount + ' 次')); } else { cc.log(('updateCb失败次数过多')); self._failCount = 0; failed = true; self._updating = false; } break; case jsb.EventAssetsManager.ERROR_DECOMPRESS: /*10解压失败*/ cc.log('updateCb解压失败'); break; } if (failed) { cc.eventManager.removeListener(self._updateListener); self._updateListener = null; self._updating = false; self._finishCallback && self._finishCallback(false); } }, // type = 2 _checkCb: function(event) { var failed = false; let self = this; switch (event.getEventCode()) { case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: /*0 本地没有配置文件*/ cc.log('checkCb本地没有配置文件'); break; case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: /*1下载配置文件错误*/ cc.log('checkCb下载配置文件错误'); failed = true; break; case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: /*2 解析文件错误*/ cc.log('checkCb解析文件错误'); failed = true; break; case jsb.EventAssetsManager.NEW_VERSION_FOUND: /*3发现新的更新*/ self._getfiles(self._fileName, 1, self._downloadCallback, self._finishCallback); break; case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: /*4 已经是最新的*/ cc.log('checkCb已经是最新的'); self._finishCallback && self._finishCallback(true); break; case jsb.EventAssetsManager.UPDATE_PROGRESSION: /*5 最新进展 */ break; case jsb.EventAssetsManager.ASSET_UPDATED: /*6需要更新*/ break; case jsb.EventAssetsManager.ERROR_UPDATING: /*7更新错误*/ cc.log('checkCb更新错误'); failed = true; break; case jsb.EventAssetsManager.UPDATE_FINISHED: /*8更新完成*/ cc.log('checkCb更新完成'); break; case jsb.EventAssetsManager.UPDATE_FAILED: /*9更新失败*/ cc.log('checkCb更新失败'); failed = true; break; case jsb.EventAssetsManager.ERROR_DECOMPRESS: /*10解压失败*/ cc.log('checkCb解压失败'); break; } this._updating = false; if (failed) { self._finishCallback && self._finishCallback(false); } }, // type = 3 _needUpdate: function(event) { let self = this; switch (event.getEventCode()) { case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: cc.log('子游戏已经是最新的,不需要更新'); self._finishCallback && self._finishCallback(false); break; case jsb.EventAssetsManager.NEW_VERSION_FOUND: cc.log('子游戏需要更新'); self._finishCallback && self._finishCallback(true); break; // 检查是否更新出错 case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: case jsb.EventAssetsManager.ERROR_UPDATING: case jsb.EventAssetsManager.UPDATE_FAILED: self._downloadCallback(); break; } }, /** * 下载子游戏 * @param {string} name - 游戏名 * @param progress - 下载进度回调 * @param finish - 完成回调 * @note finish 返回true表示下载成功,false表示下载失败 */ downloadSubgame: function(name, progress, finish) { this._getfiles(name, 2, progress, finish); }, /** * 进入子游戏 * @param {string} name - 游戏名 */ enterSubgame: function(name) { if (!this._storagePath[name]) { this.downloadSubgame(name); return; } require(this._storagePath[name] + '/src/main.js'); }, /** * 判断子游戏是否已经下载 * @param {string} name - 游戏名 */ isSubgameDownLoad: function (name) { let file = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name + '/project.manifest'; if (jsb.fileUtils.isFileExist(file)) { return true; } else { return false; } }, /** * 判断子游戏是否需要更新 * @param {string} name - 游戏名 * @param isUpdateCallback - 是否需要更新回调 * @param failCallback - 错误回调 * @note isUpdateCallback 返回true表示需要更新,false表示不需要更新 */ needUpdateSubgame: function (name, isUpdateCallback, failCallback) { this._getfiles(name, 3, failCallback, isUpdateCallback); }, }; module.exports = SubgameManager; 

  调用的过程如下:
    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来运行,如果共用大厅的资源,以后想抽取出来,又是一项艰巨的任务。但是这样必然会造成一定的重复资源。具体取舍,等到项目后期再协调。

 来源:https://www.jianshu.com/p/fe54ca980384

点赞
收藏
评论区
推荐文章
Stella981 Stella981
2年前
Shodan的http.favicon.hash语法详解与使用技巧
  在Shodan搜索中有一个关于网站icon图标的搜索语法,http.favicon.hash,我们可以使用这个语法来搜索出使用了同一icon图标的网站,不知道怎么用的朋友请参考我上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fmia
Stella981 Stella981
2年前
Cocos Creator 虚拟摇杆
版本:2.3.4参考:【持续更新】CocosCreator源码分享——针对游戏中的各种功能(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fforum.cocos.org%2Ft%2Fcocoscreator%2F87522)和原文的区别1. 监听事件不使用字符串
Wesley13 Wesley13
2年前
Java计模模式之六
前言在上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fzhaosq%2Fp%2F10219533.html)中我们学习了结构型模式的外观模式和装饰器模式。本篇则来学习下组合模式和过滤器模式。组合模式简介
Stella981 Stella981
2年前
Android Binder——framework
上一篇BindermRemote的前世今生(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.jianshu.com%2Fp%2F72bc48d59f0e)PowerManger.isScreenOn()的调用流程已经调用到了BinderProxy.transact();
Wesley13 Wesley13
2年前
1. 容器化部署一套云服务 第一讲 Jenkins(Docker + Jenkins + Yii2 + 云服务器))
容器化部署一套云服务系列1\.容器化部署一套云服务之Jenkins(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fjackson0714%2Fp%2Fdeploy1.html)一、购买服务器服务器!caeef00
Stella981 Stella981
2年前
Creator开源游戏、插件、教程、视频汇总
Creator开源游戏、插件、教程、视频汇总来源http://forum.cocos.com/t/creator/44782王哲(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fforum.cocos.com%2Fusers%2Fwa
Stella981 Stella981
2年前
Cocos Creator 下载不同版本引擎
CocosCreator版本:2.3.4例如已安装下载最新版本2.3.4,想安装2.3.3。下载引擎打开cocos官网 https://www.cocos.com/creator(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cocos.com%2Fcr
Stella981 Stella981
2年前
Spring Boot demo系列(三):Spring Web+MyBatis Plus
2021.2.24更新1概述SpringWebMyBatisPlus的一个Demo,内容和上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fblog.csdn.net%2Fqq_27525611%2Farticle%
Wesley13 Wesley13
2年前
Java进阶篇设计模式之三
前言在上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fxuwujing%2Fp%2F9363142.html)中我们学习了工厂模式,介绍了简单工厂模式、工厂方法和抽象工厂模式。本篇则介绍设计模式中属于创建型模式的建造者模式和原型模式。
Stella981 Stella981
2年前
HttpClient
接着上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.jianshu.com%2Fp%2Ff38a62efaa96),总结一下HttpClient发送https请求相关的内容。先简单介绍连接工厂(interfaceorg.apache.http.conn.sock