설계

설계 :: Object Pooling(오브젝트 풀링)

BirdHead 2024. 10. 14. 12:12

Q.오브젝트 풀링이란?

A. Scene에서 사용할 오브젝트(객체)들을 미리 만들어놓고서 컨테이너에 저장해놓는다.
필요할때 오브젝트 풀링에서 해당 오브젝트를 호출해서 사용한 뒤 다시 컨테이너(오브젝트 풀)에 돌려주는 설계 패턴이다.

 

Q.오브젝트 풀링을 왜 사용하는가?

A.

객체를 생성/파괴하는 메서드인 Instantiate / Destroy는 호출시에 처리비용이 매우 크다.

즉, 처리비용이 매우 크다는 말은 최적화에 안좋다는 뜻이다.

 

미리 객체를 만들어 놓으면 첫 시작할때만 생성을 하기에 실제 인게임시에는 오브젝트를 사용하려고 꺼낼때 처리비용이 크지 않다.

또한 파괴하지 않고 컨테이너(오브젝트 푸)에 돌려주기 때문에 계속 재활용이 가능한 장점도 있다.

 

Q.오브젝트 풀링을 어떡해 사용해?

A.오브젝트 풀링은 프로그래머의 재량에 따라 형태나 많이 바뀐다. 

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
	//Pool 구조체는 오브젝트 하나를 의미함
    [System.Serializable]
    public class Pool
    {
        public string tag; //오브젝트의 태그
        public GameObject prefab; //오브젝트의 프리팹
        public int size; //Pool에 담을 오브젝트의 갯수
        //TODO CODE : 오브젝트에서 사용할 필드값
    }

    public List<Pool> pools = new List<Pool>(); //사용할 오브젝트를 보관하는 컨테이너
    public Dictionary<string, Queue<GameObject>> PoolDictionary;//실제로 호출하여 사용하게 될 컨테이너

    private void Awake()
    {
        PoolDictionary = new Dictionary<string, Queue<GameObject>>();
		
        //List에 있는 오브젝트를 하나씩 호출하여 n개만큼 생성 후 오브젝트 풀(PoolDictionary)에 담음
        foreach (var pool in pools)
        {
        	//Unity 하이어라키에 오브젝트를 담을 부모 오브젝트 생성
            GameObject poolContainer = new GameObject("Pool_Container_" + pool.tag);
			
            //Queue 자료구조를 사용하여 오브젝트를 관리
            //Queue를 사용한 이유 -> 순서에 상관없는 오브젝트들 사용하기에
            Queue<GameObject> queue = new Queue<GameObject>()
            
            //생서할 오브젝트 갯수만큼 생성
            for (int i = 0; i < pool.size; i++)
            {
            	//오브젝트를 생성 -> 오브젝트를 비활성화 -> 오브젝트 풀(queue)에 담음
                GameObject obj = Instantiate(pool.prefab, poolContainer.transform);
                obj.SetActive(false);
                queue.Enqueue(obj);
            }
			
            //queue에 담은 풀을 Dictionary에 추가 -> 하나의 오브젝트 풀이 완성 
            PoolDictionary.Add(pool.tag, queue);
        }
    }
	
    //외부에서 오브젝트가 필요할때 호출되는 함수
    public GameObject SpawnFromPool(string tag)
    {
    	//오브젝트 풀의 key값을 비교하여 일치하지 않으면 탈출
        if(!PoolDictionary.ContainsKey(tag))
        {
            return null;
        }
		
        //해당 key의 오브젝트를 하나 dequeue
        GameObject obj = PoolDictionary[tag].Dequeue();
        
        //Queue에서 오브젝트 하나가 dequeue됫기에 다시 채워준다.
        PoolDictionary[tag].Enqueue(obj);
		
       	//꺼낸 오브젝트를 활성화
        obj.SetActive(true);
		
        return obj;
    }
}

해당 코드는 총알이나 투사체 및 파티클 같이 순서는 상관없고 재활용을 계속 해야되는 오브젝트에 사용되는 패턴이다.


 

이 코드의 단점은 오브젝트가 하나씩 발사 될때마다 다시 Enqueue로 챙겨주고 있으며 반납을 하지 않고있다.

즉, 투사체들은 오브젝트에 닿을때 해당 조건이되면 Destroy로 파괴되고 있기에 최적화에 좋지 않은 모습을 보이고있다.

 

대처법으로는 외부에서 사용이 완료된 오브젝트를 다시 반납하는 메서드를 만들어 사용하는것이 최적화에 좋다.

 


또 다른 단점은 오브젝트가 많은 숫자를 필요로 만들때 기존 오브젝트 갯수보다 많으면 문제가 생긴다.

예시로 이런 상황이 있다. 

1. 1초에 50개 발사 

2. 오브젝트를 20개만 만들어놓음 
3. 20개를 발사하고 21개쨰를 발사할떄 미리 발사해놧던 20개 중 하나가 사라지고 21개째가 되어 재활용이 되어버리는 문제
4. 총이 나가다가 뒤로 돌아오는 현상이 발생 

이런 부자연스러운 현상이 발생하기에 

 

대처법으로는 Queue대신 List를 활용하여 List 각 하나의 원소에 접근하여 사용중인지를 체크, 만약 모든 오브젝트가 사용 중인경우 추가로 오브젝트 풀에 오브젝트를 추가 생성하는 방식이 있다. 

 


위에 코드는 예시일 뿐이며 , 다른 사람들이 사용한 오브젝트풀링의 좋은 예제가 많이 있다.

여기서는 이런 개념이 있다라는 것을 파악하고 다음 프로젝트 진행시에 사용하는것을 목표로 하자.