Cocos Creator 性能優(yōu)化——對象池

?對于游戲開發(fā)人員來說晨川,性能優(yōu)化是一個(gè)永遠(yuǎn)繞不過的話題刷喜,極致的性能是我們畢生的追求荣回,今天就來帶大家學(xué)習(xí)一下性能優(yōu)化方法之一——「對象池」侄榴。

為什么要使用對象池雹锣?
在開始之前要先弄明白為什么要使用對象池?在運(yùn)行時(shí)進(jìn)行節(jié)點(diǎn)的創(chuàng)建(cc.instantiate)和銷毀(node.destroy)操作是非常耗費(fèi)性能的癞蚕,因此我們在比較復(fù)雜的場景中蕊爵,通常只有在場景初始化邏輯(onLoad)中才會進(jìn)行節(jié)點(diǎn)的創(chuàng)建,在切換場景時(shí)才會進(jìn)行節(jié)點(diǎn)的銷毀。如果制作有大量敵人或子彈需要反復(fù)生成和被消滅的動(dòng)作類游戲,我們要如何在游戲進(jìn)行過程中隨時(shí)創(chuàng)建和銷毀節(jié)點(diǎn)并且不會大幅度的消耗性能呢朗和?這里就需要對象池的幫助了鸟蟹。

通過下面這組數(shù)據(jù)可以看出使用對象池對游戲的性能是有質(zhì)的提升的:

對象池的概念(這里我們直接引用官方文檔
對象池就是一組可回收的節(jié)點(diǎn)對象,我們通過創(chuàng)建 cc.NodePool 的實(shí)例來初始化一種節(jié)點(diǎn)的對象池。通常當(dāng)我們有多個(gè) prefab 需要實(shí)例化時(shí),應(yīng)該為每個(gè) prefab 創(chuàng)建一個(gè) cc.NodePool 實(shí)例。當(dāng)我們需要?jiǎng)?chuàng)建節(jié)點(diǎn)時(shí)鸦概,向?qū)ο蟪厣暾堃粋€(gè)節(jié)點(diǎn),如果對象池里有空閑的可用節(jié)點(diǎn)甩骏,就會把節(jié)點(diǎn)返回給用戶窗市,用戶通過 node.addChild 將這個(gè)新節(jié)點(diǎn)加入到場景節(jié)點(diǎn)樹中。

當(dāng)我們需要銷毀節(jié)點(diǎn)時(shí)饮笛,調(diào)用對象池實(shí)例的 put(node) 方法咨察,傳入需要銷毀的節(jié)點(diǎn)實(shí)例,對象池會自動(dòng)完成把節(jié)點(diǎn)從場景節(jié)點(diǎn)樹中移除的操作福青,然后返回給對象池摄狱。這樣就實(shí)現(xiàn)了少數(shù)節(jié)點(diǎn)的循環(huán)利用。假如玩家在一關(guān)中要?dú)⑺?100 個(gè)敵人无午,但同時(shí)出現(xiàn)的敵人不超過 5 個(gè)媒役,那我們就只需要生成 5 個(gè)節(jié)點(diǎn)大小的對象池,然后循環(huán)使用就可以了宪迟。

使用對象池的一般工作流程
下面正式開始介紹對象池的使用流程酣衷。

1.準(zhǔn)備 Prefab
如果你對如何創(chuàng)建 Prefab 和動(dòng)態(tài)添加子節(jié)點(diǎn)流程還不熟悉的話,可以參考我之前寫的「一文帶你徹底明白如何實(shí)現(xiàn)動(dòng)態(tài)添加子節(jié)點(diǎn)及修改子節(jié)點(diǎn)屬性」次泽,這里不再具體展開說明穿仪。

2.初始化對象池
在場景加載的初始化腳本中席爽,我們可以將需要數(shù)量的節(jié)點(diǎn)創(chuàng)建出來,并放進(jìn)對象池:

//...
properties: {
    enemyPrefab: cc.Prefab
},
onLoad: function () {
    this.enemyPool = new cc.NodePool();
    let initCount = 5;
    for (let i = 0; i < initCount; ++i) {
        let enemy = cc.instantiate(this.enemyPrefab); // 創(chuàng)建節(jié)點(diǎn)
        this.enemyPool.put(enemy); // 通過 put 接口放入對象池
    }
}

對象池里需要的初始節(jié)點(diǎn)數(shù)量可以根據(jù)游戲的需要來控制啊片,即使我們對初始節(jié)點(diǎn)數(shù)量的預(yù)估不準(zhǔn)確也不要緊拳昌,后面我們會進(jìn)行處理。

3.從對象池請求對象
接下來在我們的運(yùn)行時(shí)代碼中就可以用下面的方式來獲得對象池中儲存的對象了:

