300x250

안녕하세요.

 

오늘은 오브젝트 풀링에 대해서 알아볼게요.

 

1. 왜 오브젝트 풀링을 써야 할까요?

 

유니티에서 오브젝트를 생성하기 위해서는 Instantiate를 사용하고 삭제할 때는 Destroy를 사용해요.

하지만 Instantiate, Destroy 이 두 함수는 상당히 비용을 크게 먹는다는 것을 구글링을 해보면 알 수 있어요.

Instantiate(오브젝트 생성)은 메모리를 새로 할당하고 리소스를 로드하는 등의 초기화 과정이 필요하고, Destroy(오브젝트 파괴)는 파괴 이후에 발생하는 가비지 컬렉팅으로 인한 프레임 드랍이 발생할 수 있어요.

 

그렇다면 플레이어의 총알과 같이 자주 생성되고 삭제되는 오브젝트들이 있을 경우 치명적일 수 있는 거죠. 이때 사용할 수 있는 것이 오브젝트 풀링이에요. 자주 사용하는 오브젝트를 미리 생성해 놓고 이걸 사용할 때마다 새로 생성 삭제 하는 것 이 아닌 사용할 때는 오브젝트풀한테 빌려서 사용하고 삭제할 때는 오브젝트풀한테 돌려줌으로써 단순하게 오브젝트를 활성화 비활성화 만 하는 개념이에요.

 

 

유니티에서 오브젝트 풀링을 처음부터 지원하진 않았어요. 그렇기에 오브젝트 풀링을 구현하는 방법이 사람마다 달랐으며 성능도 달랐을 거라고 생각돼요. 하지만 지금은 유니티에서 오브젝트 풀링을 공식적으로 지원해요. 이 글은 유니티에서 공식적으로 제공하는 오브젝트 풀링 사용방법에 대해서 정리하는 글이에요.

 

 

2. 기본 프로젝트 세팅

 

간단하게 플레이어가 움직이고 플레이어가 총알을 발사하는 프로젝트를 만들어줘요.

 

Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed = 1f;
    public Transform bulletSpawnPoint;
    public GameObject bulletPrefab;

    void Update()
    {

        if (Input.GetKeyDown(KeyCode.Space))
        {
            var bulletGo = Instantiate(bulletPrefab);

            bulletGo.transform.position = this.bulletSpawnPoint.position;
        }

        // 가로 이동 반환값 : LeftArrow = -1 RightArrow = 1
        var h = Input.GetAxisRaw("Horizontal");
        // 세로 이동 반환값 : DownArrow = -1 UpArrow = 1        
        var v = Input.GetAxisRaw("Vertical");

        //단위 벡터 (크기가 1인 벡터)
        var dir = new Vector3(h, v, 0).normalized;

        this.transform.Translate(dir * this.speed * Time.deltaTime);
    }
}

플레이어가 이동하면서 space키를 누르면 총알을 발사하는 간단한 코드예요. 

 

 

Bullet.cs

using UnityEngine;

public class Bullet : MonoBehaviour
{
    public float speed = 5f;

    void Update()
    {

        // 총알이 많이 날라가면 삭제 해주기
        if (this.transform.position.y > 5)
        {
            Destroy(this.gameObject);
        }

        this.transform.Translate(Vector3.up * this.speed * Time.deltaTime);
    }
}

총알이 생성되고 나서 위로 계속 올라가다가 일정 위치 이상으로 올라가면 삭제되는 간단한 코드예요.

 

 

 

간단하게 Circle 오브젝트에 빨간색을 넣어 만든 Bullet오브젝트는 프리팹으로 만들고 씬에선 삭제해 줘요

 

다음으로 Square 오브젝트에 검은색을 넣고 총알이 스폰될 위치를 잡아주고 이 위치와 총알 프리팹을 어싸인해 줘요.

 

 

 

 

3. 유니티 오브젝트 풀링 사용하기

이제 이 기존 코드를 유니티에서 제공하는 오브젝트 풀링을 사용하는 방식으로 바꿔볼게요.

 

바꾸기 전 오브젝트 풀링을 구현해 줘요.

ObjectPoolManager.cs

using UnityEngine;
using UnityEngine.Pool;

