1. 기본 인터페이스 및 추상 클래스

(모든 풀링 가능한 오브젝트는 이 인터페이스를 구현해야함)

public interface IPoolable
{
	// 오브젝트가 풀에서 꺼내질 때 호출
    void OnSpawn();
    // 오브젝트가 풀로 돌아갈 때 호출
    void OnDespawn();
}
using UnityEngine;
using UnityEngine.Pool;

/// <summary>
/// 풀링 가능한 오브젝트의 기본 클래스
/// </summary>
/// <typeparam name="T">풀링될 컴포넌트의 타입</typeparam>
public abstract class PooledObject<T> : MonoBehaviour, IPoolable where T : Component
{
    // 이 오브젝트가 속한 오브젝트 풀 참조
    protected IObjectPool<T> pool;

    // 풀 초기화 상태를 추적하여 중복 초기화 방지
    private bool isPoolInitialized = false;

    // 컴포넌트 캐싱으로 GetComponent 호출 최소화
    private T cachedComponent;

    // Transform 컴포넌트 캐싱 (자주 사용되는 컴포넌트)
    protected Transform cachedTransform;

    /// <summary>
    /// 컴포넌트 참조 초기화 및 캐싱
    /// 파생 클래스에서 재정의할 때는 base.Awake()를 호출해야 함
    /// </summary>
    protected virtual void Awake()
    {
        // this가 T 타입이어야 함을 확실히 하기 위한 체크
        if (this is T component)
        {
            cachedComponent = component;
            cachedTransform = transform;
#if UNITY_EDITOR
            Debug.Log($"[PooledObject] {gameObject.name}의 컴포넌트 캐싱 성공: {typeof(T).Name}");
#endif
        }
        else
        {
#if UNITY_EDITOR
            Debug.LogError($"[PooledObject] {gameObject.name}에서 {typeof(T).Name}로의 캐스팅 실패. " +
                         "제네릭 타입 매개변수가 현재 컴포넌트 타입과 일치하는지 확인하세요.");
#endif
        }
    }

    /// <summary>
    /// 오브젝트의 풀을 초기화하는 메서드
    /// ObjectPoolManager에 의해 호출됨
    /// </summary>
    /// <param name="pool">이 오브젝트가 속할 오브젝트 풀</param>
    public void InitializePool(IObjectPool<T> pool)
    {
        // 중복 초기화 방지 및 null 체크
        if (!isPoolInitialized && pool != null)
        {
            this.pool = pool;
            isPoolInitialized = true;
#if UNITY_EDITOR
            Debug.Log($"[PooledObject] {gameObject.name}의 풀 초기화 성공");
#endif
        }
        else if (pool == null)
        {
#if UNITY_EDITOR
            Debug.LogError($"[PooledObject] {gameObject.name}의 풀 초기화 실패: 풀이 null입니다");
#endif
        }
    }

    /// <summary>
    /// 오브젝트를 풀로 반환하는 메서드
    /// 파생 클래스에서 오브젝트를 풀로 반환할 때 사용
    /// </summary>
    protected void ReturnToPool()
    {
        // 풀과 캐시된 컴포넌트 유효성 검사
        if (pool != null && cachedComponent != null)
        {
#if UNITY_EDITOR
            Debug.Log($"[PooledObject] {gameObject.name}를 풀에 반환 시도");
#endif
            pool.Release(cachedComponent);
        }
        else
        {
            string errorReason = pool == null ? "풀이 null" : "컴포넌트 캐싱 실패";
#if UNITY_EDITOR
            Debug.LogWarning($"[PooledObject] {gameObject.name}를 풀에 반환 실패: {errorReason}\n" +
                           $"Pool: {(pool != null ? "Valid" : "Null")}, " +
                           $"CachedComponent: {(cachedComponent != null ? "Valid" : "Null")}");
#endif
        }
    }

    // IPoolable 인터페이스 구현
    public abstract void OnSpawn();
    public abstract void OnDespawn();
}

2. 오브젝트 풀 매니저

using UnityEngine;
using UnityEngine.Pool;
using System.Collections.Generic;

/// <summary>
/// 오브젝트 풀링을 전역적으로 관리하는 싱글톤 매니저 클래스
/// </summary>
public class ObjectPoolManager : MonoBehaviour
{
    // 싱글톤 인스턴스
    private static ObjectPoolManager instance;
    public static ObjectPoolManager Instance
    {
        get
        {
            if (instance == null)
            {
                var go = new GameObject("ObjectPoolManager");
                instance = go.AddComponent<ObjectPoolManager>();
                DontDestroyOnLoad(go);
            }
            return instance;
        }
    }

    // 프리팹의 ID를 키로 사용하는 풀 캐시 딕셔너리
    private Dictionary<int, IObjectPool<Component>> poolCache = new Dictionary<int, IObjectPool<Component>>();

    // 풀링된 오브젝트들을 담을 컨테이너
    private Transform poolContainer;