// ...
createEnemy: function (parentNode) {
    let enemy = null;
    if (this.enemyPool.size() > 0) { // 通過 size 接口判斷對象池中是否有空閑的對象
        enemy = this.enemyPool.get();
    } else { // 如果沒有空閑對象钠龙,也就是對象池中備用對象不夠時(shí),我們就用 cc.instantiate 重新創(chuàng)建
        enemy = cc.instantiate(this.enemyPrefab);
    }
    enemy.parent = parentNode; // 將生成的敵人加入節(jié)點(diǎn)樹
    enemy.getComponent('Enemy').init(); //接下來就可以調(diào)用 enemy 身上的腳本進(jìn)行初始化
}

安全使用對象池的要點(diǎn)就是在 get 獲取對象之前御铃,永遠(yuǎn)都要先用 size 來判斷是否有可用的對象碴里,如果沒有就使用正常創(chuàng)建節(jié)點(diǎn)的方法,雖然會消耗一些運(yùn)行時(shí)性能上真,但總比游戲崩潰要好咬腋!另一個(gè)選擇是直接調(diào)用 get,如果對象池里沒有可用的節(jié)點(diǎn)睡互,會返回 null根竿,在這一步進(jìn)行判斷也可以。

4.將對象返回對象池
當(dāng)我們殺死敵人時(shí)就珠,需要將敵人節(jié)點(diǎn)退還給對象池寇壳,以備之后繼續(xù)循環(huán)利用,我們用這樣的方法:

// ...
onEnemyKilled: function (enemy) {
    // enemy 應(yīng)該是一個(gè) cc.Node
    this.enemyPool.put(enemy); // 和初始化時(shí)的方法一樣妻怎,將節(jié)點(diǎn)放進(jìn)對象池壳炎,這個(gè)方法會同時(shí)調(diào)用節(jié)點(diǎn)的 removeFromParent
}

這樣我們就完成了一個(gè)完整的循環(huán),主角需要刷多少怪都不成問題了逼侦!將節(jié)點(diǎn)放入和從對象池取出的操作不會帶來額外的內(nèi)存管理開銷匿辩,因此只要是可能,應(yīng)該盡量去利用榛丢。

5.使用組件來處理回收和復(fù)用的事件
使用構(gòu)造函數(shù)創(chuàng)建對象池時(shí)铲球,可以指定一個(gè)組件類型或名稱,作為掛載在節(jié)點(diǎn)上用于處理節(jié)點(diǎn)回收和復(fù)用事件的組件晰赞。假如我們有一組可點(diǎn)擊的菜單項(xiàng)需要做成對象池稼病,每個(gè)菜單項(xiàng)上有一個(gè) MenuItem.js 組件:

// MenuItem.js
cc.Class({
    extends: cc.Component,
?
    onLoad: function () {
        this.node.selected = false;
        this.node.on(cc.Node.EventType.TOUCH_END, this.onSelect, this.node);
    },
?
    unuse: function () {
        this.node.off(cc.Node.EventType.TOUCH_END, this.onSelect, this.node);
    },
?
    reuse: function () {
        this.node.on(cc.Node.EventType.TOUCH_END, this.onSelect, this.node);
    }
});

在創(chuàng)建對象池時(shí)可以用:

let menuItemPool = new cc.NodePool('MenuItem');

這樣當(dāng)使用 menuItemPool.get() 獲取節(jié)點(diǎn)后,就會調(diào)用 MenuItem 里的 reuse 方法宾肺,完成點(diǎn)擊事件的注冊溯饵。當(dāng)使用 menuItemPool.put(menuItemNode) 回收節(jié)點(diǎn)后,會調(diào)用 MenuItem 里的 unuse 方法锨用,完成點(diǎn)擊事件的反注冊丰刊。

另外 cc.NodePool.get() 可以傳入任意數(shù)量類型的參數(shù),這些參數(shù)會被原樣傳遞給 reuse 方法:

// BulletManager.js
let myBulletPool = new cc.NodePool('Bullet'); //創(chuàng)建子彈對象池
// ...
let newBullet = myBulletPool.get(this); // 傳入 manager 的實(shí)例增拥,用于之后在子彈腳本中回收子彈
?
// Bullet.js
reuse (bulletManager) {
    this.bulletManager = bulletManager; // get 中傳入的管理類實(shí)例
}
?
hit () {
    // ...
    this.bulletManager.put(this.node); // 通過之前傳入的管理類實(shí)例回收子彈
}

6.清除對象池
如果對象池中的節(jié)點(diǎn)不再被需要啄巧,我們可以手動(dòng)清空對象池寻歧,銷毀其中緩存的所有節(jié)點(diǎn):

myPool.clear(); // 調(diào)用這個(gè)方法就可以清空對象池