public class ObjectPoolManager : MonoBehaviour
{

    public static ObjectPoolManager instance;

    public int defaultCapacity = 10;
    public int maxPoolSize = 15;
    public GameObject bulletPrefab;

    public IObjectPool<GameObject> Pool { get; private set; }

    private void Awake()
    {
        if (instance == null)
            instance = this;
        else
            Destroy(this.gameObject);


        Init();
    }

    private void Init()
    {
        Pool = new ObjectPool<GameObject>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool,
        OnDestroyPoolObject, true, defaultCapacity, maxPoolSize);

        // 미리 오브젝트 생성 해놓기
        for (int i = 0; i < defaultCapacity; i++)
        {
            Bullet bullet = CreatePooledItem().GetComponent<Bullet>();
            bullet.Pool.Release(bullet.gameObject);
        }
    }

    // 생성
    private GameObject CreatePooledItem()
    {
        GameObject poolGo = Instantiate(bulletPrefab);
        poolGo.GetComponent<Bullet>().Pool = this.Pool;
        return poolGo;
    }

    // 사용
    private void OnTakeFromPool(GameObject poolGo)
    {
        poolGo.SetActive(true);
    }

    // 반환
    private void OnReturnedToPool(GameObject poolGo)
    {
        poolGo.SetActive(false);
    }

    // 삭제
    private void OnDestroyPoolObject(GameObject poolGo)
    {
        Destroy(poolGo);
    }
}

 

 

이제 Bullet 오브젝트는 ObjectPoolManager가 생성하고 사용할 때마다 ObjectPoolManager가 관리하고 있는 Bullet 오브젝트를 받아서 사용할 수 있는 코드예요.

 

기존 Bullet 코드를 바꿔줘요.

Bullet.cs

using UnityEngine;
using UnityEngine.Pool;

public class Bullet : MonoBehaviour
{
    public IObjectPool<GameObject> Pool { get; set; }
    public float speed = 5f;

    void Update()
    {

        // 총알이 많이 날라가면 삭제 해주기
        if (this.transform.position.y > 5)
        {
            // 이제 자신이 Destroy를 하지 않는다.
            //Destroy(this.gameObject);

            // 오브젝트 풀에 반환
            Pool.Release(this.gameObject);
        }

        this.transform.Translate(Vector3.up * this.speed * Time.deltaTime);
    }
}

 

Player도 바꿔줘요.

using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed = 1f;
    public Transform bulletSpawnPoint;

    void Update()
    {

        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 총알생성을 플레이어가 하지 않는다.
            //var bulletGo = Instantiate(bulletPrefab);

            // 오브젝트풀 에서 빌려오기
            var bulletGo = ObjectPoolManager.instance.Pool.Get();

            bulletGo.transform.position = this.bulletSpawnPoint.position;
        }

        // 가로 이동 반환값 : LeftArrow = -1 RightArrow = 1
        var h = Input.GetAxisRaw("Horizontal");
        // 세로 이동 반환값 : DownArrow = -1 UpArrow = 1        
        var v = Input.GetAxisRaw("Vertical");

        //단위 벡터 (크기가 1인 벡터)
        var dir = new Vector3(h, v, 0).normalized;

        this.transform.Translate(dir * this.speed * Time.deltaTime);
    }
}

 

이제 프로젝트를 시작하면 오브젝트를 미리 최소 정해진 수만큼 만들어 놓고 플레이어가 요청할떄마다 대여해서 사용할 거예요.

 

 

 

4. 오브젝트 여러 개 관리하기

 

지금까지는 총알 1개만 관리하는 거였다면 여러 종류의 오브젝트를 관리하는 오브젝트풀을 관리할 수 있어야 실용성이 있을 거예요.

 

그래서 저는 오브젝트풀을 관리하는 딕셔너리를 선언해서 외부에서는 오브젝트풀매니저에게 오브젝트를 요청만 하면 매니저가 해당 오브젝트에 해당하는 오브젝트풀에 접근해서 오브젝트를 받아서 넘겨주는 방식으로 구현했어요.

 

