289 lines
12 KiB
C#
289 lines
12 KiB
C#
#nullable enable
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
namespace UVC.Pool
|
|
{
|
|
/// <summary>
|
|
/// MonoBehaviour를 상속받는 컴포넌트의 오브젝트 풀링을 관리하는 제네릭 클래스입니다.
|
|
/// 오브젝트 풀링은 오브젝트를 반복적으로 생성하고 파괴하는 데 드는 비용을 줄여 성능을 향상시키는 데 사용됩니다.
|
|
/// 사용하지 않는 오브젝트를 비활성화하여 보관했다가 필요할 때 재사용합니다.
|
|
///
|
|
/// 사용 예:
|
|
/// <code>
|
|
/// public class BulletManager : MonoBehaviour
|
|
/// {
|
|
/// public GameObject bulletPrefab;
|
|
/// public Transform bulletContainer;
|
|
/// private GameObjectPool<Bulle> _bulletPool;
|
|
///
|
|
/// void Start()
|
|
/// {
|
|
/// // 총알 프리팹과 부모 컨테이너를 사용하여 풀을 초기화합니다.
|
|
/// _bulletPool = new GameObjectPool<Bulle>(bulletPrefab, bulletContainer);
|
|
/// }
|
|
///
|
|
/// void SpawnBullet()
|
|
/// {
|
|
/// // 풀에서 총알을 가져와 사용합니다.
|
|
/// Bullet bullet = _bulletPool.GetItem();
|
|
/// bullet.transform.position = transform.position;
|
|
/// bullet.Fire();
|
|
/// }
|
|
///
|
|
/// public void ReturnBulletToPool(Bullet bullet)
|
|
/// {
|
|
/// // 사용이 끝난 총알을 풀에 반환합니다.
|
|
/// _bulletPool.ReturnItem(bullet);
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </summary>
|
|
/// <typeparam name="T">풀링할 MonoBehaviour 타입입니다.</typeparam>
|
|
public class GameObjectPool<T> where T : MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// Resources 폴더에 있는 프리팹의 경로입니다.
|
|
/// </summary>
|
|
protected string _prefabsPath = "";
|
|
/// <summary>
|
|
/// 새로운 오브젝트를 생성할 때 사용할 원본 프리팹입니다.
|
|
/// </summary>
|
|
protected GameObject _originalPrefab;
|
|
|
|
/// <summary>
|
|
/// 활성화된(사용 중인) 오브젝트들을 담아둘 부모 Transform입니다.
|
|
/// </summary>
|
|
protected Transform _activeItemContainer;
|
|
|
|
/// <summary>
|
|
/// 재활용(비활성화된) 오브젝트들을 담아둘 부모 Transform입니다.
|
|
/// </summary>
|
|
protected Transform? _recycledItemContainer;
|
|
|
|
/// <summary>
|
|
/// 현재 활성화되어 사용 중인 아이템을 저장하는 딕셔너리입니다. (Key: 아이템 고유 키, Value: 아이템 인스턴스)
|
|
/// </summary>
|
|
protected Dictionary<string, T> _activeItems = new Dictionary<string, T>();
|
|
|
|
/// <summary>
|
|
/// 재활용을 위해 비활성화된 아이템 리스트입니다.
|
|
/// </summary>
|
|
protected List<T> _recycledItems = new List<T>();
|
|
|
|
/// <summary>
|
|
/// 현재 활성화된 아이템들의 딕셔너리를 읽기 전용으로 가져옵니다.
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, T> ActiveItems => _activeItems;
|
|
|
|
/// <summary>
|
|
/// 활성화된 아이템들의 부모 컨테이너를 가져옵니다.
|
|
/// </summary>
|
|
public Transform ActiveItemContainer { get { return _activeItemContainer; } }
|
|
|
|
/// <summary>
|
|
/// 재활용 아이템들의 부모 컨테이너를 가져옵니다.
|
|
/// </summary>
|
|
public Transform? RecycledItemContainer { get { return _recycledItemContainer; } }
|
|
|
|
// --- 통계용 필드 ---
|
|
private int _inUseCount = 0;
|
|
private int _peakUsage = 0;
|
|
private int _poolMisses = 0;
|
|
private readonly object _statsLock = new object();
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// GameObject 프리팹을 사용하여 풀을 초기화합니다.
|
|
/// </summary>
|
|
/// <param name="originalPrefab">풀링할 오브젝트의 원본 프리팹입니다.</param>
|
|
/// <param name="activeItemContainer">활성화된 오브젝트가 위치할 부모 Transform입니다.</param>
|
|
/// <param name="recycledItemContainer">비활성화된 오브젝트가 위치할 부모 Transform입니다. 지정하지 않으면 activeItemContainer가 사용됩니다.</param>
|
|
public GameObjectPool(GameObject originalPrefab, Transform activeItemContainer, Transform? recycledItemContainer = null)
|
|
{
|
|
_originalPrefab = originalPrefab;
|
|
_activeItemContainer = activeItemContainer;
|
|
_recycledItemContainer = recycledItemContainer;
|
|
|
|
if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resources 폴더 내의 프리팹 경로를 사용하여 풀을 초기화합니다.
|
|
/// </summary>
|
|
/// <param name="prefabsPath">Resources 폴더 기준의 프리팹 경로입니다.</param>
|
|
/// <param name="activeItemContainer">활성화된 오브젝트가 위치할 부모 Transform입니다.</param>
|
|
/// <param name="recycledItemContainer">비활성화된 오브젝트가 위치할 부모 Transform입니다. 지정하지 않으면 activeItemContainer가 사용됩니다.</param>
|
|
public GameObjectPool(string prefabsPath, Transform activeItemContainer, Transform recycledItemContainer)
|
|
{
|
|
_prefabsPath = prefabsPath;
|
|
_originalPrefab = Resources.Load<GameObject>(prefabsPath);
|
|
_activeItemContainer = activeItemContainer;
|
|
_recycledItemContainer = recycledItemContainer;
|
|
if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 키와 연결된 항목을 검색하고, 선택적으로 해당 항목의 부모 변환을 설정합니다.
|
|
/// </summary>
|
|
/// <remarks>항목이 아직 활성화되지 않은 경우, 검색되어 활성 항목 컬렉션에 추가됩니다.
|
|
///</remarks>
|
|
/// <param name="key">검색할 항목과 연결된 키입니다. null이거나 비어 있으면 안 됩니다.</param>
|
|
/// <param name="autoSetParent">항목의 부모 변환을 자동으로 설정할지 여부를 나타내는 부울 값입니다. 부모를 설정하려면 <see
|
|
/// langword="true"/>를 사용하고, 그렇지 않으면 <see langword="false"/>를 사용합니다.</param>
|
|
/// <param name="parent"> <paramref name="autoSetParent"/>가 <see langword="true"/>인 경우 항목에 설정할 부모 변환입니다. 부모를 설정하지 않으면
|
|
/// null이 될 수 있습니다.</param>
|
|
/// <returns>지정된 키와 연관된 <typeparamref name="T"/> 유형의 항목입니다. 항목이 없는 경우
|
|
/// <see langword="null"/>입니다.</returns>
|
|
public T? GetItem(string key, bool autoSetParent = true, Transform? parent = null)
|
|
{
|
|
if (_activeItems.ContainsKey(key))
|
|
{
|
|
return _activeItems[key];
|
|
}
|
|
T? item = GetItem_(autoSetParent, parent);
|
|
if(item != null) _activeItems.Add(key, item);
|
|
return item;
|
|
|
|
}
|
|
|
|
private T? GetItem_(bool autoSetParent = true, Transform? parent = null)
|
|
{
|
|
T? item = null;
|
|
|
|
lock (_statsLock)
|
|
{
|
|
_inUseCount++;
|
|
if (_inUseCount > _peakUsage)
|
|
{
|
|
_peakUsage = _inUseCount;
|
|
}
|
|
}
|
|
|
|
while (_recycledItems.Count > 0)
|
|
{
|
|
item = _recycledItems[0]; //부모가 삭제 되면 null이 될 수 있습니다.
|
|
_recycledItems.RemoveAt(0);
|
|
if (item != null) break; // 유효한 아이템을 찾았으므로 루프를 종료합니다.
|
|
// item이 null이면, 해당 아이템은 파괴된 것으로 간주하고 다음 아이템을 확인합니다.
|
|
}
|
|
|
|
if (item == null)
|
|
{
|
|
lock (_statsLock)
|
|
{
|
|
_poolMisses++;
|
|
}
|
|
GameObject go = UnityEngine.Object.Instantiate(_originalPrefab);
|
|
item = go.GetComponent<T>();
|
|
}
|
|
|
|
item.gameObject.SetActive(true);
|
|
if (autoSetParent)
|
|
{
|
|
if (parent != null)
|
|
{
|
|
item.transform.SetParent(parent, false);
|
|
}
|
|
else if (_activeItemContainer != null)
|
|
{
|
|
item.transform.SetParent(_activeItemContainer, false);
|
|
}
|
|
}
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 키를 사용하여 활성화된 아이템을 풀에 반환합니다.
|
|
/// </summary>
|
|
/// <param name="key">반환할 아이템의 고유 키입니다.</param>
|
|
/// <param name="clearParent">true일 경우, 아이템의 부모를 RecycledItemContainer로 설정합니다.</param>
|
|
public void ReturnItem(string key, bool clearParent = false)
|
|
{
|
|
if (!_activeItems.TryGetValue(key, out T item))
|
|
{
|
|
return; // 키에 해당하는 아이템이 없으면 반환
|
|
}
|
|
|
|
lock (_statsLock)
|
|
{
|
|
_inUseCount--;
|
|
}
|
|
|
|
_activeItems.Remove(key);
|
|
_recycledItems.Add(item);
|
|
|
|
if (clearParent)
|
|
{
|
|
if (item.gameObject.activeInHierarchy && _recycledItemContainer != null)
|
|
{
|
|
item.transform.SetParent(_recycledItemContainer, false);
|
|
}
|
|
}
|
|
item.gameObject.SetActive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 키를 사용하여 활성화된 아이템을 찾습니다.
|
|
/// </summary>
|
|
/// <param name="key">찾을 아이템의 고유 키입니다.</param>
|
|
/// <returns>키에 해당하는 아이템. 없으면 null을 반환합니다.</returns>
|
|
public T? FindActiveItem(string key)
|
|
{
|
|
_activeItems.TryGetValue(key, out T item);
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 활성화된 모든 아이템을 풀에 반환합니다.
|
|
/// </summary>
|
|
public void ReturnAll()
|
|
{
|
|
// ToList()를 사용하여 키 컬렉션의 복사본을 만들어 순회 중 변경 문제를 방지합니다.
|
|
foreach (var key in _activeItems.Keys.ToList())
|
|
{
|
|
ReturnItem(key);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재활용 중인 모든 아이템을 파괴하여 메모리에서 완전히 제거합니다.
|
|
/// </summary>
|
|
public void ClearRecycledItems()
|
|
{
|
|
if (_recycledItems == null)
|
|
return;
|
|
|
|
foreach (var item in _recycledItems)
|
|
{
|
|
if (item != null)
|
|
UnityEngine.Object.Destroy(item.gameObject);
|
|
}
|
|
_recycledItems.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀의 현재 성능 통계를 문자열로 반환합니다.
|
|
/// </summary>
|
|
/// <returns>풀 통계 (최대 사용량, 현재 사용량, 풀 비어있을 때 생성 횟수, 현재 풀 크기)</returns>
|
|
public string GetStats()
|
|
{
|
|
return $"최대 사용량: {_peakUsage}, 현재 사용량: {_inUseCount}, 풀 비어있을 때 생성 횟수: {_poolMisses}, 현재 풀 크기: {_recycledItems.Count}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀 통계를 초기화합니다.
|
|
/// </summary>
|
|
public void ResetStats()
|
|
{
|
|
lock (_statsLock)
|
|
{
|
|
_peakUsage = 0;
|
|
_poolMisses = 0;
|
|
}
|
|
}
|
|
}
|
|
} |