當(dāng)對象池實(shí)例不再被任何地方引用時(shí),引擎的垃圾回收系統(tǒng)會自動(dòng)對對象池中的節(jié)點(diǎn)進(jìn)行銷毀和回收秩仆。但這個(gè)過程的時(shí)間點(diǎn)不可控码泛,另外如果其中的節(jié)點(diǎn)有被其他地方所引用,也可能會導(dǎo)致內(nèi)存泄露澄耍,所以最好在切換場景或其他不再需要對象池的時(shí)候手動(dòng)調(diào)用 clear 方法來清空緩存節(jié)點(diǎn)噪珊。

7.使用 cc.NodePool 的優(yōu)勢
cc.NodePool 除了可以創(chuàng)建多個(gè)對象池實(shí)例,同一個(gè) prefab 也可以創(chuàng)建多個(gè)對象池齐莲,每個(gè)對象池中用不同參數(shù)進(jìn)行初始化痢站,大大增強(qiáng)了靈活性;此外 cc.NodePool 針對節(jié)點(diǎn)事件注冊系統(tǒng)進(jìn)行了優(yōu)化选酗,用戶可以根據(jù)自己的需要自由的在節(jié)點(diǎn)回收和復(fù)用的生命周期里進(jìn)行事件的注冊和反注冊阵难。

心得:
在使用對象池的時(shí)候,有時(shí)對象的回收和對象池的創(chuàng)建會不在一個(gè) js 文件里面芒填,這時(shí)對「節(jié)點(diǎn)樹」的理解和「對其他節(jié)點(diǎn)組件的訪問」就會顯得尤為重要呜叫,雖然這些東西在學(xué)的時(shí)候可能就是一個(gè)概念、一兩行代碼殿衰,但在整個(gè)游戲開發(fā)的過程中都是在這些基礎(chǔ)之上進(jìn)行的朱庆,切不可操之過急!共勉闷祥!

最后:
本來是想把這幾天使用對象池的心得寫一下的椎工,但是發(fā)現(xiàn)官方文檔對于初學(xué)者來說還是非常詳細(xì)的,于是幾乎原封不動(dòng)的搬了過來蜀踏,與其說分享到不如說是一個(gè)記錄维蒙。


我是「Super于」,立志做一個(gè)每天都有正反饋的人果覆!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颅痊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子局待,更是在濱河造成了極大的恐慌斑响,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钳榨,死亡現(xiàn)場離奇詭異舰罚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)薛耻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門营罢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事饲漾◎Γ” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵考传,是天一觀的道長吃型。 經(jīng)常有香客問我,道長僚楞,這世上最難降的妖魔是什么勤晚? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮泉褐,結(jié)果婚禮上运翼,老公的妹妹穿的比我還像新娘。我一直安慰自己兴枯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布矩欠。 她就那樣靜靜地躺著财剖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪癌淮。 梳的紋絲不亂的頭發(fā)上躺坟,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機(jī)與錄音乳蓄,去河邊找鬼咪橙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虚倒,可吹牛的內(nèi)容都是我干的美侦。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼魂奥,長吁一口氣:“原來是場噩夢啊……” “哼菠剩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起耻煤,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤具壮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后哈蝇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棺妓,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年炮赦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了怜跑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吠勘,死狀恐怖妆艘,靈堂內(nèi)的尸體忽然破棺而出彤灶,到底是詐尸還是另有隱情,我是刑警寧澤批旺,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布幌陕,位于F島的核電站,受9級特大地震影響汽煮,放射性物質(zhì)發(fā)生泄漏搏熄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一暇赤、第九天 我趴在偏房一處隱蔽的房頂上張望心例。 院中可真熱鬧,春花似錦鞋囊、人聲如沸止后。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽译株。三九已至,卻和暖如春挺益,著一層夾襖步出監(jiān)牢的瞬間歉糜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工望众, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匪补,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓烂翰,卻偏偏與公主長得像夯缺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子甘耿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 使用對象池 在運(yùn)行時(shí)進(jìn)行節(jié)點(diǎn)的創(chuàng)建(cc.instantiate)和銷毀(node.destroy)操作是非常耗費(fèi)...
    工匠良辰閱讀 1,812評論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,113評論 1 32
  • cc.Component 生命周期回調(diào) cc.path cc.loader.loadRes cc.find('ta...
    niccgz閱讀 2,727評論 1 7
  • aed是什么? AED指自動(dòng)體外除顫器喳逛,是一種便攜式的醫(yī)療設(shè)備,使用簡單棵里,它可以診斷特定的心律失常润文,并且自動(dòng)判斷是...
    帥呆呆沒有毛病閱讀 1,368評論 1 0
  • 記得小學(xué)的時(shí)候,流行做手抄報(bào)殿怜,在其他小朋友看來很痛苦的作業(yè)典蝌,確是我每周最喜愛的作業(yè)。記得那時(shí)候每周六或周末...
    張慧_3e37閱讀 536評論 0 0