Реализовать пул объектов (Object Pool)
Условие
Реализуйте систему пула объектов в Unity для переиспользования игровых объектов вместо их постоянного создания и уничтожения.
Требования
- Создайте класс ObjectPool<T> где T : MonoBehaviour
- Реализуйте методы:
- Get() - получить объект из пула (или создать новый если пул пуст)
- Return(T obj) - вернуть объект в пул
- Prewarm(int count) - создать заданное количество объектов заранее
- Объекты должны деактивироваться при возврате в пул
- Пул должен быть потокобезопасным
Пример использования
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)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Что такое 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; // или логировать ошибку
// ...
}
Эта реализация обеспечивает высокую производительность, безопасность потоков и удобство использования.