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