    /// <summary>
    /// 싱글톤 초기화 및 중복 인스턴스 처리
    /// </summary>
    private void Awake()
    {
        if (instance != null && instance != this)
        {
#if UNITY_EDITOR
            Debug.LogWarning($"[ObjectPoolManager] 이미 인스턴스가 존재합니다. {gameObject.name} 파괴됨");
#endif
            Destroy(gameObject);
            return;
        }

        poolContainer = new GameObject("PoolContainer").transform;
        poolContainer.SetParent(transform);
    }

    /// <summary>
    /// 지정된 프리팹의 오브젝트 풀을 가져오거나 새로 생성
    /// </summary>
    /// <typeparam name="T">풀링할 컴포넌트 타입</typeparam>
    /// <param name="prefab">풀링할 프리팹</param>
    /// <param name="defaultCapacity">풀의 초기 용량 (기본값: 10)</param>
    /// <param name="maxSize">풀의 최대 크기 (기본값: 100)</param>
    /// <returns>해당 프리팹의 오브젝트 풀</returns>
    public IObjectPool<T> GetPool<T>(T prefab, int defaultCapacity = 10, int maxSize = 100) where T : Component
    {
        int prefabId = prefab.GetInstanceID();

        if (!poolCache.ContainsKey(prefabId))
        {
#if UNITY_EDITOR
            Debug.Log($"{prefab.name}의 새로운 풀 생성");
#endif

            // 순환 참조를 피하기 위해 풀 변수를 먼저 선언
            ObjectPool<T> newPool = null;

            newPool = new ObjectPool<T>(
                createFunc: () =>
                {
                    var instance = CreatePooledItem(prefab);
                    // PooledObject 컴포넌트가 있는 경우 풀 초기화
                    if (instance is PooledObject<T> pooledObject)
                    {
                        pooledObject.InitializePool(newPool);
                    }
                    return instance;
                },
                actionOnGet: OnTakeFromPool,
                actionOnRelease: OnReturnToPool,
                actionOnDestroy: OnDestroyPoolObject,
                collectionCheck: true,
                defaultCapacity: defaultCapacity,
                maxSize: maxSize
            );

            poolCache.Add(prefabId, newPool as IObjectPool<Component>);
            return newPool;
        }

        return poolCache[prefabId] as IObjectPool<T>;
    }

    /// <summary>
    /// 풀링된 아이템을 생성하는 메서드
    /// </summary>
    /// <typeparam name="T">생성할 컴포넌트 타입</typeparam>
    /// <param name="prefab">생성할 프리팹</param>
    /// <returns>생성된 인스턴스</returns>
    private T CreatePooledItem<T>(T prefab) where T : Component
    {
        var instance = Instantiate(prefab);
        instance.name = prefab.name; // 이름맞춰주기 (안해도됨)
        instance.transform.SetParent(poolContainer);
#if UNITY_EDITOR
        Debug.Log($"새로운 인스턴스 생성됨: {instance.name}");
#endif
        return instance;
    }

    /// <summary>
    /// 풀에서 오브젝트를 가져올 때 호출되는 메서드
    /// </summary>
    private void OnTakeFromPool<T>(T obj) where T : Component
    {
        obj.gameObject.SetActive(true);
        if (obj is IPoolable poolable)
        {
            poolable.OnSpawn();
        }
    }

    /// <summary>
    /// 오브젝트가 풀로 반환될 때 호출되는 메서드
    /// </summary>
    private void OnReturnToPool<T>(T obj) where T : Component
    {
        obj.gameObject.SetActive(false);
        if (obj is IPoolable poolable)
        {
            poolable.OnDespawn();
        }
    }

    /// <summary>
    /// 풀에서 오브젝트가 제거될 때 호출되는 메서드
    /// </summary>
    private void OnDestroyPoolObject<T>(T obj) where T : Component
    {
        Destroy(obj.gameObject);
    }
}

 

아래는 이 시스템을 적용하여 몇가지 예제를 구현해보겠다.

최종_최종 버전에서 새로운 예제로 바뀌면서 주석만 보고 확인해주시길 바랍니다.

(지금은 큐브를 스폰하고 땅에 닿으면 사라지는걸 테스트만 해보았음)

 

using UnityEngine;
using UnityEngine.Pool;

/// <summary>
/// 큐브를 주기적으로 생성하고 관리하는 스포너 클래스
/// </summary>
public class CubeSpawner : MonoBehaviour
{
    // 풀링에 사용될 큐브 프리팹
    [SerializeField] private PooledCube cubePrefab;
    // 큐브 생성 간격 (초)
    [SerializeField] private float spawnInterval = 1f;
    // 생성된 큐브에 가할 힘의 크기
    [SerializeField] private float spawnForce = 2f;

    // 큐브 오브젝트 풀 참조
    private IObjectPool<PooledCube> cubePool;
    // 다음 큐브 생성 시간
    private float nextSpawnTime;

