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