← Назад к вопросам

Реализовать пул объектов (Object Pool)

2.2 Middle🔥 231 комментариев
#C# и ООП#Асинхронность и многопоточность#Оптимизация#Паттерны проектирования#Управление памятью

Условие

Реализуйте систему пула объектов в Unity для переиспользования игровых объектов вместо их постоянного создания и уничтожения.

Требования

  1. Создайте класс ObjectPool<T> где T : MonoBehaviour
  2. Реализуйте методы:
    • Get() - получить объект из пула (или создать новый если пул пуст)
    • Return(T obj) - вернуть объект в пул
    • Prewarm(int count) - создать заданное количество объектов заранее
  3. Объекты должны деактивироваться при возврате в пул
  4. Пул должен быть потокобезопасным

Пример использования

public class BulletSpawner : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;
    private ObjectPool<Bullet> bulletPool;
    
    void Start()
    {
        bulletPool = new ObjectPool<Bullet>(bulletPrefab, transform);
        bulletPool.Prewarm(20);
    }
    
    void SpawnBullet()
    {
        var bullet = bulletPool.Get();
        bullet.Initialize(/* ... */);
    }
}

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Что такое Object Pool?

Объектный пул — это паттерн проектирования, который переиспользует объекты вместо их постоянного создания и уничтожения. Это критически важно в Unity для оптимизации производительности, так как операции Instantiate() и Destroy() — дорогостоящие и могут вызвать сборку мусора (GC spikes). Пул особенно полезен для пуль, врагов, эффектов и других часто создаваемых объектов.

Реализация ObjectPool<T>

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool<T> where T : MonoBehaviour
{
    private readonly T prefab;
    private readonly Transform parent;
    private readonly Queue<T> availableObjects = new Queue<T>();
    private readonly HashSet<T> allObjects = new HashSet<T>();
    private readonly object lockObject = new object();
    
    public ObjectPool(T prefab, Transform parent = null)
    {
        this.prefab = prefab;
        this.parent = parent;
    }
    
    /// <summary>Предварительно создаёт объекты в пуле</summary>
    public void Prewarm(int count)
    {
        for (int i = 0; i < count; i++)
        {
            CreateNewObject();
        }
    }
    
    /// <summary>Получить объект из пула или создать новый</summary>
    public T Get()
    {
        lock (lockObject)
        {
            T obj;
            
            if (availableObjects.Count > 0)
            {
                obj = availableObjects.Dequeue();
            }
            else
            {
                obj = CreateNewObject();
            }
            
            obj.gameObject.SetActive(true);
            return obj;
        }
    }
    
    /// <summary>Вернуть объект в пул</summary>
    public void Return(T obj)
    {
        if (obj == null)
        {
            Debug.LogWarning("Попытка вернуть null объект в пул");
            return;
        }
        
        lock (lockObject)
        {
            if (!allObjects.Contains(obj))
            {
                Debug.LogWarning($"Объект {obj.name} не принадлежит этому пулу");
                Object.Destroy(obj.gameObject);
                return;
            }
            
            obj.gameObject.SetActive(false);
            availableObjects.Enqueue(obj);
        }
    }
    
    /// <summary>Получить объект в пул с задержкой</summary>
    public void ReturnDelayed(T obj, float delay)
    {
        obj.StartCoroutine(ReturnDelayedCoroutine(obj, delay));
    }
    
    /// <summary>Очистить весь пул</summary>
    public void Clear()
    {
        lock (lockObject)
        {
            foreach (var obj in allObjects)
            {
                Object.Destroy(obj.gameObject);
            }
            
            allObjects.Clear();
            availableObjects.Clear();
        }
    }
    
    /// <summary>Получить количество доступных объектов</summary>
    public int AvailableCount
    {
        get
        {
            lock (lockObject)
            {
                return availableObjects.Count;
            }
        }
    }
    
    /// <summary>Получить общее количество объектов в пуле</summary>
    public int TotalCount
    {
        get
        {
            lock (lockObject)
            {
                return allObjects.Count;
            }
        }
    }
    
    private T CreateNewObject()
    {
        T obj = Object.Instantiate(prefab, parent);
        obj.gameObject.name = prefab.gameObject.name;
        obj.gameObject.SetActive(false);
        allObjects.Add(obj);
        availableObjects.Enqueue(obj);
        
        return obj;
    }
    
    private System.Collections.IEnumerator ReturnDelayedCoroutine(T obj, float delay)
    {
        yield return new WaitForSeconds(delay);
        Return(obj);
    }
}

Пример использования

public class BulletSpawner : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;
    [SerializeField] private int prewarmCount = 20;
    
    private ObjectPool<Bullet> bulletPool;
    
    void Start()
    {
        // Создаём пул и предварительно создаём объекты
        bulletPool = new ObjectPool<Bullet>(bulletPrefab, transform);
        bulletPool.Prewarm(prewarmCount);
    }
    
    void SpawnBullet(Vector3 position, Vector3 direction)
    {
        var bullet = bulletPool.Get();
        bullet.transform.position = position;
        bullet.Initialize(direction);
    }
    
    void OnDestroy()
    {
        bulletPool?.Clear();
    }
}

public class Bullet : MonoBehaviour
{
    private ObjectPool<Bullet> pool;
    private Vector3 direction;
    [SerializeField] private float lifetime = 5f;
    
    public void Initialize(Vector3 dir, ObjectPool<Bullet> bulletPool)
    {
        direction = dir;
        pool = bulletPool;
        // Вернуть пулю в пул через lifetime
        pool.ReturnDelayed(this, lifetime);
    }
    
    void Update()
    {
        transform.position += direction * Time.deltaTime * 10f;
    }
}

Ключевые особенности реализации

1. Потокобезопасность — используется lock (lockObject) для защиты от race conditions при многопоточном доступе.

2. Generic типизацияObjectPool<T> where T : MonoBehaviour позволяет использовать пул с любыми MonoBehaviour компонентами.

3. Queue vs List — Queue обеспечивает FIFO порядок, что справедливо распределяет объекты и избегает их долгого "сна" в памяти.

4. HashSet для отслеживания — быстрая проверка принадлежности объекта пулу за O(1).

5. Деактивация вместо уничтожения — SetActive(false) значительно быстрее Destroy(), полностью избегая GC spikes.

6. Методы для отладки — AvailableCount и TotalCount помогают мониторить состояние пула во время разработки.

Оптимизация памяти

DynamicObject Pool автоматически увеличивается при необходимости. Если нужно фиксировать максимальный размер, добавьте проверку:

private readonly int maxPoolSize = 100;

private T CreateNewObject()
{
    if (allObjects.Count >= maxPoolSize)
        return null; // или логировать ошибку
    // ...
}

Эта реализация обеспечивает высокую производительность, безопасность потоков и удобство использования.

Реализовать пул объектов (Object Pool) | PrepBro