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 하는 부분이 있는데, 생성시점에는 안전하다고 판단했음. 단 특별한 행동이 있을때 여기서 문제가 생길 수 있으니 사용시 문제가 생겼다면 이부분을 의심해봐야함
(이부분을 지워도 구조상 문제가 없음. 문제가 생겼다면 지우고 사용하길 바람)