為什么要使用對象池
對象池優化是游戲開發中非常重要的優化方式,也是影響游戲性能的重要因素之一。
在游戲中有許多對象在不停的創建與移除,比如角色攻擊子彈、特效的創建與移除,NPC的被消滅與刷新等,在創建過程中非常消耗性能,特別是數量多的情況下。
對象池技術能很好解決以上問題,在對象移除消失的時候回收到對象池,需要新對象的時候直接從對象池中取出使用。
優點是減少了實例化對象時的開銷,且能讓對象反復使用,減少了新內存分配與垃圾回收器運行的機會。
Cocos官方文檔說明的使用方式
https://docs.cocos.com/creator/manual/zh/scripting/pooling.html
- 這樣的一個對象池,其實嚴格意義上來說更像是節點池,因為它已經處理了節點移除等操作。
- 無法將普通的TS對象放入cc.NodePool 進行管理。那么當我們需要對普通的TS對象進行管理的時候還是需要自己再寫一個對象池。
- 好處就是回收節點的時候不需要對節點做任何操作。
- 將節點添加到場景中時不需要考慮是否存在的問題,直接addChild就可以了,因為存在於對象池中的節點必定是從場景中移除的節點。
- 在使用的過程中頻繁移除和添加有性能問題。
針對以上問題,我分享一下自己使用對象池的經驗。
對象池的封裝
- 節點對象池
import { IPool } from "./IPool";
export default class CCNodePool implements IPool{
private pool: cc.NodePool;
private resItem: cc.Prefab;
private name: string = ''
/**
*
* @param prefab 預制體
* @param conut 初始化個數
*/
constructor(name: string, resItem: cc.Prefab, conut: number) {
this.name = name
this.pool = new cc.NodePool();
this.resItem = resItem;
for (let i = 0; i < conut; i++) {
let obj: cc.Node = this.getNode(); // 創建節點
this.pool.put(obj); // 通過 putInPool 接口放入對象池
}
}
getName() {
return this.name
}
get() {
let go: cc.Node = this.pool.size() > 0 ? this.pool.get() : this.getNode();
return go;
}
getNode() {
if(this.resItem){
return cc.instantiate(this.resItem);
}else{
console.error(' 預制體沒有賦值 ')
return null;
}
}
size() {
return this.pool.size();
}
put(go: cc.Node) {
this.pool.put(go);
}
clear() {
this.pool.clear();
}
}
- 非節點對象池
export default class TSObjectPool<T> {
private pool:any [] = []
private className:string;
constructor(className:string,type: { new(): T ;},count:number = 0){
this.className = className;
for (let index = 0; index < count; index++) {
this.pool.push(new type());
}
}
getClassName(){
return this.className;
}
get<T>(type: { new(): T ;} ): T {
let go = this.pool.length > 0 ? this.pool.shift() : null;
if(!go){
go = new type();
}
return go;
}
put(instance:T){
this.pool.push(instance);
}
clear(){
this.pool = [];
}
}
對象池管理器
不論是節點對象池還是非節點對象池。我都習慣通過一個管理器封裝起來使用。
這樣的好處就是集中管理,修改時也非常方便。
- 節點對象池管理器
import CCNodePool from "./CCNodePool";
import SelfPool from "./SelfPool";
export default class CCPoolManager {
private static ins: CCPoolManager;
static instance(): CCPoolManager {
if (!this.ins) {
this.ins = new CCPoolManager();
}
return this.ins;
}
//對象池表
private pools = {};
// 對象名稱 和給定 key的 映射表 這樣在回收對象的時候就不需要傳入key了。通過節點的name就可以找到key。
private nameMap = {};
init(key: string, resItem: cc.Prefab, count: number) {
if (!this.pools[key]) {
this.pools[key] = new SelfPool(new CCNodePool(key, resItem, count))
}
}
getPool(key: string) {
return this.pools[key].getPool();
}
get(key: string): cc.Node {
if (this.pools[key]) {
let go: cc.Node = this.pools[key].get();
if (!this.nameMap[go.name] && go.name != key) {
this.nameMap[go.name] = key;
}
return go;
}
return null;
}
put(go: cc.Node, nodePool: boolean = false) {
let key = this.nameMap[go.name];
if (!key) {
key = go.name;
}
if (!this.pools[key]) {
cc.warn(" not have name ", key, ' ,go.name ', go.name);
return;
}
this.pools[key].put(go, nodePool);
}
clear(name: string) {
if (this.pools[name]) {
this.pools[name].clear();
this.pools[name] = null;
}
}
clealAll() {
for (const key in this.pools) {
this.clear(key);
}
this.pools = {};
}
}
- 非節點對象池管理器
import TSObjectPool from "./TSObjectPool";
export default class TSPoolManager {
//對象池表
private pools = {}
private static ins: TSPoolManager;
static instance(): TSPoolManager {
if (!this.ins) {
this.ins = new TSPoolManager();
}
return this.ins;
}
init<T>(key: string, type: { new(): T; }, count: number = 1): void {
if (!this.pools[key]) {
this.pools[key] = new TSObjectPool(key, type, count);
}
}
/**
* 獲得被銷毀的對象
* @param key
*/
get<T>(key: string, type: { new(): T; }, count: number = 1): T {
if (!this.pools[key]) {
this.pools[key] = new TSObjectPool(key, type, count);
}
return this.pools[key].get(type);
}
put(key: string, obj) {
let pool = this.pools[key]
if (pool) {
pool.put(obj);
}
}
}
通用對象池
對象由外部創建。不用考慮是否為預制體創建的節點對象。
- 對象池
export default class ObjectPool<T>{
private buffList: T[] = []
private key: string;
constructor(key: string) {
this.key = key;
}
get(func: () => T) {
let item = this.buffList.length > 0 ? this.buffList.shift() : func();
return item;
}
put(obj: T) {
this.buffList.push(obj)
}
size() {
return this.buffList.length
}
destroy() {
this.buffList.length = 0;
}
}
- 對象池管理器
import ObjectPool from "./ObjectPool";
import TSMap from "../struct/TSMap";
export default class PoolManager {
private static ins: PoolManager
static instance() {
if (!this.ins) {
this.ins = new PoolManager();
}
return this.ins;
}
private map: TSMap<string, ObjectPool<any>> = new TSMap();
get(key: any, func: () => any) {
if (!this.map.has(key)) {
this.map.set(key, new ObjectPool(key))
}
return this.map.get(key).get(func)
}
put(key: any, obj: any) {
if (this.map.has(key)) {
this.map.get(key).put(obj)
} else {
}
}
size(key: string) {
if (this.map.has(key)) {
return this.map.get(key).size()
}
return 0;
}
destroy() {
this.map.clear();
}
}
針對Cocos對象池的優化
針對Cocos的這一性能問題,我利用裝飾模式,自定義了SelfPool類改變了獲取和回收時的操作。
import CCNodePool from "./CCNodePool";
import { IPool } from "./IPool";
/**
* 使用opacity方式隱藏對象
*/
export default class SelfPool implements IPool{
private list:cc.Node[] = []
private pool:CCNodePool;
constructor(pool:CCNodePool){
this.pool = pool;
}
get(){
let go:cc.Node = this.list.length > 0 ? this.list.shift() : this.pool.get();
go.opacity = 255;
return go;
}
getPool(){
return this.pool
}
size(){
return this.pool.size() + this.list.length;
}
/**
*
* @param go
* @param nodePool 是否放入NodePool中
*/
put(go:cc.Node,nodePool:boolean = false){
if(nodePool){
this.pool.put(go)
}else{
this.list.push(go);
go.stopAllActions();
go.opacity = 0;
}
}
clear(){
this.pool.clear();
this.list.length = 0;
}
}
在對象池初始化的時候做了這樣的處理
如果不想使用隱藏方式,可以去掉這一層封裝,接口都是一樣的。
對象池回收的偷懶方式
在回收對象時的一貫操作是put(key,obj)
如果obj肯定擁有name或者其他某個可以標識類別的屬性,可以將key與name做一個映射。通過name直接獲得key,從而找到對應的對象池,那么在put的時候也就不需要傳入key了。
結語
以上就是我在游戲開發中使用對象池的幾種的方式,分享出來,供大家參考使用。
歡迎掃碼關注公眾號《微笑游戲》,瀏覽更多內容。
歡迎掃碼關注公眾號《微笑游戲》,瀏覽更多內容。