cocos creator 大廳+子游戲模式探討(creator版本1.8.2)
之前一直從事android開發,接觸cocos creator不久。近期公司安排我研究大廳子游戲模式的熱更新,前後花了近一週時間,在論壇上查資料,請教大神,期間得到了一些幫助,在此很感謝cocos中文論壇裡的前輩們。
談到cocos creator的子游戲的熱更新,相較於其他,諸如cocos-js,cocos2d-x等開發模式是有本質區別的。究其原因,是由於creator專案中無論是指令碼或是資源,對整體專案而言,都是資料檔案。由creator生成的專案是純粹的資料驅動而非程式碼驅動,一切皆為資料,包括場景、指令碼、圖片、音訊、動畫等。而資料驅動需要有一個入口,即是creator編譯器為我們生成的
基於上述特性,在官方熱更新的基礎上(具體詳見官方文件),我們開闢了一條子游戲獨有的更新思路,那就是將編譯生成的main.js檔案一併移入src目錄,讓其成為資料的一部分,在子游戲更新的時候會下載完整的
對於構建所生成的main.js檔案,顯然並非是我們想要的,因為它會載入預設的配置,對於整個原生應用而言,預設配置即為大廳的配置路徑,所以我們必須修改main.js檔案,使其找到子游戲自身的整個遊戲環境配置檔案,以下是子游戲main.js檔案的精簡程式碼,基本上註釋已經很完整,寫這篇文章的目的不是為了寫實現流程,因為大廳子游戲模式的實現流程論壇中已有較為完整的demo,這篇文章主要是分析子游戲的兩個進出檔案,另外是想說明在真實專案中運用這種方式會遇到哪些問題,以及我們怎麼處理這些問題。
(function () {
//以下兩句主要作用是釋放資源,垃圾回收,個人感覺作用不大,有或者沒有可
//能對記憶體有一點影響,釋放不會很乾淨
cc.loader.releaseAll();
cc.sys.garbageCollect();
//以下這句話官方解釋是fix bug ,不過實際上似乎沒什麼用,在這加著也無妨
cc.director.startAnimation();
'use strict';
var settings = null;
//以下是定義子游戲的路徑,與大廳中熱更新的某個子游戲路徑對應
cc.INGAME = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + “ALLGame/ subgame/4/“;
//以下的邏輯判斷做了下配置載入快取,第二次進入就不重複載入setting.js 與 project.js了很有必要
if (!cc.subgame4) {
console.log("首次載入路徑名稱-------" + cc.INGAME);
//這裡的require,論壇中沒有說清楚,在1.5.2版本中creator可以通過var settings =require(‘…..')這種方式給settings賦值,但是後面引擎組對require //做了修改,後續版本中當require(‘…’)後,去取變數要看require對應的檔案中的程式碼而定.
require(cc.INGAME + 'src/settings.js');
cc.subgame4 = settings = window._CCSettings;
console.log("首次載入成功-------" + JSON.stringify(cc.subgame4));
require(cc.INGAME + 'src/project.js');
} else {
console.log("非首次載入路徑名稱-------" + cc.INGAME);
console.log("非首次載入成功-------" + JSON.stringify(cc.subgame4));
settings = cc.subgame4;
}
//將_ccsettings置位undefined,main.js都是這麼寫的
window._CCSettings = undefined;
//以下程式碼就是處理setting.js中的資源了,debug模式與非debug模式是不一樣的,下面的程式碼都是在非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]];
}
}
}
//遊戲的啟動函式
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);
}
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
);
}
// Limit downloading max concurrent task to 2,
// more tasks simultaneously may cause performance draw back on some android system / brwosers.
// You can adjust the number based on your own test result, you have to set it before any loading process to take effect.
if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_ANDROID) {
cc.macro.DOWNLOAD_MAX_CONCURRENT = 2;
}
//初始化資源方法,下面的路徑就是子游戲目錄下的,所以要加上cc.INGAME
cc.AssetLibrary.init({
libraryPath: cc.INGAME + 'res/import',
rawAssetsBase: cc.INGAME + 'res/raw-',
rawAssets: settings.rawAssets,
packedAssets: settings.packedAssets,
md5AssetsMap: settings.md5AssetsMap
});
var launchScene = settings.launchScene;
if (cc.runtime) {
cc.director.setRuntimeLaunchScene(launchScene);
}
// load scene
cc.director.loadScene(launchScene, null,
function () {
cc.loader.onProgress = null;
console.log('Success to load scene: ' + launchScene);
}
);
};
// jsList,指令碼的路徑也要加cc.INGAME
var jsList = settings.jsList;
var bundledScript = settings.debug ? cc.INGAME + 'src/project.dev.js' : cc.INGAME + 'src/project.js';
if (jsList) {
jsList = jsList.map(function (x) {
return cc.INGAME + 'src/' + x;
});
jsList.push(bundledScript);
}
else {
jsList = [bundledScript];
}
var option = {
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);
cc.sys.garbageCollect();
})();
再來看dating.js檔案
(function () {
cc.loader.releaseAll();
cc.sys.garbageCollect();
'use strict’;
//這裡只針對android的路徑做了修改,因為我發現論壇中如果定義cc.INGAME=“”,會找不到主大廳資源,ios沒有測
cc.INGAME = "assets/";
require(cc.INGAME+'src/settings.js');
var settings = window._CCSettings;
window._CCSettings = undefined;
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]];
}
}
}
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);
}
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
);
}
// Limit downloading max concurrent task to 2,
// more tasks simultaneously may cause performance draw back on some android system / brwosers.
// You can adjust the number based on your own test result, you have to set it before any loading process to take effect.
if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_ANDROID) {
cc.macro.DOWNLOAD_MAX_CONCURRENT = 2;
}
// init assets
cc.AssetLibrary.init({
libraryPath: cc.INGAME + 'res/import',
rawAssetsBase: cc.INGAME + 'res/raw-',
rawAssets: settings.rawAssets,
packedAssets: settings.packedAssets,
md5AssetsMap: settings.md5AssetsMap
});
//我們遊戲大廳就一個場景,所以無論是直接定義還是使用settings檔案中的launchScene都是可以的,如果大廳包含兩個場景,那需要這種方式了,不過做這種熱更方式,不建議大廳有多場景,可能會遇到意想不到的結果.
var launchScene = "db://assets/scene/MainScene.fire";
// load scene
if (cc.runtime) {
cc.director.setRuntimeLaunchScene(launchScene);
}
cc.director.loadScene(launchScene, null,
function () {
cc.loader.onProgress = null;
console.log('Success to load scene: ' + launchScene);
}
);
};
// jsList
var jsList = settings.jsList;
var bundledScript = settings.debug ? cc.INGAME + 'src/project.dev.js' : cc.INGAME + 'src/project.js';
if (jsList) {
jsList = jsList.map(function (x) {
return cc.INGAME + 'src/' + x;
});
jsList.push(bundledScript);
}
else {
jsList = [bundledScript];
}
var option = {
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);
cc.sys.garbageCollect();
})();
以上是從大廳進入子游戲的main.js與子游戲回大廳的dating.js的精簡版,當然還可以更精簡。
我們利用程式碼執行兩個helloword子游戲時是不會存在任何問題的,一旦把實際專案整合進來時問題就逐一顯現了,下面來一一列舉。
問題一:從大廳進入一個子遊戲,退回大廳,再進另一個子遊戲時,畫面整個錯亂
引發原因:兩個子游戲中,有部分資源uuid重複,很可能是在開發一個遊戲的時候,匯出了該遊戲的部分資源或場景進入了第二個遊戲。
解決方法下載uuid.php這個指令碼,放入assets目錄下執行 php uuid.php 將專案中所有的uuid替換,前提是事先要安裝php環境,注意指令碼中$QIANZIU=“b11”;每一個子遊戲都需要改一下這個值,因為這個指令碼會把所有uuid的前三位替換為在此定義的三位。
問題二:修改了uuid覺得萬事大吉的時候,那就想的太簡單了,你忽然發現進了一個遊戲,進另外一個黑了。
原因:場景、指令碼、節點名稱重複,程式沒有重新載入。
解決辦法:每個遊戲非公有的程式碼的指令碼名稱,元件名稱以及場景全部改名,建議以子游戲名稱打頭。
問題三:運行了一個遊戲後,再執行另一個遊戲,某些元件為null,聲音還在播放。
原因:返回遊戲時事件沒有反註冊,事件監聽器中存在元件例項。垃圾回收後,元件例項被回收,但事件監聽器中引用仍然存在,下一個遊戲發了同一條事件後,就會報錯,聲音資源沒有被釋放。
解決辦法:返回遊戲時,清空事件監聽器,釋放聲音資源。
問題四:全域性變數被覆蓋。
原因:兩個遊戲中存在相同名稱全域性變數,值不同,導致遊戲執行會有問題。
解決辦法:每個遊戲的全域性變數必須保證名稱不一致。
問題五:回大廳發現存在子游戲的畫面。
原因之一:定時任務沒有在返回時關閉。
解決辦法:查詢遊戲中的每一個定時任務,在返回時記得關閉任務。
以上問題都解決完之後似乎可以不衝突運行了,伴隨而來的是記憶體的壓力,不過無所謂,問題肯定還會有,不過已經少了。
對上述問題的的總結:在公司專案確定要使用這種方案來做子游戲更新時,勸大家評估一下專案命名是否規範,是否能容忍進出遊戲的緩慢,對專案命名的建議是:公共程式碼保證所有遊戲一樣時,無需對程式碼及變數改名,一旦公共程式碼載入記憶體,不會載入第二次,所以不用理會,同時在公共程式碼中不要儲存任何非公共的變數以及物件引用。對於非公共部分,要保證資源名稱,js指令碼名稱以及場景、節點名稱儘量使用子游戲名稱打頭來命名,然而即使都沒問題了,進入遊戲與返回大廳的速度依然是硬傷。
子游戲+大廳共享資源臨時實現方案,此方案通過修改jsb_polyfill.js 程式碼,也就是定義幾個全域性變數,這種方式在creator 1.8.2中行不通,進大廳就黑屏,creator1.7.2可行。這種方式確實能訪問到主大廳的資源,甚至能通過載入場景的方式進出子游戲,而且速度非常快,與載入一般場景的速度沒什麼區別,但是這其中的問題也非常多,就我遇到的,重啟大廳後進子游戲,子游戲都打不開了,還有一些資源的丟失問題,要使用這種方式需要對引擎有足夠多的瞭解,小弟不才,對cocos引擎研究不深。
無論是require方式還是通過修改jsb_polyfill.js方式,都有弊病,這種非官方的載入方式能否商用,值得商榷,至少我覺得真想做creator大廳子游戲模式還是等待官方更新比較靠譜,以上是我一個禮拜的cocos子游戲更新的心得,藉此分享一下。