Cocos Creator大厅+子游戏模式

叁歲伎倆 2022-03-26 04:08 308阅读 0赞

文章转载自:https://www.jianshu.com/p/fe54ca980384

一、前言

根据上一篇(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的内容如下:

  1. (function () {
  2. 'use strict';
  3. if (window.jsb) {
  4. /// 1.初始化资源Lib路径Root.
  5. var subgameSearchPath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/')+'ALLGame/subgame/';
  6. /// 2.subgame资源未映射,则初始化资源映射表,否则略过映射.
  7. if(!cc.HallAndSubGameGlobal.subgameGlobal){
  8. cc.HallAndSubGameGlobal.subgameGlobal = {};
  9. /// 加载settings.js
  10. require(subgameSearchPath + 'src/settings.js');
  11. var settings = window._CCSettings;
  12. window._CCSettings = undefined;
  13. if ( !settings.debug ) {
  14. var uuids = settings.uuids;
  15. var rawAssets = settings.rawAssets;
  16. var assetTypes = settings.assetTypes;
  17. var realRawAssets = settings.rawAssets = {};
  18. for (var mount in rawAssets) {
  19. var entries = rawAssets[mount];
  20. var realEntries = realRawAssets[mount] = {};
  21. for (var id in entries) {
  22. var entry = entries[id];
  23. var type = entry[1];
  24. // retrieve minified raw asset
  25. if (typeof type === 'number') {
  26. entry[1] = assetTypes[type];
  27. }
  28. // retrieve uuid
  29. realEntries[uuids[id] || id] = entry;
  30. }
  31. }
  32. var scenes = settings.scenes;
  33. for (var i = 0; i < scenes.length; ++i) {
  34. var scene = scenes[i];
  35. if (typeof scene.uuid === 'number') {
  36. scene.uuid = uuids[scene.uuid];
  37. }
  38. }
  39. var packedAssets = settings.packedAssets;
  40. for (var packId in packedAssets) {
  41. var packedIds = packedAssets[packId];
  42. for (var j = 0; j < packedIds.length; ++j) {
  43. if (typeof packedIds[j] === 'number') {
  44. packedIds[j] = uuids[packedIds[j]];
  45. }
  46. }
  47. }
  48. }
  49. /// 加载project.js
  50. var projectDir = 'src/project.js';
  51. if ( settings.debug ) {
  52. projectDir = 'src/project.dev.js';
  53. }
  54. require(subgameSearchPath + projectDir);
  55. /// 如果当前搜索路径没有subgame,则添加进去搜索路径。
  56. var currentSearchPaths = jsb.fileUtils.getSearchPaths();
  57. if(currentSearchPaths && currentSearchPaths.indexOf(subgameSearchPath) === -1){
  58. jsb.fileUtils.addSearchPath(subgameSearchPath, true);
  59. console.log('subgame main.js 之前未添加,添加下subgameSearchPath' + currentSearchPaths);
  60. }
  61. cc.AssetLibrary.init({
  62. libraryPath: 'res/import',
  63. rawAssetsBase: 'res/raw-',
  64. rawAssets: settings.rawAssets,
  65. packedAssets: settings.packedAssets,
  66. md5AssetsMap: settings.md5AssetsMap
  67. });
  68. cc.HallAndSubGameGlobal.subgameGlobal.launchScene = settings.launchScene;
  69. /// 将subgame的场景添加到cc.game中,使得cc.director.loadScene可以从cc.game._sceneInfos查找到相关场景
  70. for(var i = 0; i < settings.scenes.length; ++i){
  71. cc.game._sceneInfos.push(settings.scenes[i]);
  72. }
  73. }
  74. /// 3.加载初始场景
  75. var launchScene = cc.HallAndSubGameGlobal.subgameGlobal.launchScene;
  76. cc.director.loadScene(launchScene, null,
  77. function () {
  78. console.log('subgame main.js 成功加载初始场景' + launchScene);
  79. }
  80. );
  81. }
  82. })();

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

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

format_png

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

  1. 执行version_generator.js文件

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

三、拷贝res,src,version.manifest 和 project.mainfest到服务器目录下

format_png 1

  很明显,现在我们只是把子游戏生成了资源包,但是没有做任何热更新的操作。
接下来,就需要在大厅项目中,添加下载,更新的逻辑了。

四、在大厅项目中,添加相应逻辑

  负责下载,检测更新,更新子游戏的工具库文件内容如下:

  1. const SubgameManager = {
  2. _storagePath: [],
  3. _getfiles: function(name, type, downloadCallback, finishCallback) {
  4. this._storagePath[name] = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name);
  5. this._downloadCallback = downloadCallback;
  6. this._finishCallback = finishCallback;
  7. this._fileName = name;
  8. /// 替换该地址
  9. var UIRLFILE = "http://192.168.200.117:8000/" + name + "/remote-assets";
  10. var filees = this._storagePath[name] + '/project.manifest';
  11. var customManifestStr = JSON.stringify({
  12. 'packageUrl': UIRLFILE,
  13. 'remoteManifestUrl': UIRLFILE + '/project.manifest',
  14. 'remoteVersionUrl': UIRLFILE + '/version.manifest',
  15. 'version': '0.0.1',
  16. 'assets': {},
  17. 'searchPaths': []
  18. });
  19. var versionCompareHandle = function(versionA, versionB) {
  20. var vA = versionA.split('.');
  21. var vB = versionB.split('.');
  22. for (var i = 0; i < vA.length; ++i) {
  23. var a = parseInt(vA[i]);
  24. var b = parseInt(vB[i] || 0);
  25. if (a === b) {
  26. continue;
  27. } else {
  28. return a - b;
  29. }
  30. }
  31. if (vB.length > vA.length) {
  32. return -1;
  33. } else {
  34. return 0;
  35. }
  36. };
  37. this._am = new jsb.AssetsManager('', this._storagePath[name], versionCompareHandle);
  38. if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
  39. this._am.retain();
  40. }
  41. this._am.setVerifyCallback(function(path, asset) {
  42. var compressed = asset.compressed;
  43. if (compressed) {
  44. return true;
  45. } else {
  46. return true;
  47. }
  48. });
  49. if (cc.sys.os === cc.sys.OS_ANDROID) {
  50. this._am.setMaxConcurrentTask(2);
  51. }
  52. if (type === 1) {
  53. this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._updateCb.bind(this));
  54. } else if (type == 2) {
  55. this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._checkCb.bind(this));
  56. } else {
  57. this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._needUpdate.bind(this));
  58. }
  59. cc.eventManager.addListener(this._updateListener, 1);
  60. if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
  61. var manifest = new jsb.Manifest(customManifestStr, this._storagePath[name]);
  62. this._am.loadLocalManifest(manifest, this._storagePath[name]);
  63. }
  64. if (type === 1) {
  65. this._am.update();
  66. this._failCount = 0;
  67. } else {
  68. this._am.checkUpdate();
  69. }
  70. this._updating = true;
  71. cc.log('更新文件:' + filees);
  72. },
  73. // type = 1
  74. _updateCb: function(event) {
  75. var failed = false;
  76. let self = this;
  77. switch (event.getEventCode()) {
  78. case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
  79. /*0 本地没有配置文件*/
  80. cc.log('updateCb本地没有配置文件');
  81. failed = true;
  82. break;
  83. case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
  84. /*1下载配置文件错误*/
  85. cc.log('updateCb下载配置文件错误');
  86. failed = true;
  87. break;
  88. case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
  89. /*2 解析文件错误*/
  90. cc.log('updateCb解析文件错误');
  91. failed = true;
  92. break;
  93. case jsb.EventAssetsManager.NEW_VERSION_FOUND:
  94. /*3发现新的更新*/
  95. cc.log('updateCb发现新的更新');
  96. break;
  97. case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
  98. /*4 已经是最新的*/
  99. cc.log('updateCb已经是最新的');
  100. failed = true;
  101. break;
  102. case jsb.EventAssetsManager.UPDATE_PROGRESSION:
  103. /*5 最新进展 */
  104. self._downloadCallback && self._downloadCallback(event.getPercentByFile());
  105. break;
  106. case jsb.EventAssetsManager.ASSET_UPDATED:
  107. /*6需要更新*/
  108. break;
  109. case jsb.EventAssetsManager.ERROR_UPDATING:
  110. /*7更新错误*/
  111. cc.log('updateCb更新错误');
  112. break;
  113. case jsb.EventAssetsManager.UPDATE_FINISHED:
  114. /*8更新完成*/
  115. self._finishCallback && self._finishCallback(true);
  116. break;
  117. case jsb.EventAssetsManager.UPDATE_FAILED:
  118. /*9更新失败*/
  119. self._failCount++;
  120. if (self._failCount <= 3) {
  121. self._am.downloadFailedAssets();
  122. cc.log(('updateCb更新失败' + this._failCount + ' 次'));
  123. } else {
  124. cc.log(('updateCb失败次数过多'));
  125. self._failCount = 0;
  126. failed = true;
  127. self._updating = false;
  128. }
  129. break;
  130. case jsb.EventAssetsManager.ERROR_DECOMPRESS:
  131. /*10解压失败*/
  132. cc.log('updateCb解压失败');
  133. break;
  134. }
  135. if (failed) {
  136. cc.eventManager.removeListener(self._updateListener);
  137. self._updateListener = null;
  138. self._updating = false;
  139. self._finishCallback && self._finishCallback(false);
  140. }
  141. },
  142. // type = 2
  143. _checkCb: function(event) {
  144. var failed = false;
  145. let self = this;
  146. switch (event.getEventCode()) {
  147. case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
  148. /*0 本地没有配置文件*/
  149. cc.log('checkCb本地没有配置文件');
  150. break;
  151. case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
  152. /*1下载配置文件错误*/
  153. cc.log('checkCb下载配置文件错误');
  154. failed = true;
  155. break;
  156. case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
  157. /*2 解析文件错误*/
  158. cc.log('checkCb解析文件错误');
  159. failed = true;
  160. break;
  161. case jsb.EventAssetsManager.NEW_VERSION_FOUND:
  162. /*3发现新的更新*/
  163. self._getfiles(self._fileName, 1, self._downloadCallback, self._finishCallback);
  164. break;
  165. case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
  166. /*4 已经是最新的*/
  167. cc.log('checkCb已经是最新的');
  168. self._finishCallback && self._finishCallback(true);
  169. break;
  170. case jsb.EventAssetsManager.UPDATE_PROGRESSION:
  171. /*5 最新进展 */
  172. break;
  173. case jsb.EventAssetsManager.ASSET_UPDATED:
  174. /*6需要更新*/
  175. break;
  176. case jsb.EventAssetsManager.ERROR_UPDATING:
  177. /*7更新错误*/
  178. cc.log('checkCb更新错误');
  179. failed = true;
  180. break;
  181. case jsb.EventAssetsManager.UPDATE_FINISHED:
  182. /*8更新完成*/
  183. cc.log('checkCb更新完成');
  184. break;
  185. case jsb.EventAssetsManager.UPDATE_FAILED:
  186. /*9更新失败*/
  187. cc.log('checkCb更新失败');
  188. failed = true;
  189. break;
  190. case jsb.EventAssetsManager.ERROR_DECOMPRESS:
  191. /*10解压失败*/
  192. cc.log('checkCb解压失败');
  193. break;
  194. }
  195. this._updating = false;
  196. if (failed) {
  197. self._finishCallback && self._finishCallback(false);
  198. }
  199. },
  200. // type = 3
  201. _needUpdate: function(event) {
  202. let self = this;
  203. switch (event.getEventCode()) {
  204. case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
  205. cc.log('子游戏已经是最新的,不需要更新');
  206. self._finishCallback && self._finishCallback(false);
  207. break;
  208. case jsb.EventAssetsManager.NEW_VERSION_FOUND:
  209. cc.log('子游戏需要更新');
  210. self._finishCallback && self._finishCallback(true);
  211. break;
  212. // 检查是否更新出错
  213. case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
  214. case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
  215. case jsb.EventAssetsManager.ERROR_UPDATING:
  216. case jsb.EventAssetsManager.UPDATE_FAILED:
  217. self._downloadCallback();
  218. break;
  219. }
  220. },
  221. /**
  222. * 下载子游戏
  223. * @param {string} name - 游戏名
  224. * @param progress - 下载进度回调
  225. * @param finish - 完成回调
  226. * @note finish 返回true表示下载成功,false表示下载失败
  227. */
  228. downloadSubgame: function(name, progress, finish) {
  229. this._getfiles(name, 2, progress, finish);
  230. },
  231. /**
  232. * 进入子游戏
  233. * @param {string} name - 游戏名
  234. */
  235. enterSubgame: function(name) {
  236. if (!this._storagePath[name]) {
  237. this.downloadSubgame(name);
  238. return;
  239. }
  240. require(this._storagePath[name] + '/src/main.js');
  241. },
  242. /**
  243. * 判断子游戏是否已经下载
  244. * @param {string} name - 游戏名
  245. */
  246. isSubgameDownLoad: function (name) {
  247. let file = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name + '/project.manifest';
  248. if (jsb.fileUtils.isFileExist(file)) {
  249. return true;
  250. } else {
  251. return false;
  252. }
  253. },
  254. /**
  255. * 判断子游戏是否需要更新
  256. * @param {string} name - 游戏名
  257. * @param isUpdateCallback - 是否需要更新回调
  258. * @param failCallback - 错误回调
  259. * @note isUpdateCallback 返回true表示需要更新,false表示不需要更新
  260. */
  261. needUpdateSubgame: function (name, isUpdateCallback, failCallback) {
  262. this._getfiles(name, 3, failCallback, isUpdateCallback);
  263. },
  264. };
  265. module.exports = SubgameManager;

  调用的过程如下:
    1. 判断子游戏是否已下载
    2. 已下载,判断是否需要更新
    3.1 下载游戏
    3.2 更新游戏
    4. 进入子游戏

  1. const SubgameManager = require('SubgameManager');
  2. cc.Class({
  3. extends: cc.Component,
  4. properties: {
  5. downloadBtn: {
  6. default: null,
  7. type: cc.Node
  8. },
  9. downloadLabel: {
  10. default: null,
  11. type: cc.Label
  12. }
  13. },
  14. onLoad: function () {
  15. const name = 'subgame';
  16. //判断子游戏有没有下载
  17. if (SubgameManager.isSubgameDownLoad(name)) {
  18. //已下载,判断是否需要更新
  19. SubgameManager.needUpdateSubgame(name, (success) => {
  20. if (success) {
  21. this.downloadLabel.string = "子游戏需要更新";
  22. } else {
  23. this.downloadLabel.string = "子游戏不需要更新";
  24. }
  25. }, () => {
  26. cc.log('出错了');
  27. });
  28. } else {
  29. this.downloadLabel.string = "子游戏未下载";
  30. }
  31. this.downloadBtn.on('click', () => {
  32. //下载子游戏/更新子游戏
  33. SubgameManager.downloadSubgame(name, (progress) => {
  34. if (isNaN(progress)) {
  35. progress = 0;
  36. }
  37. this.downloadLabel.string = "资源下载中 " + parseInt(progress * 100) + "%";
  38. }, function(success) {
  39. if (success) {
  40. SubgameManager.enterSubgame('subgame');
  41. } else {
  42. cc.log('下载失败');
  43. }
  44. });
  45. }, this);
  46. },
  47. });

说到这呢,就得提一下,
如果界面设计时,从大厅点击子游戏,中间有loading的界面的话,
loading界面就应该放在大厅的工程中了。

五、测试

  打开服务———>编译大厅目录———>安装运行
注意:
    一定要生成原生apk,在真机(也可以是类似于夜神的模拟器啦)上运行测试。
结果:
    1. 第一次,本地没有子游戏,提示“游戏未下载”,下载后,无需重启,可直接进入子游戏;
    2. 修改version_generator.js中的版本号,将步骤二,再走一遍,能检测到更新,同样无需重启;
    3. 在大厅中,使用cc.sys.localStorage存储的值,在子游戏中可以获取到;

本人的一点小思考:

在研究之前,想着一定要研究一下资源共享的问题;
现在想来,既然要将子游戏独立出一个项目,自然也期望以后子游戏可以作为一个单独的apk来运行,如果共用大厅的资源,以后想抽取出来,又是一项艰巨的任务。但是这样必然会造成一定的重复资源。具体取舍,等到项目后期再协调。

作者:1号是雨天
链接:https://www.jianshu.com/p/fe54ca980384
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

发表评论

表情:
评论列表 (有 0 条评论,308人围观)

还没有评论,来说两句吧...

相关阅读