There’s a couple of limitations I can think of:
- Unity’s object creation and destruction is far less evil than people make it out to be. It could just be that it’s faster to instantiate an object and destroy it than to retrieve it from the pool and initialize it. Especially when the amount of objects and frequency of use is low.
- What is your object pool code? Maybe you have an inefficient implementation.
- The bottleneck is typically object-initialization/resetting, e.g., SetActive may have a considerable overhead depending on how many children and components the GameObject has.
Show us your code, if you’d like some feedback. Other than that, I can share a few object pool implementations:
using System;
using System.Collections.Generic;
using UnityEngine.Assertions;
public class GenericPool<T>
{
private readonly Stack<T> m_Pool;
private readonly HashSet<T> m_Active;
private readonly Func<T> m_FuncCreate;
private readonly Action<T> m_ActionGet;
private readonly Action<T> m_ActionReturn;
public GenericPool(Func<T> createFunction, Action<T> onGet, Action<T> onReturn, int capacity = 0)
{
Assert.IsNotNull(createFunction);
m_Pool = new Stack<T>(capacity);
m_Active = new HashSet<T>();
m_FuncCreate = createFunction;
m_ActionGet = onGet;
m_ActionReturn = onReturn;
}
public void Prewarm(int capacity)
{
for (int i = 0; i < capacity; i++)
Return(m_FuncCreate.Invoke());
}
public T Get()
{
T element;
if (m_Pool.Count == 0)
element = m_FuncCreate.Invoke();
else
element = m_Pool.Pop();
if (m_ActionGet != null)
m_ActionGet.Invoke(element);
m_Active.Add(element);
return element;
}
public void Return(T element)
{
#if DEBUG
if (m_Pool.Contains(element))
throw new InvalidOperationException("Element '" + element + "' already in pool.");
#endif
if (m_ActionReturn != null)
m_ActionReturn.Invoke(element);
m_Active.Remove(element);
m_Pool.Push(element);
}
public void GatherAll()
{
foreach (T element in m_Active)
Return(element);
}
public void ActionOnAll(Action<T> action)
{
foreach (T element in m_Active)
action.Invoke(element);
}
}
using UnityEngine;
using System.Collections.Generic;
namespace Demo
{
public class ObjectPool_A : EnemyFactory
{
public int startCount = 21;
Stack<GameObject> instances = new Stack<GameObject>();
void Start()
{
for (int i = 0; i < startCount; i++)
GrowPool();
}
void GrowPool()
{
GameObject instance = Instantiate(prefab);
instance.SetActive(false);
instances.Push(instance);
}
public override GameObject Spawn(Transform spawnPoint)
{
if (instances.Count < 1)
GrowPool();
GameObject instance = instances.Pop();
instance.transform.SetPositionAndRotation(spawnPoint.position, spawnPoint.rotation);
instance.SetActive(true);
return instance;
}
public override void Despawn(GameObject instance)
{
instance.SetActive(false);
instances.Push(instance);
}
}
}
using UnityEngine;
using System.Collections.Generic;
namespace Demo
{
public class ObjectPool_B : EnemyFactory
{
public int startCount = 21;
List<GameObject> instances = new List<GameObject>();
List<bool> inUse = new List<bool>();
void Start()
{
for (int i = 0; i < startCount; i++)
GrowPool();
}
void GrowPool()
{
GameObject instance = Instantiate(prefab);
instance.SetActive(false);
instances.Add(instance);
inUse.Add(false);
}
public override GameObject Spawn(Transform spawnPoint)
{
for (int i = 0; i < inUse.Count; i++)
{
if (inUse *== false)*
_ inUse = true;_
_ instances*.transform.SetPositionAndRotation(spawnPoint.position, spawnPoint.rotation);
instances.SetActive(true);
return instances;
}
}
return null;
}*_
* public override void Despawn(GameObject instance)*
* {*
* inUse[instances.IndexOf(instance)] = false;*
* instance.SetActive(false);*
* }*
* }*
}
----------
using UnityEngine;
using System.Collections.Generic;
namespace Demo
{
* public class ObjectPool_C : MonoBehaviour*
* {*
* // Each prefab gets its own pool.*
* static Dictionary<GameObject, ObjectPool_C> m_Pools;*
* GameObject m_Prefab;
List m_Instances = new List();*
* static ObjectPool_C()
_ {_
m_Pools = new Dictionary<GameObject, ObjectPool_C>();
_ }*_
* public static ObjectPool_C GetPool(GameObject prefab)
_ {_
ObjectPool_C pool;
if (m_Pools.TryGetValue(prefab, out pool))
return m_Pools[prefab];
_ else*
* return CreateNewPool(prefab);
}*_
* public static ObjectPool_C CreateNewPool(GameObject prefab, int size = 0)
_ {
GameObject go = new GameObject(prefab.name + " (Pool)");_
ObjectPool_C pool = go.AddComponent<ObjectPool_C>();
pool.m_Prefab = prefab;*
* for (int i = 0; i < size; i++)*
* pool.GrowPool();*
* return pool;*
* }*
* public static GameObject GetInstance(GameObject prefab)*
* {*
* ObjectPool_C pool = ObjectPool_C.GetPool(prefab);
_ return pool.GetInstance();
}*_
* public GameObject GetInstance()*
* {*
* GameObject instance = null;*
* int count = m_Instances.Count;
_ for (int i = 0; i < count; i++)
{_
if (m_Instances.activeSelf == false)
_ {_
instance = m_Instances;
_ instance.SetActive(true);
return instance;
}
}*_
* // If we failed to find an inactive instance.*
* instance = GrowPool();*
* instance.SetActive(true);*
* return instance;*
* }*
* GameObject GrowPool()*
* {*
* GameObject go = Instantiate(m_Prefab, transform);
_ go.SetActive(false);_
m_Instances.Add(go);
_ return go;
}*_
* void OnDestroy()*
* {*
* if (m_Pools != null)
m_Pools = null;
_ }
}
}*_
The EnemyFactory was just an abstract MonoBehaviour base class, which shows that you can switch the ObjectPool implementation for testing purposes, e.g., via an inspector reference. All implementations have slightly different performance characteristics and may need to be adjusted for specific use-cases.