먼저 오브젝트풀을 관리하기 위해선 오브젝트풀에서 사용할 프리팹과 몇 개를 생성할지 count를 알아야 해요. 이걸 딕셔너리로 관리할 거 기 때문에 key값으로 사용할 오브젝트네임까지 받는 클래스를 선언했어요.

    [System.Serializable]
    private class ObjectInfo
    {
        // 오브젝트 이름
        public string objectName;
        // 오브젝트 풀에서 관리할 오브젝트
        public GameObject perfab;
        // 몇개를 미리 생성 해놓을건지
        public int count;
    }

 

이렇게 하고 오브젝트 매니저에게 할당하면 아래처럼 오브젝트풀에 등록할 오브젝트들을 등록할 수 있어요.

 

그리고 저는 딕셔너리를 2개와 String 변수 하나를 선언했는데

    // 생성할 오브젝트의 key값지정을 위한 변수
    private string objectName;
    
    // 오브젝트풀들을 관리할 딕셔너리
    private Dictionary<string, IObjectPool<GameObject>> 
    ojbectPoolDic = new Dictionary<string, IObjectPool<GameObject>>();

    // 오브젝트풀에서 오브젝트를 새로 생성할때 사용할 딕셔너리
    private Dictionary<string, GameObject> poolGoDic = new Dictionary<string, GameObject>();

 

objectPoolDic 은 오브젝트풀을 관리하기 위한 딕셔너리라면 poolGoDIc은 왜 필요하냐면요

 

    // 생성
    private GameObject CreatePooledItem()
    {
        GameObject poolGo = Instantiate(poolGoDic[objectName]);
        poolGo.GetComponent<PoolAble>().Pool = ojbectPoolDic[objectName];
        return poolGo;
    }

처음에 오브젝트풀에서 오브젝트를 생성할 때 어떤 오브젝트를 생성해야 하는지를 알 수가 없어요. 그렇기 때문에 생성해야 하는 오브젝트들을 objecgtName을 key값으로 갖게 하고 프리팹들을 저장해 놓고 풀에서 오브젝트를 새로 생성할 때 생성할 objecgtName key값에 해당하는 poolGoDic에 접근해서 오브젝트를 생성하는 거예요.

 

또한 보시면 이젠 Bullet 스크립트가 아니라 PoolAble 스크립트에 접근해서 Pool을 지정해주고 있는데 PoolAble 스크립트는 오브젝트풀을 사용할 오브젝트들이 모두 상속받게 해서 이 스크립트를 상속받은 오브젝트들만 오브젝트풀에 등록할 수 있게 했어요.

그래서 PoolAble 스크립트엔 그냥 자신이 돌려줘야 할 Pool을 저장할 프로퍼티와 자신의 게임 오브젝트를 넘겨줄 ReleaseObject 메서드만 정의를 해두면 돼요.

 

 

PoolAble.cs

using UnityEngine;
using UnityEngine.Pool;

public class PoolAble : MonoBehaviour
{
    public IObjectPool<GameObject> Pool { get; set; }

    public void ReleaseObject()
    {
        Pool.Release(gameObject);
    }
}

 

Bullet.cs

using UnityEngine;

public class Bullet : PoolAble
{
    public float speed = 5f;

    void Update()
    {
        // 총알이 많이 날라가면 삭제 해주기
        if (this.transform.position.y > 5)
        {
            // 오브젝트 풀에 반환
            ReleaseObject();
        }

        this.transform.Translate(Vector3.up * this.speed * Time.deltaTime);
    }
}

 

ObjectPoolManager.cs

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

public class ObjectPoolManager : MonoBehaviour
{
    [System.Serializable]
    private class ObjectInfo
    {
        // 오브젝트 이름
        public string objectName;
        // 오브젝트 풀에서 관리할 오브젝트
        public GameObject perfab;
        // 몇개를 미리 생성 해놓을건지
        public int count;
    }


    public static ObjectPoolManager instance;

    // 오브젝트풀 매니저 준비 완료표시
    public bool IsReady { get; private set; }

    [SerializeField]
    private ObjectInfo[] objectInfos = null;

    // 생성할 오브젝트의 key값지정을 위한 변수
    private string objectName;

    // 오브젝트풀들을 관리할 딕셔너리
    private Dictionary<string, IObjectPool<GameObject>> ojbectPoolDic = new Dictionary<string, IObjectPool<GameObject>>();

