[Unity] ObjectPooling 시스템 분석

Connected Brain·2025년 3월 20일

ObjectPooling 시스템 분석하기

ObjectPooling이란?

  • 슈팅게임의 탄막이나 생존 게임의 자원 등 여러 개의 오브젝트가 생성되고 소멸되는 것이 반복되어야 하는 상황에서 적용하는 것이 유리
  • 매번 오브젝트를 생성-소멸하는 것이 아닌 사용 중인 오브젝트는 활성화하고 사용하고 있지 않은 오브젝트를 비활성화해 매번 새로운 오브젝트를 만드는 비용을 줄임

1. IPool 인터페이스 구성

public interface IPoolable
{
    void Initialize(Action<GameObject> returnAction);
    void OnSpawn();
    void OnDespawn();
}
  • ObjectPooling을 사용할 오브젝트에 포함될 인터페이스를 구성한다.

    OnSpawn()

    • 오브젝트를 사용해야 할 때 기존에 생성된 Queue에 있는 것을 활성화하거나 새로 생성한다.

    OnDespawn()

    • 오브젝트가 사용된 후 비활성화 상태로 바꾸고 Queue에 포함시킨다.

    Initialize(Action<GameObject> returnAction)

    • 초기화시 오브젝트를 비활성화하고 Queue로 되돌려 보내는 Action을 연결한다.

2. ObjectPoolManager

필드

public GameObject[] prefabs;
private Dictionary<int, Queue<GameObject>> pools = new Dictionary<int, Queue<GameObject>>();

public static ObjectPoolManager Instance { get; private set; }
  • Object Pooling을 사용할 프리팹 배열과 인덱스와 게임 오브젝트를 담는 Queue로 이루어진 Dictionary가 선언되어 있다.
  • ObjectPoolManager를 전역에서 접근하기 위해 static으로 만들어 주었다.

Method

private void Awake()
{
    Instance = this;

    for(int i = 0; i < prefabs.Length; i++)
    {
        pools[i] = new Queue<GameObject>();
    }
}
  • Awake 시점에서 지정된 프리팹들을 Dictionary에 저장한다. 배열에서 각 프리팹의 인덱스가 pools에서 Queue를 구분하는 key 값이 된다.
public GameObject GetObject(int prefabIndex, Vector3 position, Quaternion rotation)
{
    if(!pools.ContainsKey(prefabIndex))
    {
        Debug.Log($"프리팹 인덱스 {prefabIndex}에 대한 풀이 존재하지 않음");
        return null;
    }

    GameObject obj;
    if (pools[prefabIndex].Count > 0)
    {
        obj = pools[prefabIndex].Dequeue();
    }
    else
    {
        obj = Instantiate(prefabs[prefabIndex]);
        obj.GetComponent<IPoolable>()?.Initialize(o => ReturnObject(prefabIndex, o));
    }

    obj.transform.SetPositionAndRotation(position, rotation);
    obj.SetActive(true);
    obj.GetComponent<IPoolable>()?.OnSpawn();
    return obj;
}
  • GetObject()를 통해 외부에서 Queue에 있는 오브젝트를 가져올 수 있다.

    int prefabIndex

    • pools에서 오브젝트를 불러오기 위한 Queue를 찾을 수 있는 key 값

    Vector3 position, Quaternion rotation

    • 오브젝트를 불러와 초기화 할 때 요구되는 위치 및 회전 정보
  • prefabIndex에서 Queue를 찾을 수 없으면 디버그 로그를 띄워 알리고, 있다면 해당 키 값에 해당하는 Queue가 크기가 0보다 큰지 확인 후 해당 Queue에서 오브젝트를 가져와 활성화하고 OnSpawn을 실행한다.

  • 만약 Queue가 크기가 0보다 작을 때는 프리팹을 이용해 새로운 오브젝트를 생성하고 해당 오브젝트를 초기화하면서 비활성화시 실행할 함수를 연결한다. 이후 과정은 동일하다.

void ReturnObject(int prefabIndex, GameObject obj)
{
    if(!pools.ContainsKey(prefabIndex))
    {
        Destroy(obj);
        return;
    }

    obj.SetActive(false);
    pools[prefabIndex].Enqueue(obj);
}
  • ReturnObject는 오브젝트를 다시 Queue로 돌려보낼 때 사용하며, 같이 입력된 키가 pools에 없으면 해당 오브젝트를 파괴하고 실행을 종료한다.
  • pools에 해당하는 Queue가 있을 경우 오브젝트를 비활성화하고 Queue로 돌려보낸다.

3. ProjectileController&Manager

  • 이번 프로젝트에서는 Object Pooling을 투사체를 관리하기 위해 사용하였다.

ProjectileController

public class ProjectileController : MonoBehaviour, IPoolable
{
	private Action<GameObject> returnToPool;
	...
    
    void DestroyProjectile(Vector3 position, bool creatFx)
	{
		...

    	//Destroy(this.gameObject);
    	OnDespawn();
	}

	public void Initialize(Action<GameObject> returnAction)
	{
    	returnToPool = returnAction;
	}

	public void OnSpawn()
	{

	}

	public void OnDespawn()
	{
    	returnToPool?.Invoke(gameObject);
	}
}
  • IPoolable을 추가하고 해당 인터페이스에서 요구되는 함수들을 구성하여 주었다. 화살이 파괴될 때 기존에 삭제하는 방식에서 OnDespawn을 사용하도록 변경하였다.

ProjectileMananger

public void ShootBullet(RangeWeaponHandler rangeWeaponHandler, Vector2 startPos, Vector2 dir)
{
    //GameObject origin = projectilePrefabs[rangeWeaponHandler.BulletIndex];
    //GameObject obj = Instantiate(origin, startPos, Quaternion.identity);

    GameObject obj = 	objectPoolManager.GetObject(rangeWeaponHandler.BulletIndex, startPos, Quaternion.identity);

    ...
}
  • ProjectileMananger에서도 프리팹을 생성하는 것에서 ObjectPoolMananger에서 GetObject를 사용해 오브젝트를 가져오도록 변경하였다.

  • 인스펙터 창에 투사체 오브젝트가 사라지는 것이 아닌 활성화-비활성화 되고 있음을 확인할 수 있다.

0개의 댓글