- Overview
- Core Concepts
- SimplePool<T>
- Usage Patterns
- Performance Considerations
- Common Issues and Solutions
- Best Practices
- Examples
- Summary
The CherryFramework SimplePool<T> provides a lightweight, type-safe object pooling system for Unity components. Object pooling reuses objects instead of creating and destroying them repeatedly, which is essential for performance-critical scenarios like spawning bullets, enemies, or visual effects.
| Without Pooling | With Pooling |
|---|---|
Instantiate() and Destroy() called constantly |
Objects reused from pool |
| Frequent garbage collection | Minimal GC overhead |
| Performance spikes during instantiation and GC cleanup | Consistent performance |
| No object limit | Controlled pool size |
┌─────────────────────────────────────────────────────────────┐
│ SimplePool<T> │
├─────────────────────────────────────────────────────────────┤
│ - Dictionary<T, List<T>> _pool │
│ │
│ + Get(T sample) │
│ + Get(T sample, Vector3 position, Quaternion rotation) │
│ + Get(T sample, position, rotation, Transform parent) │
│ + List<T> ActiveObjects(T sample) │
│ + void Clear() │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Pool A │ │ Pool B │ │ Pool C │
│ (BulletPrefab)│ │ (EnemyPrefab) │ │ (EffectPrefab)│
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ [Inactive] │ │ [Inactive] │ │ [Inactive] │
│ [Inactive] │ │ [Active] │ │ [Inactive] │
│ [Active] │ │ [Inactive] │ │ [Active] │
│ [Active] │ │ [Active] │ │ [Inactive] │
└───────────────┘ └───────────────┘ └───────────────┘
- First Request: Pool empty → new object instantiated
- Subsequent Requests: Inactive object reactivated and returned
- Object Return: Object deactivated → automatically available for reuse
- Per-Sample Pools: Each prefab gets its own pool
- Null Cleanup: Destroyed objects removed on next access
| Component | Purpose |
|---|---|
SimplePool<T> |
Generic pool class for any Component type |
Get() |
Retrieves an object from pool (or creates new) |
ActiveObjects() |
Returns all currently active objects |
Clear() |
Destroys all pooled objects |
Namespace: CherryFramework.SimplePool
public class SimplePool<T> where T : Component
{
// Constructor
public SimplePool();
// Methods
public T Get(T sample, Vector3 position, Quaternion rotation, Transform parent = null);
public T Get(T sample);
public List<T> ActiveObjects(T sample);
public void Clear();
}public T Get(T sample, Vector3 position, Quaternion rotation, Transform parent = null)Retrieves an object from pool, positions it, and returns it.
Example:
var bullet = bulletPool.Get(bulletPrefab, firePoint.position, firePoint.rotation);public T Get(T sample)Retrieves an object without changing its transform.
Example:
var effect = effectPool.Get(effectPrefab);
effect.transform.position = spawnPoint;public List<T> ActiveObjects(T sample)Returns all active objects for a sample type.
Example:
int activeCount = enemyPool.ActiveObjects(enemyPrefab).Count;
Debug.Log($"Active enemies: {activeCount}");public void Clear()Destroys all pooled objects.
Example:
private void OnDestroy()
{
bulletPool.Clear();
}public class Weapon : MonoBehaviour
{
private SimplePool<Bullet> _bulletPool;
[SerializeField] private Bullet _bulletPrefab;
[SerializeField] private Transform _firePoint;
private void Awake()
{
_bulletPool = new SimplePool<Bullet>();
}
public void Shoot()
{
var bullet = _bulletPool.Get(_bulletPrefab, _firePoint.position, _firePoint.rotation);
bullet.Initialize();
}
}public class Bullet : MonoBehaviour
{
private float _speed = 20f;
private void OnEnable()
{
// Auto-return after 3 seconds
Invoke(nameof(ReturnToPool), 3f);
}
public void Initialize()
{
gameObject.SetActive(true);
// Setup bullet
}
private void Update()
{
transform.Translate(Vector3.forward * _speed * Time.deltaTime);
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Enemy"))
{
other.GetComponent<Enemy>().Die();
ReturnToPool();
}
}
private void ReturnToPool()
{
gameObject.SetActive(false); // Returns to pool
}
}public class EnemySpawner : MonoBehaviour
{
private SimplePool<Enemy> _enemyPool;
[SerializeField] private Enemy _enemyPrefab;
[SerializeField] private Transform[] _spawnPoints;
private void Awake()
{
_enemyPool = new SimplePool<Enemy>();
PrewarmPool(10);
}
private void PrewarmPool(int count)
{
for (int i = 0; i < count; i++)
{
var enemy = _enemyPool.Get(_enemyPrefab);
enemy.gameObject.SetActive(false);
}
}
public void SpawnEnemy()
{
var point = _spawnPoints[Random.Range(0, _spawnPoints.Length)];
var enemy = _enemyPool.Get(_enemyPrefab, point.position, point.rotation);
enemy.Initialize();
}
public int ActiveEnemyCount => _enemyPool.ActiveObjects(_enemyPrefab).Count;
}public class EffectManager : MonoBehaviour
{
private SimplePool<ParticleSystem> _explosionPool;
[SerializeField] private ParticleSystem _explosionPrefab;
private void Awake()
{
_explosionPool = new SimplePool<ParticleSystem>();
}
public void SpawnExplosion(Vector3 position)
{
var explosion = _explosionPool.Get(_explosionPrefab, position, Quaternion.identity);
explosion.gameObject.SetActive(true);
explosion.Play();
StartCoroutine(DeactivateAfterSeconds(explosion, explosion.main.duration));
}
private IEnumerator DeactivateAfterSeconds(ParticleSystem ps, float seconds)
{
yield return new WaitForSeconds(seconds);
ps.gameObject.SetActive(false);
}
}public class PoolManager : MonoBehaviour
{
private SimplePool<Bullet> _bulletPool;
private SimplePool<Enemy> _enemyPool;
private SimplePool<ParticleSystem> _effectPool;
[SerializeField] private Bullet _bulletPrefab;
[SerializeField] private Enemy _enemyPrefab;
[SerializeField] private ParticleSystem _effectPrefab;
private void Awake()
{
_bulletPool = new SimplePool<Bullet>();
_enemyPool = new SimplePool<Enemy>();
_effectPool = new SimplePool<ParticleSystem>();
PrewarmPools();
}
private void PrewarmPools()
{
// Prewarm bullets
for (int i = 0; i < 20; i++)
{
var bullet = _bulletPool.Get(_bulletPrefab);
bullet.gameObject.SetActive(false);
}
// Prewarm enemies
for (int i = 0; i < 10; i++)
{
var enemy = _enemyPool.Get(_enemyPrefab);
enemy.gameObject.SetActive(false);
}
// Prewarm effects
for (int i = 0; i < 5; i++)
{
var effect = _effectPool.Get(_effectPrefab);
effect.gameObject.SetActive(false);
}
}
}private void PrewarmPool(int count)
{
for (int i = 0; i < count; i++)
{
var obj = _pool.Get(_prefab);
obj.gameObject.SetActive(false); // Return to pool
}
Debug.Log($"Prewarmed pool with {count} objects");
}private void Update()
{
// Check pool usage every few seconds
if (Time.frameCount % 300 == 0)
{
int active = _pool.ActiveObjects(_prefab).Count;
Debug.Log($"Active objects: {active}");
if (active > _warningThreshold)
{
Debug.LogWarning($"High pool usage: {active}");
}
}
}private void OnDestroy()
{
_bulletPool.Clear();
_enemyPool.Clear();
_effectPool.Clear();
}
// Or when changing scenes
private void OnLevelWasLoaded()
{
_bulletPool.Clear();
_enemyPool.Clear();
_effectPool.Clear();
}public class LimitedPool<T> : SimplePool<T> where T : Component
{
private int _maxSize;
private int _totalCreated;
public LimitedPool(int maxSize)
{
_maxSize = maxSize;
}
public new T Get(T sample, Vector3 position, Quaternion rotation, Transform parent = null)
{
if (_totalCreated >= _maxSize)
{
// Reuse oldest active object
var active = ActiveObjects(sample);
if (active.Count > 0)
{
active[0].gameObject.SetActive(false);
}
}
var obj = base.Get(sample, position, rotation, parent);
if (!_pool.ContainsKey(sample) || !_pool[sample].Contains(obj))
{
_totalCreated++;
}
return obj;
}
}Symptoms: Pool creates new objects endlessly, memory usage grows
Solution: Always deactivate when done
private void ReturnToPool()
{
gameObject.SetActive(false); // Returns to pool
}
private void OnDisable()
{
CancelInvoke(); // Clean up any pending calls
StopAllCoroutines();
}Symptoms: Reused objects retain old health, position, or visual state
Solution: Reset in OnEnable or with initialize method
private void OnEnable()
{
_health = 100;
_speed = 5f;
transform.localScale = Vector3.one;
GetComponent<Renderer>().material.color = Color.white;
}
public void Initialize(Vector3 position, int health)
{
transform.position = position;
_health = health;
}Symptoms: Get() always creates new objects even when pool has inactive ones
Solution: Always use the same reference
// GOOD
private SimplePool<Bullet> _pool;
[SerializeField] private Bullet _prefab; // Inspector reference
private void Shoot()
{
var bullet = _pool.Get(_prefab); // Use same reference
}
// BAD
var bullet = _pool.Get(Instantiate(_prefab)); // Different reference!Symptoms: Pool size increases indefinitely, memory usage high
Solution: Implement size monitoring and limits
public class MonitoredPool<T> : SimplePool<T> where T : Component
{
public int PeakActive { get; private set; }
public new T Get(T sample, Vector3 position, Quaternion rotation, Transform parent = null)
{
var obj = base.Get(sample, position, rotation, parent);
int active = ActiveObjects(sample).Count;
PeakActive = Mathf.Max(PeakActive, active);
return obj;
}
public void LogStats(T sample)
{
int active = ActiveObjects(sample).Count;
int total = _pool.ContainsKey(sample) ? _pool[sample].Count : 0;
Debug.Log($"Pool stats - Active: {active}, Total: {total}, Peak: {PeakActive}");
}
}Symptoms: Objects remain after scene change
Solution: Clear pools when no longer needed
public class SceneCleanup : BehaviourBase
{
[Inject] private PoolManager _poolManager;
private void OnEnable()
{
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
private void OnDisable()
{
SceneManager.sceneUnloaded -= OnSceneUnloaded;
}
private void OnSceneUnloaded(Scene scene)
{
_poolManager.ClearAllPools();
}
}private void OnTriggerEnter(Collider other)
{
// Handle collision
gameObject.SetActive(false); // Return to pool
}
private void Update()
{
if (transform.position.y < -10)
{
gameObject.SetActive(false); // Return when out of bounds
}
}private void OnEnable()
{
_health = 100;
_ammo = 30;
_trail?.Clear();
_rigidbody.velocity = Vector3.zero;
_rigidbody.angularVelocity = Vector3.zero;
}private void Start()
{
// Prewarm pools at start to avoid runtime spikes
PrewarmPool(_bulletPool, _bulletPrefab, 20);
PrewarmPool(_enemyPool, _enemyPrefab, 10);
PrewarmPool(_effectPool, _effectPrefab, 5);
}
private void PrewarmPool<T>(SimplePool<T> pool, T prefab, int count) where T : Component
{
for (int i = 0; i < count; i++)
{
var obj = pool.Get(prefab);
obj.gameObject.SetActive(false);
}
}private void OnDestroy()
{
_bulletPool.Clear();
_enemyPool.Clear();
_effectPool.Clear();
}
// Or in a persistent manager
DontDestroyOnLoad(gameObject);// Bullet.cs
public class Bullet : MonoBehaviour
{
[SerializeField] private float _speed = 20f;
[SerializeField] private float _lifetime = 3f;
[SerializeField] private int _damage = 10;
private void OnEnable()
{
Invoke(nameof(ReturnToPool), _lifetime);
}
public void Initialize(Vector3 direction)
{
gameObject.SetActive(true);
GetComponent<Rigidbody>().velocity = direction * _speed;
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Enemy"))
{
other.GetComponent<Enemy>().TakeDamage(_damage);
ReturnToPool();
}
}
private void ReturnToPool()
{
gameObject.SetActive(false);
}
private void OnDisable()
{
CancelInvoke();
}
}
// Weapon.cs
public class Weapon : MonoBehaviour
{
[SerializeField] private Transform _firePoint;
[SerializeField] private Bullet _bulletPrefab;
[SerializeField] private float _fireRate = 0.1f;
private SimplePool<Bullet> _bulletPool;
private float _nextFireTime;
private void Awake()
{
_bulletPool = new SimplePool<Bullet>();
PrewarmPool(20);
}
private void PrewarmPool(int count)
{
for (int i = 0; i < count; i++)
{
var bullet = _bulletPool.Get(_bulletPrefab);
bullet.gameObject.SetActive(false);
}
}
public void Fire()
{
if (Time.time < _nextFireTime) return;
_nextFireTime = Time.time + _fireRate;
var bullet = _bulletPool.Get(_bulletPrefab, _firePoint.position, _firePoint.rotation);
bullet.Initialize(_firePoint.forward);
}
}// Enemy.cs
public class Enemy : MonoBehaviour
{
[SerializeField] private int _health = 3;
[SerializeField] private float _speed = 2f;
[SerializeField] private int _scoreValue = 100;
private Transform _player;
private void OnEnable()
{
_player = GameObject.FindGameObjectWithTag("Player")?.transform;
_health = 3;
}
private void Update()
{
if (_player == null) return;
var direction = (_player.position - transform.position).normalized;
transform.Translate(direction * _speed * Time.deltaTime, Space.World);
}
public void TakeDamage(int damage)
{
_health -= damage;
if (_health <= 0)
{
Die();
}
}
private void Die()
{
// Add score
ScoreManager.Instance.AddScore(_scoreValue);
// Play death effect
FindObjectOfType<EffectManager>()?.SpawnExplosion(transform.position);
gameObject.SetActive(false); // Return to pool
}
}
// EnemySpawner.cs
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private Enemy _enemyPrefab;
[SerializeField] private Transform[] _spawnPoints;
[SerializeField] private float _spawnInterval = 2f;
[SerializeField] private int _maxEnemies = 20;
private SimplePool<Enemy> _enemyPool;
private float _spawnTimer;
private void Awake()
{
_enemyPool = new SimplePool<Enemy>();
PrewarmPool(_maxEnemies);
}
private void PrewarmPool(int count)
{
for (int i = 0; i < count; i++)
{
var enemy = _enemyPool.Get(_enemyPrefab);
enemy.gameObject.SetActive(false);
}
}
private void Update()
{
_spawnTimer += Time.deltaTime;
if (_spawnTimer >= _spawnInterval)
{
_spawnTimer = 0f;
if (_enemyPool.ActiveObjects(_enemyPrefab).Count < _maxEnemies)
{
SpawnEnemy();
}
}
}
private void SpawnEnemy()
{
var point = _spawnPoints[Random.Range(0, _spawnPoints.Length)];
var enemy = _enemyPool.Get(_enemyPrefab, point.position, point.rotation);
enemy.gameObject.SetActive(true);
// Enemy automatically starts due to OnEnable
}
public int ActiveEnemyCount => _enemyPool.ActiveObjects(_enemyPrefab).Count;
}// EffectManager.cs
public class EffectManager : MonoBehaviour
{
[System.Serializable]
public class EffectConfig
{
public string name;
public ParticleSystem prefab;
public int prewarmCount = 5;
public float duration = -1; // -1 = use particle system duration
}
[SerializeField] private List<EffectConfig> _effects;
[SerializeField] private Transform _effectParent;
private Dictionary<string, SimplePool<ParticleSystem>> _pools = new();
private void Awake()
{
// Create parent if not assigned
if (_effectParent == null)
{
var parentObj = new GameObject("EffectPool");
_effectParent = parentObj.transform;
_effectParent.SetParent(transform);
}
// Initialize pools
foreach (var effect in _effects)
{
var pool = new SimplePool<ParticleSystem>();
// Prewarm
for (int i = 0; i < effect.prewarmCount; i++)
{
var instance = pool.Get(effect.prefab);
instance.transform.SetParent(_effectParent);
instance.gameObject.SetActive(false);
}
_pools[effect.name] = pool;
}
}
public void PlayEffect(string name, Vector3 position)
{
PlayEffect(name, position, Quaternion.identity);
}
public void PlayEffect(string name, Vector3 position, Quaternion rotation)
{
if (!_pools.TryGetValue(name, out var pool)) return;
var config = _effects.Find(e => e.name == name);
if (config == null) return;
var effect = pool.Get(config.prefab, position, rotation, _effectParent);
effect.gameObject.SetActive(true);
effect.Play();
float duration = config.duration > 0 ? config.duration : effect.main.duration;
StartCoroutine(DeactivateAfterSeconds(effect, duration));
}
private IEnumerator DeactivateAfterSeconds(ParticleSystem ps, float seconds)
{
yield return new WaitForSeconds(seconds);
ps.Stop();
ps.gameObject.SetActive(false);
}
private void OnDestroy()
{
foreach (var pool in _pools.Values)
{
pool.Clear();
}
_pools.Clear();
}
}
// Usage
public class GameEvents : MonoBehaviour
{
[Inject] private EffectManager _effects;
public void OnPlayerShoot()
{
_effects.PlayEffect("MuzzleFlash", firePoint.position);
}
public void OnEnemyDeath(Vector3 position)
{
_effects.PlayEffect("Explosion", position);
_effects.PlayEffect("BloodSplat", position);
}
public void OnPowerupCollected(Vector3 position)
{
_effects.PlayEffect("PowerupGlow", position);
}
}┌─────────────────────────────────────────────────────────────┐
│ SimplePool<T> │
├─────────────────────────────────────────────────────────────┤
│ - Dictionary<T, List<T>> _pool │
│ │
│ + Get(T sample) ←───┐ │
│ + Get(T sample, position, rotation) │ │
│ + List<T> ActiveObjects(T sample) │ Uses │
│ + void Clear() ←───┘ │
└─────────────────────────────────────────────────────────────┘
│ │
│ One pool per │ Tracks
│ prefab type │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ List of objects│ │ Active objects │
│ for Prefab A │ │ for Prefab A │
│ [obj1, obj2...]│ │ [obj3, obj5] │
└─────────────────┘ └─────────────────┘
| Method | Description | Example |
|---|---|---|
Get(T sample) |
Get object from pool | pool.Get(bulletPrefab) |
Get(T sample, pos, rot) |
Get and position object | pool.Get(prefab, pos, rot) |
Get(T sample, pos, rot, parent) |
Get, position, and parent | pool.Get(prefab, pos, rot, parent) |
ActiveObjects(T sample) |
Get all active objects | pool.ActiveObjects(prefab).Count |
Clear() |
Destroy all pooled objects | pool.Clear() |
| # | Key Point | Why It Matters |
|---|---|---|
| 1 | Always deactivate objects when done | Returns them to pool for reuse (gameObject.SetActive(false)) |
| 2 | Always reactivate objects when got from pool | Manually handle object activity when needed |
| 3 | Reset state in OnEnable | Ensures fresh state when object is reused |
| 4 | Clear pools when changing scenes | Prevents memory leaks |
| 8 | Use the same sample reference | Different references create different pools |
| Use Pooling | Don't Pool |
|---|---|
| Bullets / Projectiles | Boss enemies (rare) |
| Enemies in waves | Unique quest items |
| Particle effects | Level geometry |
| UI elements that appear frequently | Objects created once |
| Anything created/destroyed frequently | Objects with complex setup |
SimplePool provides an efficient, easy-to-use object pooling solution that integrates seamlessly with the CherryFramework, helping you write high-performance Unity games with minimal garbage collection overhead.