유니티(Unity)/최적화

오브젝트 풀링(Object Pooling)이란?

Whiny 2021. 7. 2. 14:53

이번 포스팅에서는 다양한 최적화 기법들 중에서 오브젝트 풀링(Object Pooling)에 관해서 알아보겠습니다.

 

간단하게 오브젝트 풀링을 설명하자면, 오브젝트의 Pool 웅덩이를 만들어두고, 그 웅덩이 안에서 필요할 때마다

객체를 꺼내서 사용하는 것을 뜻합니다.

 

게임에서는 엄청나게 많고 다양한 오브젝트를 사용하는데, 이런 오브젝트들을 실시간으로 생성하고, 파괴하고를 반복하면 어떻게 어떻게 될까요?

 

수천수백의 오브젝트들이 게임상에서 등장하고 사라질 때, 새로 오브젝트들을 생성하고 거기에 파괴하면 메모리를 할당하고 해제하는 일이 반복될 것입니다.

 

할당과 해제는 CPU가 담당하고, CPU에 부담이 오브젝트가 많아질수록 더해지게 됩니다. 게다가 유니티에서 메모리 해제를 하며 가비지 컬렉터가 발생하는데, 많은 오브젝트를 파괴할수록 많은 가비지 컬렉터가 발생하고, 이는 CPU에 더 큰 부담이 됩니다.

 


그림으로 한번 볼까요?

 

생성, 파괴 반복하는 방식

오브젝트 풀링 방식


이번에는 직접 2가지 방식을 구현하여 비교해보도록 합시다.

 

먼저 생성, 파괴 반복하는 방식부터 구현해봅시다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeSpawner : MonoBehaviour
{
    public GameObject SpawnObject;
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine("Spawn");

    }

    IEnumerator Spawn()
    {
        yield return new WaitForSeconds(0.01f);
        Instantiate(SpawnObject);
        StartCoroutine("Spawn");
    }
}

위 코드는 0.01초마다 오브젝트를 생성해주는 코드입니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingCube : MonoBehaviour
{
    private Vector3 direction;
    void Start()
    {
        direction = new Vector3(Random.Range(-0.1f, 0.1f), Random.Range(-0.1f, 0.1f), Random.Range(-0.1f, 0.1f));
        StartCoroutine("DestroyObject");
    }
    private void FixedUpdate()
    {
        transform.position += direction;
    }
    IEnumerator DestroyObject()
    {
        yield return new WaitForSeconds(4.0f);
        Destroy(gameObject);
    }
}

 위 코드는 생성되는 오브젝트를 랜덤한 방향으로 움직이고 4초 후에 파괴하는 코드입니다.

 

실행을 시키면?

평균 150 FPS 내외로 출력되고, 간혹 120 FPS까지 떨어지는 것을 확인할 수 있었습니다.

 

이번에는 오브젝트 풀링 방식 구현해봅시다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeController : MonoBehaviour
{
    public GameObject CubeObject;
    public GameObject[] gameObjects;
    private int pivot = 0;
    void Start()
    {
        gameObjects = new GameObject[500];
        for(int i = 0; i < 500; i++)
        {
            GameObject gameObject= Instantiate(CubeObject);
            gameObjects[i] = gameObject;
            gameObject.SetActive(false);
        }
        StartCoroutine("EnableCube");
    }
    IEnumerator EnableCube()
    {
        yield return new WaitForSeconds(0.01f);
        gameObjects[pivot++].SetActive(true);
        if (pivot == 500) pivot = 0;
        StartCoroutine("EnableCube");
    }

}

 

위 코드는 시작할 때 풀을 구성할 500개의 객체를 미리 만들고0.01초 마다 하나씩 활성화시키는 코드입니다.

pivot변수로 마지막 객체까지 활성화시켰다면 처음 객체로 다시 돌아가 활성화 시키기 시작합니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingCube_Pool : MonoBehaviour
{
    private Vector3 direction;
    private void OnEnable()
    {
        transform.position = new Vector3(0, 0, 0);
        StartCoroutine("DisableObject");

    }
    void Start()
    {
        direction = new Vector3(Random.Range(-0.1f, 0.1f), Random.Range(-0.1f, 0.1f), Random.Range(-0.1f, 0.1f));
        StartCoroutine("DisableObject");
    }
    private void FixedUpdate()
    {
        transform.position += direction;
    }
    IEnumerator DisableObject()
    {
        yield return new WaitForSeconds(4.0f);
        gameObject.SetActive(false);
    }
}

위 코드는 오브젝트를 랜덤한 방향으로 움직이고, 4초 후에 비활성화시키는 코드입니다.

 

실행을 시키면?

 

180~200 FPS정도의 성능을 보여줍니다. 간혹 200 FPS까지의 성능도 보여줍니다.

 

둘을 동시에 비교해봅시다.

 

생성 파괴 방식
오브젝트 풀링

단순한 Cube 오브젝트로 테스트를 진행해보았는데 이정도의 차이가 발생합니다.

 

실제 게임에서의 성능차는 엄청납니다.

 

몇개~ 몇십 개까지의 오브젝트 숫자 정도에서는 두 방식 모두 큰 차이가 없을 것입니다.

 

하지만, 오브젝트의 개수가 많아질수록 두 방식에 따른 차이는 점점 벌어집니다.

 

물론, 오브젝트 풀링 방식은 메모리를 할당 해두기 때문에 메모리를 희생하여 성능을 높이는 것이지만, 실시간으로 작동하는 게임에서의 프레임 차이는 무시할 수 없기 때문에 오브젝트 풀링 방식이 선호 됩니다.