[유니티/C#]배열과 리스트를 사용한 오브젝트 풀링, 가비지 콜렉팅을 통한 메모리 최적화

728x90

오브젝트 풀링(Object Pooling)와 가비지 콜렉션(Garbage Collection)

오브젝트 풀링은 총알을 미리 탄창에 넉넉하게 장전해 놓는 다는 개념으로 생각하면 된다. 오브젝트 풀의 핵심은 배열과 리스트로, 배열은 방의 크기가 정해져있고 자리가 고정되어 있기 때문에 추가와 삭제가 매우 어렵다. 반면 리스트는 추가와 삭제가 매우 쉽고 검색은 때에 따라 배열보다 쉽다.

 

오브젝트 풀링은 메모리 최적화에 매우 중요한 기능이다. 해당 내용을 이해하기 위해서는 먼저 가비지 콜렉터(Garbage Collector)의 개념을 이해해야 한다. 가비지 콜렉터가 수행하는 가비지 콜렉션(Garbage Collection)은 파편화된 메모리리를 정리해주는 기능이다.

시스템 상 메모리는 배열 형식으로 순서대로 저장이 된다. 하지만 중간 중간에 저장된 데이터를 검출하면 메모리가 듬성 듬성 빈 상태가 된다. 이를 메모리 파편화(Memory Fragment)라고 하는데, 컴퓨터는 다음 검출을 그 상태에서 바로 수행하는 것이 아니라 다시 맨 처음부터 배열을 순서대로 정리한 후 수행하도록 되어 있다. 이러한 작업을 바로 가비지 컬렉션이라고 한다. 

하지만 가비지 콜렉션은 연산 횟수가 많아질수록 급격한 성능 저하를 불러일으킬 수 있다. 배열인 메모리 공간을 하나씩 정리해주는 과정에서 연산이 수십만번씩 돌아가게 되면 분명 버벅거리는 현상이 발생한다. 

가비지 콜렉션은 썼다 지웠다 하는 것은 너무 많은 연산량이 필요하기 때문에, 실무에서는 오브젝트 풀링을 사용해 데이터를 껐다/켰다를 반복하므로서 메모리를 안정화시킨다.

 

다음은 배열을 사용한 오브젝트 풀링으로, Bullet을 발사하는 코드와 Bullet에 닿았을때 destroy 되는 enemy를 구현하는 코드이다.

public GameObject bulletFactory;
public int bulletPoolSize = 10;
GameObject[] bulletPool;
   
// Start is called before the first frame update
void Start()
{
    // 태어날 때 탄창에 총알을 만들어 넣고 싶다
    bulletPool = new GameObject[bulletPoolSize];
    for (int i = 0; i < bulletPoolSize; i++)
    {
        // 만약 4개까지 생성됐다면
        if(i == 4)
        {
            // 그만 생성하고 싶다
            break;
            // return; 아예 함수 종료
            // continue; 4만 빼고 실행
        }
        // 총알이 있어야 한다.
        GameObject bullet = Instantiate(bulletFactory);
        // 탄창에 총알을 넣고 싶다  
        bulletPool[i] = bullet;
        bullet.SetActive(false);
    }    
}
// Update is called once per frame
void Update()
{
    // 사용자가 발사버튼을 눌렀으니까
    if(Input.GetButtonDown("Fire1"))
    {
        // 총알 발사 시 탄창에서 총알을 가져와 발사하고 싶다
        for(int i = 0; i < bulletPoolSize; i++)
        {
            // 단 비활성화 되어 있는 총알을 찾아서 발사하고 싶다
            // 총알이 있어야 한다
            GameObject bullet = bulletPool[i];
            // 만약 총알이 비활성화되어 있다면
            if (bullet.activeSelf == false)
            {
                bullet.SetActive(true);
                // 총알을 총구앞에 놓기
                bullet.transform.position = transform.position;
                break;
            }
        }            
    }
}
private void OnCollisionEnter(Collision collision)
{
    // 만약 부딪힌 녀석이 총알이라면
    if(collision.gameObject.name.Contains("Bullet"))
    {
        // 탄창에 넣어주고 싶다
        collision.gameObject.SetActive(false);
    }
    else   //그렇지 않으면
    {
        // 갸도 죽고
        Destroy(collision.gameObject);            
    }
    // 나도 죽고
    Destroy(gameObject);
}

 

배열(Array)과 리스트(List)

배열을 for문으로 돌리는 것은 메모리 성능을 떨어트린다, for문을 돌리는 횟수가 많고 매번 for문을 돌려서 특정 변수를 검출하면 프레임이 떨어지고 성능 저하로 인해 컴퓨터가 느려진다. 이런 경우 리스트를 사용해서 최적화해줄 수  있다. 

리스트는 저장된 변수마다 메모리 주소를 가지고 있다. 변수a - 변수b - 변수c 가 순서대로 저장된 상황에서 a에서 c로 바로 이동하려면 a-b / b-c의 연결을 끊고 바로 a-c로 연결해주기만 하면 된다.  for문을 돌릴 때 배열에 저장된 데이터 삭제 - 재배열 - 생성하는 순서를 반복하는 것이 아니라 연결만 바꾸어주면 되기 때문에 오브젝트 풀링 기법에 리스트를 적용하는 것이 성능적으로 유리하다.

 

다음은 위의 총알 발사와 적 제거의 동일 코드를 배열로 변경한 스크립트이다.

public GameObject bulletFactory;
public int bulletPoolSize = 10;
//GameObject[] bulletPool; // 배열
public List<GameObject> bulletPool = new List<GameObject>(); // 리스트, public으로 공개해서 enemy 스크립트에서 접근 가능
 
// Start is called before the first frame update
void Start()
{
     // 태어날 때 탄창에 총알을 만들어 넣고 싶다
     // bulletPool = new GameObject[bulletPoolSize]; // 배열
     for (int i = 0; i < bulletPoolSize; i++)
     {
         // 만약 4개까지 생성됐다면
         if(i == 4)
         {
         // 그만 생성하고 싶다
         break;
         }
         // 총알이 있어야 한다.
         GameObject bullet = Instantiate(bulletFactory);
         // 탄창에 총알을 넣고 싶다  
         // bulletPool[i] = bullet; //배열
         bulletPool.Add(bullet);
         bullet.SetActive(false);
    }    
}

void Update()
{
    // 사용자가 발사버튼을 눌렀으니까
    if(Input.GetButtonDown("Fire1"))
    {
        // 총알 발사 시 탄창에서 총알을 가져와 발사하고 싶다
        // -> 탄창에 총알이 있다면 맨 위에 있는 한 발 발사하고 싶다
        if(bulletPool.Count > 0)
        {
            GameObject bullet = bulletPool[0];
            bullet.SetActive(true);
            bullet.transform.position = transform.position;
            // 탄창에서 총알을 제거함
            bulletPool.RemoveAt(0);
        }
    }
}
private void OnCollisionEnter(Collision collision)
{
    // 만약 부딪힌 녀석이 총알이라면
    if(collision.gameObject.name.Contains("Bullet"))
    {
        // 탄창에 넣어주고 싶다
        // 1. 플레이어가 있어야 한다
        // 2. PlayerFire가 있어야 한다
        // 3. 탄창이 있어야 한다

        GameObject player = GameObject.Find("Player");
        PlayerFire pf = player.GetComponent<PlayerFire>();
        pf.bulletPool.Add(collision.gameObject);
        collision.gameObject.SetActive(false);
    }
    else   //그렇지 않으면
    {
        // 갸도 죽고
        Destroy(collision.gameObject);            
    }
    // 나도 죽고
    Destroy(gameObject);
}

 

728x90