Let’s made improvements to object pooling by the results of field tests.
Not so long ago, we looked at the pattern "pool of objects" and create it minimalistic and relatively straightforward implementation. We solved the assigned task, but the result was not very convenient to use. Transfer of additional name parameters makes setup not so straight. In this article, we will present an updated version of the pools without these shortcomings.
Singleton
The first thing which can help avoid the unnecessary pointer transfer is the use of pool object as a singleton. This implementation is no different from the one given in the corresponding article, except for the name, with the aim of protection against conflicts.
usingUnityEngine;publicclassKhtSingleton<T>:MonoBehaviourwhereT:KhtSingleton<T>{privatestaticT_instance;publicstaticTInstance{get=>_instance;}publicstaticboolIsInstantiated{get=>_instance!=null;}protectedvirtualvoidAwake(){if(_instance!=null){Debug.LogError("["+typeof(T)+"] '"+transform.name+"' trying to instantiate a second instance of singleton class previously created in '"+_instance.transform.name+"'");}else{_instance=(T)this;}}protectedvoidOnDestroy(){if(_instance==this){_instance=null;}}}
Object pool
This implementation of objects pools based on the GetInstanceID() method, which allows you to get a unique identifier for an object in runtime.
Pools discovery uses the prefab identifier. If there is no pool for the given prefab, a new one will be created so you can skip the initial assigning. New objects are also tied to the pool by their identifier.
If you specify the prefab for the pool in the editor, you can configure its prefilling.
In the absence of a singleton that provides work with pools, new objects will be created and deleted through Instantiate() and Destroy() methods.
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;publicclassKhtPool:KhtSingleton<KhtPool>{// There are no support of Dictionary type for Inspector, so let's use this
[Serializable]publicclassPrefabData{publicGameObjectprefab;publicintinitPoolSize=0;} [SerializeField]privateList<PrefabData>prefabDatas=null;// Bind the pool to the prefab id
privatereadonlyDictionary<int,Queue<GameObject>>_pools=newDictionary<int,Queue<GameObject>>();// Bind an object to a pool by its identifier
privatereadonlyDictionary<int,int>_objectToPoolDict=newDictionary<int,int>();privatenewvoidAwake(){// Set up singleton
base.Awake();// If necessary, prefill the pools of objects
foreach(varprefabDatainprefabDatas){_pools.Add(prefabData.prefab.GetInstanceID(),newQueue<GameObject>());for(inti=0;i<prefabData.initPoolSize;i++){GameObjectretObject=Instantiate(prefabData.prefab,Instance.transform,true);Instance._objectToPoolDict.Add(retObject.GetInstanceID(),prefabData.prefab.GetInstanceID());Instance._pools[prefabData.prefab.GetInstanceID()].Enqueue(retObject);retObject.SetActive(false);}}prefabDatas=null;}// Get the object from the pool
publicstaticGameObjectGetObject(GameObjectprefab){// If there is no singleton, create a new object
if(!Instance){returnInstantiate(prefab);}// Unique identifier of the prefab used to bind to the pool
intprefabId=prefab.GetInstanceID();// If the pool for the prefab does not exist, then create a new
if(!Instance._pools.ContainsKey(prefabId)){Instance._pools.Add(prefabId,newQueue<GameObject>());}// If there is an object in the pool, return it
if(Instance._pools[prefabId].Count>0){returnInstance._pools[prefabId].Dequeue();}// In case of shortage of objects, create a new
GameObjectretObject=Instantiate(prefab);// Add the binding of the object to the pool by its identifier
Instance._objectToPoolDict.Add(retObject.GetInstanceID(),prefabId);returnretObject;}// Return the object to the pool
publicstaticvoidReturnObject(GameObjectpoolObject){// In the absence of a singleton, we destroy the object
if(!Instance){Destroy(poolObject);return;}// Object identifier for pool definition
intobjectId=poolObject.GetInstanceID();// If there is no binding of the object to the pool, we destroy it
if(!Instance._objectToPoolDict.TryGetValue(objectId,outintpoolId)){Destroy(poolObject);return;}// Return the object to the pool
Instance._pools[poolId].Enqueue(poolObject);poolObject.transform.SetParent(Instance.transform);poolObject.SetActive(false);}}
usingUnityEngine;publicclassFire:MonoBehaviour{// Non-Pooling Information
[SerializeField]privateTransformleftCannon=null; [SerializeField]privateTransformrightCannon=null; [SerializeField]privateUnevirseHandleruniverse=null; [SerializeField]privateTransformspace=null;// Prefab to get an object from the pool
[SerializeField]privateGameObjectprojectilePrefab=null;privatebool_rightCannon=false;voidUpdate(){if(Input.GetMouseButtonDown(0)){// Get the object from the pool
GameObjectpr=KhtPool.GetObject(projectilePrefab);// Objects from the pool need proper cleanup and initialization
pr.transform.SetParent(space);pr.transform.SetPositionAndRotation(_rightCannon?rightCannon.position:leftCannon.position,_rightCannon?rightCannon.rotation:leftCannon.rotation);Projectileprpr=pr.GetComponent<Projectile>();prpr.MoveVector=universe.LookVector.normalized;pr.SetActive(true);_rightCannon=!_rightCannon;}}}
usingUnityEngine;publicclassProjectile:MonoBehaviour{ [SerializeField]privatefloatspeed=0;privateVector2_moveVector=Vector2.zero;publicVector2MoveVector{set=>_moveVector=value;}voidUpdate(){transform.Translate(_moveVector.x*Time.deltaTime*speed,_moveVector.y*Time.deltaTime*speed,0,Space.World);}voidOnBecameInvisible(){// After the object leaves the screen, return it to the pool
KhtPool.ReturnObject(gameObject);}}
Result
New objects created on-demand:
Conclusion
The updated version greatly simplifies our previous implementation. However, it expands the possibilities for adding additional functionality, such as a prohibition of new pool creation or a limitation on their expanding. But these features are used much less frequently than prefilling, so let’s leave them for the future. See you next time! =)