Today I Learned

[TIL][Unity] 오브젝트 풀링

twotop6357 2024. 6. 3. 20:28

팀 프로젝트 시작

24.06.03 월요일 '3D 런닝액션 게임 제작'을 주제로 팀 프로젝트가 시작되었다. 필수 요구사항 중 내가 맡은 요구사항은 다음과 같다

  • 장애물 생성 로직 : 오브젝트 풀링을 이용하여 장애물이 지속적으로 생성되도록 하는 로직 구현
  • 맵 요소 구성 : 실제 플레이어는 움직이지 않으나 움직이는 듯한 느낌이 들도록 맵 구성
  • 점수 시스템

오늘 구현한 기능은 장애물 생성 로직, 맵 요소 구성이다. 두 파트 모두 오브젝트 풀링을 이용하였다.

 

오브젝트 풀링

오브젝트 풀링은 이미 많이 다루었던 코딩 기술이다. 하지만 제대로 이해하지 못해 다루는 데에 어려움을 겪었다. 앞으로도 많이 사용될 기술이기에 이번 기회에 확실히 이해하고 넘어가고자 하였다.

실제 플레이어는 고정된 위치에 있으나 주변 사물과 방해물 오브젝트를 움직였다.

위 영상은 상술한 두 요구사항을 구현한 것이다. 두 요구사항 모두 오브젝트 풀링을 사용하였다. 게임 시간 내내 재생성되어야 할 두 오브젝트들이 지속적으로 생성 및 소멸한다면 메모리 할당가비지 컬렉션에 따른 성능 저하를 겪을 수 있다.

생성(Instantiate)소멸(Destroy)은 모두 비용이 큰 작업인데, 이를 최소화하여 성능을 향상시키는 것이다. 위 기능을 구현하는 데에 사용한 스크립트를 설명한다.

// ObjectPool.cs
[System.Serializable]
public class Pool
{
    public string tag;
    public GameObject[] prefabs;
    public int size;
}

 

먼저 Pool 클래스를 내부에 선언하였다. 각각 참조에 사용할 tag, 오브젝트의 prefab, 오브젝트 풀의 size를 선언한다. Unity의 Inspector 창에서 간편하게 선언할 수 있다.

 

위 영상의 기능을 구현하는 데에 총 2개의 Pool이 선언되었다. 하나는 Building tag를 가진 Pool이며, 다른 하나는 Impediment tag를 가진 Pool이다. prefab을 배열로 선언한 이유는 위 영상에서와 같이 장애물이 각기 다른 형태를 띄게 하기 위함이다. 영상에서는 같은 줄에 같은 Prefab이 사용되었으나 우연이다.

public List<Pool> pools = new List<Pool>();
public Dictionary<string, Queue<GameObject>> PoolDictionary;

 

상술한 바 총 2개의 Pool이 선언되었다 하였다. 이를 효율적으로 관리하고자 Pool의 List를 선언하여 관리하였다. 또한 Pool에서 꺼내어 쓸 Object에 접근하는 과정을 간소화하기 위해 Dictionary를 사용하여 접근한다.

private void Awake()
{
    PoolDictionary = new Dictionary<string, Queue<GameObject>>();
    int prefabsIndex;
    foreach(var pool in pools)
    {
        Queue<GameObject> queue = new Queue<GameObject>();
        for(int i = 0; i < pool.size; i++)
        {
            prefabsIndex = Random.Range(0, pool.prefabs.Length);
            GameObject obj = Instantiate(pool.prefabs[prefabsIndex], transform);
            obj.SetActive(false);
            queue.Enqueue(obj);
        }

        PoolDictionary.Add(pool.tag, queue);
    }
}

 

ObjectPool.cs의 Awake 메서드에서는 선언한 List와 Dictionary에 구성 요소를 추가하는 작업을 한다. 반복문을 사용하여 Dictionary에 queue를 추가한다. 추가된 queue는 tag값으로 찾을 수 있다.

 public GameObject SpawnFromPool(string tag)
 {
     if (!PoolDictionary.ContainsKey(tag))
     {
         return null;
     }

     GameObject obj = PoolDictionary[tag].Dequeue();
     PoolDictionary[tag].Enqueue(obj);

     obj.SetActive(true);
     return obj;
 }

 

SpawnFromPool 메서드는 tag를 입력받아 Dictionary에서 해당 queue를 찾고, 그 queue에서 알맞은 Object를 배정받는 역할을 한다.

 

위 코드에서는 하나의 상황이 배제되었다. 바로 '배정된 Pool의 크기보다 많은 오브젝트가 생성되었을 때의 상황'이다. 하지만 이 경우는 우리가 진행할 팀 프로젝트에서 발생하지 않을 상황으로 간주하여 코드를 작성하였다.

 

오브젝트 풀링 기술 사용 시 주의사항

게임 성능 개선에 만능으로 보이는 이 기술도 특수한 상황에서는 오히려 성능을 악화시킬 수 있다. 위 코드에서는 Pool의 크기를 직접 지정하는데, 이 크기는 합리적인 수치로 입력되어야 한다. 극단적인 예시로 많이 사용되어야 10개 정도 사용되는 오브젝트의 오브젝트 풀 사이즈를 10000으로 지정한다면, 오히려 성능을 악화시킬 수 있게 된다. 앞서 설명하였듯이 생성은 비용이 큰 작업이다. 오브젝트 풀링 초기 작업에서 10000개의 생성 작업을 진행하고 그 오브젝트 풀에서 10개 남짓한 오브젝트만을 다룬다면 과연 합리적인 작업이라 할 수 있을까? 이렇듯 성능 개선을 위한 이 기술의 효율은 온전히 개발자에게 달려있다. 가장 합리적인 Pool의 사이즈를 찾아 지정하는 것을 통해 성능을 개선할 수 있다.

 

오브젝트 풀링은 수많은 오브젝트가 날아다니는 게임 특성 상 떼어놓을 수 없는 기술이다. 이 기술을 배운 직후에 사용한다고 완벽하게 효율적인 오브젝트 풀링 기술 사용이 가능하냐 하면 그렇지 않다고 당당하게 말하고 싶다. 뭐든 같겠지만 반복 숙달이 중요하다. 다양한 상황에서 이 기술을 적용해보고 더 나은 개선 방법을 찾아 보는 것도 개발 능력 향상에 매우 큰 도움이 될 것 같다.

 

24.06.03 Today I Learned