본문 바로가기

Unity

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

반응형

오브젝트 풀링은 유니티에서 자주 사용되는 오브젝트를 생성과 파괴가 아닌 리스트(List)나 큐(Queue) 등에 저장해 두고 불러와 사용하고 사용 후엔 다시 반납하는 역할을 합니다.

 

유니티에서 ObjectPool이란 이름의 빈 오브젝트를 하나 만들고 아래 ObjectPool.cs를 넣어 줍니다.

[System.Serializable]
public class ObjectInfo {
    public string objectName;
    public GameObject perfab;
    public int count;
}

public class ObjectPool : MonoBehaviour
{
    public static ObjectPool instance;

    [SerializeField]
    ObjectInfo[] objectInfos = null;

    [Header("오브젝트 풀의 위치")]
    [SerializeField]
    Transform tfPoolParent;

    public List<Queue<GameObject>> objectPoolList;

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

    void Start() {
        objectPoolList = new List<Queue<GameObject>>();
        ObjectPoolState();
    }

    void ObjectPoolState() {
        if (objectInfos != null) {
            for (int i = 0; i < objectInfos.Length; i++) {
                objectPoolList.Add(InsertQueue(objectInfos[i]));
            }
        }
    }

    Queue<GameObject> InsertQueue(ObjectInfo perfab_objectInfo) {
        Queue<GameObject> test_queue = new Queue<GameObject>();

        for (int i = 0; i < perfab_objectInfo.count; i++) {
            GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
            objectClone.SetActive(false);
            objectClone.transform.SetParent(tfPoolParent);
            test_queue.Enqueue(objectClone);
        }

        return test_queue;
    }
}

 

[System.Serializable]
public class ObjectInfo {
    public string objectName;
    public GameObject perfab;
    public int count;
}

오브젝트의 등록 정보를 관리합니다.

objectName -> 오브젝트의 이름

perfab -> 사용될 프리 펩 오브젝트

count -> 생성될 오브젝트의 수량

 

public static ObjectPool instance;

오브젝트 풀은 어디에서나 사용될 수 있도록 싱글톤 패턴을 줍니다.

 

[SerializeField]
ObjectInfo[] objectInfos = null;

등록될 오브젝트들의 목록을 여기에 저장합니다. (아래 이미지 참조)

 

[SerializeField]
Transform tfPoolParent;

생성된 클론(Clone) 오브젝트들이 Hierarchy창에서 위치할 곳을 정해 줍니다.

 

public List<Queue<GameObject>> objectPoolList;

위에서 설명된 objectInfos에 등록된 오브젝트들의 생성 수가 각각 다릅니다. 이 모든 걸 objectPoolList에 담아 둡니다.

여러 곳에서 오브젝트를 불러올 때 objectPoolList를 사용하게 됩니다.

 

void ObjectPoolState() {
    if (objectInfos != null) {
        for (int i = 0; i < objectInfos.Length; i++) {
            objectPoolList.Add(InsertQueue(objectInfos[i]));
        }
    }
}

등록된 오브젝트의 목록을 List에 저장합니다.

 

Queue<GameObject> InsertQueue(ObjectInfo perfab_objectInfo) {
    Queue<GameObject> test_queue = new Queue<GameObject>();

    for (int i = 0; i < perfab_objectInfo.count; i++) {
        GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
        objectClone.SetActive(false);
        objectClone.transform.SetParent(tfPoolParent);
        test_queue.Enqueue(objectClone);
     }

    return test_queue;
}

이곳에서 등록된 오브젝트 목록의 수만큼 클론(Clone) 오브젝트를 생성 후 바로 위 소스의 objectPoolList로 넘겨줍니다.

 

GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;

for문이 objectInfo의 count만큼 돌면서 objectClone을 생성 후

objectClone.SetActive(false)로 비활성화해 줍니다.

objectClone.transform.SetParent(tfPoolParent) 생성된 클론(Clone) 오브젝트의 부모 오브젝트를 지정해 줍니다.