    /// <summary>
    /// 초기화 시 오브젝트 풀을 생성
    /// </summary>
    private void Start()
    {
        cubePool = ObjectPoolManager.Instance.GetPool(cubePrefab, 10, 30);
    }

    /// <summary>
    /// 매 프레임마다 큐브 생성 시간을 체크
    /// </summary>
    private void Update()
    {
        if (Time.time >= nextSpawnTime)
        {
            SpawnCube();
            nextSpawnTime = Time.time + spawnInterval;
        }
    }

    /// <summary>
    /// 큐브를 생성하고 초기 힘을 가하는 메서드
    /// </summary>
    private void SpawnCube()
    {
        // 풀에서 큐브 가져오기
        var cube = cubePool.Get();

        // 스포너 위치에 큐브 배치
        cube.transform.position = transform.position;
        cube.transform.rotation = Random.rotation;

        // 캐싱된 Rigidbody 사용
        var rb = cube.Rigidbody;
        if (rb != null)
        {
            Vector3 randomDirection = new Vector3(
                Random.Range(-1f, 1f),
                0,
                Random.Range(-1f, 1f)
            ).normalized;

            rb.AddForce(randomDirection * spawnForce, ForceMode.Impulse);
        }
    }
}

 

using UnityEngine;

/// <summary>
/// 풀링 시스템에서 관리되는 큐브 오브젝트 클래스
/// </summary>
public class PooledCube : PooledObject<PooledCube>
{
    // 물리 효과를 위한 Rigidbody 컴포넌트
    private Rigidbody rb;

    // Rigidbody에 대한 public 프로퍼티 추가
    public Rigidbody Rigidbody => rb;

    /// <summary>
    /// 초기화 시 Rigidbody 컴포넌트를 가져옴
    /// </summary>
    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        Debug.Log("큐브 Awake - Rigidbody 초기화됨");
    }

    /// <summary>
    /// 풀에서 꺼내질 때 호출되며, 물리 속성을 초기화
    /// </summary>
    public override void OnSpawn()
    {
        if (rb != null)
        {
            // 선형 속도와 각속도를 0으로 초기화
            rb.velocity = Vector3.zero;
            rb.angularVelocity = Vector3.zero;
            Debug.Log("큐브 스폰됨 - 속도 초기화됨");
        }
    }

    /// <summary>
    /// 다른 오브젝트와 충돌했을 때 호출되는 메서드
    /// Ground 태그를 가진 오브젝트와 충돌하면 풀로 반환
    /// </summary>
    /// <param name="collision">충돌 정보를 담고 있는 객체</param>
    private void OnCollisionEnter(Collision collision)
    {
        Debug.Log($"충돌 감지됨: {collision.gameObject.name}");
        Debug.Log($"충돌체 태그: {collision.gameObject.tag}");
        Debug.Log($"풀 참조 존재 여부: {pool != null}");

        // Ground 태그를 가진 오브젝트와 충돌하면 풀로 반환
        if (collision.gameObject.CompareTag("Ground"))
        {
            Debug.Log("Ground 태그 감지됨 - 풀 반환 시도");
            ReturnToPool();
        }
    }

    /// <summary>
    /// 풀로 반환될 때 호출되는 메서드
    /// </summary>
    public override void OnDespawn()
    {
        Debug.Log("큐브 디스폰됨");
    }
}

 

작동체크

 

 

 

 

여기까지가 유니티에서 제공하는 ObjectPool API를 활용해서 커스터마이징하여 여러곳에 사용할 수 있도록 개선하는 작업을 해보았다.

 

생각보다 쓸만할 것 같다.

 

예제에서는 직접참조를 했기에, 로드하는 방식으로 참조하는것도 아래에 작성해두겠다.

    private Bullet bulletPrefab;
    private PooledEffect explosionPrefab;

    private void Awake()
    {
        bulletPrefab = Resources.Load<Bullet>("Prefabs/Bullet");
        explosionPrefab = Resources.Load<PooledEffect>("Prefabs/Explosion");
    }

 

장점:

1. 중앙 집중식 풀 관리

2. 인터페이스 및 상속으로 최대한 쉽게 사용하도록 했음...

3. 오브젝트풀의 장점 및 유니티에서 제공해주는 오브젝트 풀 API의 효율을 받을 수 있음

4. 확장성있는 구조임(맞을걸요)

5. 매니저에서 딕셔너리로 관리하기때문에 쉽게 접근도 가능함

 

주의할점:

1. SetParent 하는 부분이 있는데, 생성시점에는 안전하다고 판단했음. 단 특별한 행동이 있을때 여기서 문제가 생길 수 있으니 사용시 문제가 생겼다면 이부분을 의심해봐야함

(이부분을 지워도 구조상 문제가 없음. 문제가 생겼다면 지우고 사용하길 바람)