326 lines
14 KiB
C#
326 lines
14 KiB
C#
#nullable enable
|
|
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace UVC.Pool
|
|
{
|
|
/// <summary>
|
|
/// Unity의 MonoBehaviour를 상속받는 컴포넌트들을 재사용하기 위한 제네릭 오브젝트 풀 클래스입니다.
|
|
/// 오브젝트 풀링은 게임 오브젝트를 반복적으로 생성(Instantiate)하고 파괴(Destroy)하는 대신,
|
|
/// 비활성화 상태로 보관하고 필요할 때 다시 활성화하여 사용하는 디자인 패턴입니다.
|
|
/// 이를 통해 불필요한 메모리 할당 및 가비지 컬렉션을 줄여 게임 성능을 향상시킬 수 있습니다.
|
|
/// </summary>
|
|
/// <typeparam name="T">풀링할 대상이며, MonoBehaviour를 상속해야 합니다.</typeparam>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 1. 풀링할 대상이 되는 스크립트 (예: Bullet.cs)
|
|
/// public class Bullet : MonoBehaviour
|
|
/// {
|
|
/// public void Fire(Vector3 direction)
|
|
/// {
|
|
/// // 발사 로직
|
|
/// }
|
|
/// }
|
|
///
|
|
/// // 2. 풀을 관리하는 클래스
|
|
/// public class PlayerController : MonoBehaviour
|
|
/// {
|
|
/// public GameObject bulletPrefab; // Unity 에디터에서 총알 프리팹을 할당
|
|
/// public Transform bulletContainer; // 활성 총알들이 위치할 부모 Transform
|
|
///
|
|
/// private ItemPool<Bullet> bulletPool;
|
|
///
|
|
/// void Start()
|
|
/// {
|
|
/// // 총알을 관리할 ItemPool을 생성합니다.
|
|
/// bulletPool = new ItemPool<Bullet>(bulletPrefab, bulletContainer);
|
|
/// }
|
|
///
|
|
/// void Update()
|
|
/// {
|
|
/// if (Input.GetKeyDown(KeyCode.Space))
|
|
/// {
|
|
/// // 풀에서 총알을 하나 가져옵니다.
|
|
/// Bullet newBullet = bulletPool.GetItem();
|
|
/// newBullet.transform.position = this.transform.position;
|
|
/// newBullet.Fire(this.transform.forward);
|
|
///
|
|
/// // 2초 후에 총알을 풀에 반환합니다.
|
|
/// StartCoroutine(ReturnBulletAfterTime(newBullet, 2.0f));
|
|
/// }
|
|
/// }
|
|
///
|
|
/// private IEnumerator ReturnBulletAfterTime(Bullet bullet, float delay)
|
|
/// {
|
|
/// yield return new WaitForSeconds(delay);
|
|
/// bulletPool.ReturnItem(bullet);
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public class ItemPool<T> where T : UnityEngine.MonoBehaviour
|
|
{
|
|
/// <summary>
|
|
/// Resources 폴더에 있는 프리팹의 경로입니다. 생성자에서 이 경로를 이용해 프리팹을 로드할 수 있습니다.
|
|
/// </summary>
|
|
protected string _prefabsPath = "";
|
|
|
|
/// <summary>
|
|
/// 풀에서 아이템을 새로 생성할 때 사용할 원본 프리팹입니다.
|
|
/// </summary>
|
|
protected GameObject _originalPrefab;
|
|
|
|
/// <summary>
|
|
/// 현재 사용 중인 (활성화된) 아이템들이 위치할 부모 Transform 입니다.
|
|
/// 씬(Scene)의 계층 구조를 깔끔하게 정리하는 데 도움이 됩니다.
|
|
/// </summary>
|
|
protected Transform _activeItemContainer;
|
|
|
|
/// <summary>
|
|
/// 재활용 대기 중인 (비활성화된) 아이템들이 위치할 부모 Transform 입니다.
|
|
/// null일 경우, _activeItemContainer를 사용합니다.
|
|
/// </summary>
|
|
protected Transform? _recycledItemContainer;
|
|
|
|
/// <summary>
|
|
/// 현재 사용 중인 아이템들의 목록입니다. GetItem() 호출 시 이 목록에 추가됩니다.
|
|
/// </summary>
|
|
protected List<T> _activeItems = new List<T>();
|
|
|
|
/// <summary>
|
|
/// 재활용을 위해 대기 중인 아이템들의 목록입니다. ReturnItem() 호출 시 이 목록에 추가됩니다.
|
|
/// </summary>
|
|
protected List<T> _recycledItems = new List<T>();
|
|
|
|
// --- 통계용 필드 ---
|
|
private int _inUseCount = 0; // 현재 사용 중인 아이템의 수
|
|
private int _peakUsage = 0; // 역대 최대 동시 사용량
|
|
private int _poolMisses = 0; // 풀이 비어있어서 새로 생성해야 했던 횟수
|
|
private readonly object _statsLock = new object(); // 멀티스레드 환경에서의 통계 데이터 접근을 위한 잠금 객체
|
|
|
|
/// <summary>
|
|
/// 현재 활성화된 아이템 목록을 가져옵니다.
|
|
/// </summary>
|
|
public List<T> ActiveItems { get { return _activeItems; } }
|
|
|
|
/// <summary>
|
|
/// 활성 아이템들의 부모 Transform을 가져옵니다.
|
|
/// </summary>
|
|
public Transform ActiveItemContainer { get { return _activeItemContainer; } }
|
|
|
|
/// <summary>
|
|
/// 재활용 아이템들의 부모 Transform을 가져옵니다.
|
|
/// </summary>
|
|
public Transform? RecycledItemContainer { get { return _recycledItemContainer; } }
|
|
|
|
/// <summary>
|
|
/// 원본 프리팹(GameObject)을 직접 전달하여 풀을 생성합니다.
|
|
/// </summary>
|
|
/// <param name="originalPrefab">풀링할 아이템의 원본 프리팹입니다.</param>
|
|
/// <param name="activeItemContainer">활성화된 아이템들이 위치할 부모 Transform입니다.</param>
|
|
/// <param name="recycledItemContainer">비활성화된 아이템들이 위치할 부모 Transform입니다. null이면 activeItemContainer가 사용됩니다.</param>
|
|
public ItemPool(GameObject originalPrefab, Transform activeItemContainer, Transform? recycledItemContainer = null)
|
|
{
|
|
_originalPrefab = originalPrefab;
|
|
_activeItemContainer = activeItemContainer;
|
|
_recycledItemContainer = recycledItemContainer;
|
|
|
|
if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resources 폴더 내의 프리팹 경로(string)를 전달하여 풀을 생성합니다.
|
|
/// </summary>
|
|
/// <param name="prefabsPath">Resources 폴더 기준의 프리팹 경로입니다. (예: "Prefabs/Enemy")</param>
|
|
/// <param name="activeItemContainer">활성화된 아이템들이 위치할 부모 Transform입니다.</param>
|
|
/// <param name="recycledItemContainer">비활성화된 아이템들이 위치할 부모 Transform입니다. null이면 activeItemContainer가 사용됩니다.</param>
|
|
public ItemPool(string prefabsPath, Transform activeItemContainer, Transform recycledItemContainer)
|
|
{
|
|
_prefabsPath = prefabsPath;
|
|
_originalPrefab = Resources.Load<GameObject>(prefabsPath);
|
|
_activeItemContainer = activeItemContainer;
|
|
_recycledItemContainer = recycledItemContainer;
|
|
if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀에서 아이템을 가져옵니다. 재활용 가능한 아이템이 있으면 그것을 반환하고, 없으면 새로 생성합니다.
|
|
/// </summary>
|
|
/// <param name="autoSetParent">true일 경우, 아이템의 부모를 자동으로 ActiveItemContainer로 설정합니다.</param>
|
|
/// <param name="parent">autoSetParent가 true일 때, ActiveItemContainer 대신 지정할 특정 부모 Transform입니다.</param>
|
|
/// <returns>활성화된 아이템의 컴포넌트</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 기본 사용법: 자동으로 부모가 설정됩니다.
|
|
/// Bullet bullet1 = bulletPool.GetItem();
|
|
///
|
|
/// // 특정 부모를 지정하여 아이템을 가져옵니다.
|
|
/// Bullet bullet2 = bulletPool.GetItem(true, specialSpawnPoint);
|
|
///
|
|
/// // 부모를 직접 설정하고 싶을 때 사용합니다.
|
|
/// Bullet bullet3 = bulletPool.GetItem(false);
|
|
/// bullet3.transform.SetParent(anotherContainer, false);
|
|
/// </code>
|
|
/// </example>
|
|
public 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);
|
|
}
|
|
}
|
|
|
|
_activeItems.Add(item);
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사용이 끝난 아이템을 풀에 반환합니다. 아이템은 비활성화되고 재활용 목록으로 이동합니다.
|
|
/// </summary>
|
|
/// <param name="item">반환할 아이템</param>
|
|
/// <param name="clearParent">true일 경우, 아이템의 부모를 RecycledItemContainer로 설정합니다.</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 총알이 어딘가에 부딪혔을 때
|
|
/// void OnCollisionEnter(Collision collision)
|
|
/// {
|
|
/// // 아이템을 풀에 반환합니다.
|
|
/// bulletPool.ReturnItem(this);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public void ReturnItem(T item, bool clearParent = false)
|
|
{
|
|
if (item == null) return;
|
|
// 이미 반환되었거나 풀에 속하지 않은 아이템은 무시합니다.
|
|
if (!_activeItems.Contains(item)) return;
|
|
|
|
lock (_statsLock)
|
|
{
|
|
_inUseCount--;
|
|
}
|
|
|
|
_activeItems.Remove(item);
|
|
_recycledItems.Add(item);
|
|
if (clearParent)
|
|
{
|
|
// 아이템의 부모를 재활용 컨테이너로 변경하여 씬을 정리합니다.
|
|
if (item.gameObject.activeInHierarchy && _recycledItemContainer != null)
|
|
{
|
|
item.transform.SetParent(_recycledItemContainer, false);
|
|
}
|
|
}
|
|
|
|
item.gameObject.SetActive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 사용 중인 모든 아이템을 풀에 반환합니다.
|
|
/// </summary>
|
|
/// <param name="clearParent">true일 경우, 아이템의 부모를 RecycledItemContainer로 설정합니다.</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 스테이지가 끝날 때 모든 적을 한번에 제거(반환)합니다.
|
|
/// enemyPool.ReturnAll();
|
|
/// </code>
|
|
/// </example>
|
|
public void ReturnAll(bool clearParent = false)
|
|
{
|
|
int i, len = _activeItems.Count;
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
ReturnItem(_activeItems[0], clearParent);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재활용 목록에 있는 모든 아이템의 게임 오브젝트를 파괴(Destroy)합니다.
|
|
/// 메모리를 정리하고 싶을 때 사용합니다. (예: 씬 전환 시)
|
|
/// </summary>
|
|
public void ClearRecycledItems()
|
|
{
|
|
if (_recycledItems == null)
|
|
return;
|
|
|
|
var c = _recycledItems.Count;
|
|
for (int i = 0; i < c; i++)
|
|
{
|
|
var item = _recycledItems[0];
|
|
if (item != null)
|
|
UnityEngine.Object.Destroy(item.gameObject);
|
|
}
|
|
_recycledItems.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀의 현재 성능 통계를 문자열로 반환합니다. 디버깅 및 성능 분석에 유용합니다.
|
|
/// </summary>
|
|
/// <returns>풀 통계 (최대 사용량, 현재 사용량, 풀 비어있을 때 생성 횟수, 현재 풀 크기)</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// void OnGUI()
|
|
/// {
|
|
/// // 화면에 풀 통계를 표시합니다.
|
|
/// GUI.Label(new Rect(10, 10, 500, 20), bulletPool.GetStats());
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public string GetStats()
|
|
{
|
|
return $"최대 사용량: {_peakUsage}, 현재 사용량: {_inUseCount}, 풀 비어있을 때 생성 횟수: {_poolMisses}, 현재 풀 크기: {_recycledItems.Count}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 풀의 성능 통계 데이터를 초기화합니다.
|
|
/// </summary>
|
|
public void ResetStats()
|
|
{
|
|
lock (_statsLock)
|
|
{
|
|
_peakUsage = 0;
|
|
_poolMisses = 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
} |