이러면 Hierarchy창의 부모 오브젝트 밑으로 들어가게 됩니다. (아래 그림 참조)

test_queue.Enqueue(objectClone);

이렇게 만들어진 클론(Clone) 오브젝트를 큐(Queue)에 저장합니다.

for문만큼 돌며 만들어진 test_queue의 데이터를

return test_queue로 List<Queue<GameObject>> objectPoolList에 저장합니다.

 

이제 유니티로 가서 빈 오브젝트 3개를 만들고 이름을 각각 A, B, C로 하며 Add Component에서 Sprite Renderer를 추가하고 기본 사각 이미지와 컬러를 따로 지정해 줍니다 그런 후 프리펩(PerFab)으로 만든 후 Hierarchy창에서 삭제합니다. (아래 이미지 참조)

이제 맨 처음 만들어 둔 ObjectPool을 선택해 오브젝트를 등록해 줍니다. (아래 이미지 참조)

위 이미지를 보시면 A오브젝트는 5개를 만들며 B는 2개 C는 3개입니다.

게임을 플레이해 보시면

위 이미지와 같이 클론(Clone) 오브젝트가 수량에 맞게 생성되었으며 ObjectPool 밑으로 들어가 있습니다.

A오브젝트는 objectPoolList [0]에 들어가 있으며

B오브젝트는 objectPoolList [1]에 C오브젝트는 objectPoolList [2]에 저장되어 있습니다.

objectInfos의 순서대로 objectPoolList에 저장됩니다.

 

이제 마지막으로 이렇게 만들어진 오브젝트를 사용하면 됩니다.

플레이어가 스페이스(Space)를 누르면 A 클론(Clone) 오브젝트를 사용 후 반납까지 알아보겠습니다.

A 클론(Clone) 오브젝트를 총알이라 가정하고 보시면 편하겠네요.

 

Player.cs에서 

void Update() {
    if (Input.GetKeyDown(KeyCode.Space)) {
        GameObject objTest = ObjectPool.instance.objectPoolList[0].Dequeue();
        objTest.SetActive(true);
        float pointX = Random.Range(- 1f, 1f);
        objTest.transform.position = new Vector2(gameObject.transform.position.x + pointX, 
           gameObject.transform.position.y);
        StartCoroutine(ObjReturn(objTest));
    }
}

IEnumerator ObjReturn(GameObject _obj) {
    yield return new WaitForSeconds(2);
    ObjectPool.instance.objectPoolList[0].Enqueue(_obj);
    _obj.SetActive(false);
}

스페이스(Space) 키를 누르면

objTest = ObjectPool.instance.objectPoolList[0].Dequeue() -> objTest에 objectPoolList[0] 오브젝트 하나를 Dequeue() 가져옵니다. 

objTest.SetActive(true) 위쪽 클론(Clone)을 만들면서 비활성화 한 오브젝트를 다시 활성화시켜 줍니다.

 

float pointX = Random.Range(- 1f, 1f);
objTest.transform.position = new Vector2(gameObject.transform.position.x + pointX, 
     gameObject.transform.position.y);
StartCoroutine(ObjReturn(objTest));

오브젝트의 위치를 잡아 준 후 코루틴 호출

 

IEnumerator ObjReturn(GameObject _obj) {
    yield return new WaitForSeconds(2);
    ObjectPool.instance.objectPoolList[0].Enqueue(_obj);
    _obj.SetActive(false);
}

사용된 클론(Clone) 오브젝트를 2초 후 다시 반납합니다.

큐(Queue)는 사용 후 Enqueue(_obj)로 반납해 주셔야 됩니다.

objectPoolList[0] 가져온 리스트의 Index 위치와 반납될 리스트의 위치가 같아야 합니다.

그런 후  _obj.SetActive(false)로 오브젝트를 비활성화해 주시면 됩니다.

 

게임을 플레이 후 화면에 보시면

스페이스(Space) 키를 누를 때마다 Object Pool 하위의 A(Clone) 오브젝트가 순차적으로 생성되며 2초 후 재 사용을 위해 순차적으로 사라지는 걸 확인할 수 있습니다.

 

