?對于游戲開發(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è)每天都有正反饋的人果覆!