    // 오브젝트풀에서 오브젝트를 새로 생성할때 사용할 딕셔너리
    private Dictionary<string, GameObject> goDic = new Dictionary<string, GameObject>();

    private void Awake()
    {
        if (instance == null)
            instance = this;
        else
            Destroy(this.gameObject);

        Init();
    }


    private void Init()
    {
        IsReady = false;

        for (int idx = 0; idx < objectInfos.Length; idx++)
        {
            IObjectPool<GameObject> pool = new ObjectPool<GameObject>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool,
            OnDestroyPoolObject, true, objectInfos[idx].count, objectInfos[idx].count);

            if (goDic.ContainsKey(objectInfos[idx].objectName))
            {
                Debug.LogFormat("{0} 이미 등록된 오브젝트입니다.", objectInfos[idx].objectName);
                return;
            }

            goDic.Add(objectInfos[idx].objectName, objectInfos[idx].perfab);
            ojbectPoolDic.Add(objectInfos[idx].objectName, pool);

            // 미리 오브젝트 생성 해놓기
            for (int i = 0; i < objectInfos[idx].count; i++)
            {
                objectName = objectInfos[idx].objectName;
                PoolAble poolAbleGo = CreatePooledItem().GetComponent<PoolAble>();
                poolAbleGo.Pool.Release(poolAbleGo.gameObject);
            }
        }

        Debug.Log("오브젝트풀링 준비 완료");
        IsReady = true;
    }

    // 생성
    private GameObject CreatePooledItem()
    {
        GameObject poolGo = Instantiate(goDic[objectName]);
        poolGo.GetComponent<PoolAble>().Pool = ojbectPoolDic[objectName];
        return poolGo;
    }

    // 대여
    private void OnTakeFromPool(GameObject poolGo)
    {
        poolGo.SetActive(true);
    }

    // 반환
    private void OnReturnedToPool(GameObject poolGo)
    {
        poolGo.SetActive(false);
    }

    // 삭제
    private void OnDestroyPoolObject(GameObject poolGo)
    {
        Destroy(poolGo);
    }

    public GameObject GetGo(string goName)
    {
        objectName = goName;

        if (goDic.ContainsKey(goName) == false)
        {
            Debug.LogFormat("{0} 오브젝트풀에 등록되지 않은 오브젝트입니다.", goName);
            return null;
        }

        return ojbectPoolDic[goName].Get();
    }
}

 

 

 

Player.cs

using UnityEngine;

public class Player : MonoBehaviour
{
    public float speed = 1f;
    public Transform bulletSpawnPoint;

    void Update()
    {

        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            // 오브젝트풀 에서 빌려오기
            var bulletGo = ObjectPoolManager.instance.GetGo("BulletRed");

            bulletGo.transform.position = this.bulletSpawnPoint.position;
        }
        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            // 오브젝트풀 에서 빌려오기
            var bulletGo = ObjectPoolManager.instance.GetGo("BulletGreen");

            bulletGo.transform.position = this.bulletSpawnPoint.position;
        }
        if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            // 오브젝트풀 에서 빌려오기
            var bulletGo = ObjectPoolManager.instance.GetGo("BulletBlue");

            bulletGo.transform.position = this.bulletSpawnPoint.position;
        }

        // 가로 이동 반환값 : LeftArrow = -1 RightArrow = 1
        var h = Input.GetAxisRaw("Horizontal");
        // 세로 이동 반환값 : DownArrow = -1 UpArrow = 1        
        var v = Input.GetAxisRaw("Vertical");

        //단위 벡터 (크기가 1인 벡터)
        var dir = new Vector3(h, v, 0).normalized;

        this.transform.Translate(dir * this.speed * Time.deltaTime);
    }
}

 

 

5. 완성

 

이렇게 한번 만들어두면 나중엔 ObjectPoolManager.instance.GetGo(가져올 오브젝트이름)으로 접근해서 사용할 수 있어요

 

 

 

 

참고자료 :

Unity - Scripting API: ObjectPool<T0> (unity3d.com)

유니티 - 오브젝트 풀링(Object pooling) (tistory.com)

300x250