마지막으로 오브젝트 풀 코드를 보시면 싱글톤 패턴을 사용했지만 objectPoolList만 필요할 뿐 나머진 외부에서 사용할 일이 없습니다.

이럴 땐 싱글톤 패턴이 적용된 예로 GamaManager가 싱글톤 패턴이 적용되어 있다면 여기에 델리게이트를 선언하여 objectPoolList를 연결해 사용하면  더 좋을 겁니다.

델리게이트에 연결된 코드도 한번 보시고 편한 걸로 사용해 보시기 바랍니다.

 

위에서도 예로 들었지만 GamaManager에 싱글톤 패턴이 적용되어 있습니다.

using System;

public class GameManager : MonoBehaviour {
    public Func<int, GameObject> objectPool; 
    public Action<int, GameObject> returnPool;
}

Func와 Action을 사용하기 위해 using System을 선언합니다.

Func<int, GameObject> int를 넘겨주며 GameIObject를 받습니다.

Action<int, GameObject> int와 GameIObject 넘겨줍니다.

 

[System.Serializable]
public class ObjectInfo {
    public string objectName;
    public GameObject perfab;
    public int count;
}

public class ObjectPool : MonoBehaviour
{
    [SerializeField]
    ObjectInfo[] objectInfos = null;

    [Header("오브젝트 풀의 위치")]
    [SerializeField]
    Transform tfPoolParent;

    List<Queue<GameObject>> objectPoolList;

    void Start() {
        GameManager.instance.objectPool += PoolList;
        GameManager.instance.returnPool += ReturnPool;
        
        objectPoolList = new List<Queue<GameObject>>();
        ObjectPoolState();
    }
    
    //* Func 연결
    GameObject PoolList(int _index) {
        return objectPoolList[_index].Dequeue();
    }

    //* Action 연결
    void ReturnPool(int _index, GameObject _poolObject) {
        objectPoolList[_index].Enqueue(_poolObject);
    }

    void ObjectPoolState() {
        if (objectInfos != null) {
            for (int i = 0; i < objectInfos.Length; i++) {
                objectPoolList.Add(InsertQueue(objectInfos[i]));
            }
        }
    }

    Queue<GameObject> InsertQueue(ObjectInfo perfab_objectInfo) {
        Queue<GameObject> test_queue = new Queue<GameObject>();

        for (int i = 0; i < perfab_objectInfo.count; i++) {
            GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
            objectClone.SetActive(false);
            objectClone.transform.SetParent(tfPoolParent);
            test_queue.Enqueue(objectClone);
        }

        return test_queue;
    }
}

ObjectPool.cs에 기존 싱글톤 패턴은 지웠습니다.

Start()에

GameManager.instance.objectPool += PoolList;
GameManager.instance.returnPool += ReturnPool;

PoolList()와 ReturnPool() 함수를 델리게이트에 연결해 줍니다.

 

//* Func 연결

GameObject PoolList(int _index) {
     return objectPoolList[_index].Dequeue();
}

PoolList는 가져다 사용할때 사용합니다.

 

//* Action 연결

void ReturnPool(int _index, GameObject _poolObject) {
     objectPoolList[_index].Enqueue(_poolObject);
}

ReturnPool은 반납할 때 사용합니다.

 

사용하는 방법은 

 

 GameObject damagePrefab = GameManager.instance.objectPool.Invoke(0);

GameManager.instance.objectPool.Invoke(0) -> objectPoolList[0] 0번 리스트에 저장된 오브젝트를 가져옵니다.

 

사용 후 반납은 

 

GameManager.instance.returnPool.Invoke(0, this.gameObject);

objectPoolList[0] 0번 리스트에 오브젝트를 Enqueue 합니다.

간편하긴 하지만 델리게이트가 조금 어렵다 생각되시면 싱글톤 패턴을 사용하시면 됩니다.

 

이상 오브젝트 풀링(Object pooling)에 대해 알아봤습니다.

 

 

반응형