지금까지 오브젝트 풀링에 대해서 강의에서 접한 적도 있고 TIL을 작성하면서 언급한 적도 있지만, 제대로 사용해본 적은 없기 때문에 이번 기회에 오브젝트 풀링을 활용해보려고 한다.
오브젝트를 동적으로 생성 및 파괴를 하는 것이 아니라 미리 생성해두고 필요할 때만 꺼내서 쓰는 방식이다. 예를 들어 게임에서 사용할 오브젝트를 로드 단계에서 일괄적으로 생성하여 오브젝트 풀에 저장해두고, 필요할 때 오브젝트 풀에서 꺼내서 사용하고 필요없을 때 오브젝트 풀에 넣는 방식이다. 이렇게 처리하면 게임 진행 단계에서의 GC.Alloc을 피할 수 있어 최적화에 유리하다.
하지만, 오브젝트 풀링을 사용하면 기본적으로 오브젝트 풀에 오브젝트들을 미리 생성해서 갖고 있기 때문에 계속해서 메모리를 사용할 수 밖에 없다. 따라서 오브젝트 풀의 크기를 작지도 크지도 않게 적당한 크기로 만드는 것이 오브젝트 풀링의 핵심이다.
먼저 풀링할 오브젝트가 필수로 가지고 있어야하는 기능을 인터페이스로 정의하여 상속받을 수 있게 만든다.
using System; using UnityEngine; public interface IPoolable { void intialize(Action<GameObject> returnAction); void OnSpawn(); void OnDespawn(); }
하나 씩 어떤 기능을 수행하는 지 간단하게 설명하자면 다음과 같다.
- void initialize(Action returnAction)
: 오브젝트를 미리 생성해 두기 위해 필요한 초기 작업- void OnSpawn()
: 오브젝트 활성화 시 호출 (사용시)- void OnDespawn();
: 오브젝트 비활성화 시 호출 (반환시)
이제 IPoolable 인터페이스를 토대로 오브젝트 풀을 만들어보자
<ObjectPoolManager 코드 전문>
using System.Collections.Generic; using UnityEngine; public class ObjectPoolManager : MonoBehaviour { public GameObject[] prefabs; private Dictionary<int, Queue<GameObject>> pools = new Dictionary<int, Queue<GameObject>>(); public static ObjectPoolManager Instance { get; private set; } private void Awake() { Instance = this; for (int i = 0; i < prefabs.Length; i++) { pools[i] = new Queue<GameObject>(); } } public GameObject GetObject(int prefabIndex, Vector3 position, Quaternion rotation) { if (!pools.ContainsKey(prefabIndex)) { Debug.Log($"프리팹 인덱스 {prefabIndex}에 대한 풀이 존재하지 않습니다."); return null; } GameObject obj; if (pools[prefabIndex].Count > 0) { obj = pools[prefabIndex].Dequeue(); } else { obj = Instantiate(prefabs[prefabIndex]); obj.GetComponent<IPoolable>()?.intialize(o => ReturnObject(prefabIndex, o)); } obj.transform.SetPositionAndRotation(position, rotation); obj.SetActive(true); obj.GetComponent<IPoolable>().OnSpawn(); return obj; } public void ReturnObject(int prefabIndex, GameObject obj) { if (!pools.ContainsKey(prefabIndex)) { Destroy(obj); return; } obj.SetActive(false); pools[prefabIndex].Enqueue(obj); } }
이렇게 만든 오브젝트 풀을 위에서부터 하나씩 살펴보자.
public GameObject[] prefabs; private Dictionary<int, Queue<GameObject>> pools = new Dictionary<int, Queue<GameObject>>(); public static ObjectPoolManager Instance { get; private set; } private void Awake() { Instance = this; for (int i = 0; i < prefabs.Length; i++) { pools[i] = new Queue<GameObject>(); } }
먼저 오브젝트 풀링에 사용할 프리팹을 받아서 딕셔너리로 오브젝트 풀들을 미리 생성해둔다. 그리고 오브젝트를 생성하는 코드에서 접근해야하기 때문에 싱글턴 패턴으로 구현하는 것이 좋다.
public GameObject GetObject(int prefabIndex, Vector3 position, Quaternion rotation) { if (!pools.ContainsKey(prefabIndex)) { Debug.Log($"프리팹 인덱스 {prefabIndex}에 대한 풀이 존재하지 않습니다."); return null; } GameObject obj; if (pools[prefabIndex].Count > 0) { obj = pools[prefabIndex].Dequeue(); } else { obj = Instantiate(prefabs[prefabIndex]); obj.GetComponent<IPoolable>()?.intialize(o => ReturnObject(prefabIndex, o)); } obj.transform.SetPositionAndRotation(position, rotation); obj.SetActive(true); obj.GetComponent<IPoolable>().OnSpawn(); return obj; }
GetObject 함수는 간단하게 설명하면 오브젝트 풀에서 가져올 프리팹, 생성할 위치, 생성할 회전 값들을 매개변수로 받아서 오브젝트 풀에 존재하면 오브젝트 풀에서 꺼내오고, 없으면 instantiate를 통해 생성하는 것이다.
public void ReturnObject(int prefabIndex, GameObject obj) { if (!pools.ContainsKey(prefabIndex)) { Destroy(obj); return; } obj.SetActive(false); pools[prefabIndex].Enqueue(obj); }
마지막으로 ReturnObject 함수는 다 사용한 오브젝트를 다시 오브젝트 풀에 넣어 두는 함수이다.
이제 위에서 만든 오브젝트 풀을 이용해서 오브젝트 풀에서 필요한 오브젝트를 꺼내서 사용하고 다시 집어 넣는 것을 테스트 할 것이다.
using System; using UnityEngine; public class Capsule : MonoBehaviour,IPoolable { private Action<GameObject> returnToPool; private void OnEnable() { Invoke("OnDespawn", 2f); } public void intialize(Action<GameObject> returnAction) { returnToPool = returnAction; } public void OnDespawn() { Debug.Log("디스폰"); returnToPool?.Invoke(gameObject); } public void OnSpawn() { Debug.Log("스폰"); } }
생성할 캡슐 프리팹은 IPoolable을 상속받아서 다음과 같이 만들었다. 생성된 캡슐은 자동으로 2초뒤에 디스폰되도록 만들었다.
using UnityEngine; public class ObjectSpawnManager : MonoBehaviour { private void Update() { if (Input.GetKeyDown(KeyCode.A)) { ObjectPoolManager.Instance.GetObject(0, new Vector3(0, 0, 0), Quaternion.identity); } } }
그리고 캡슐을 생성하기 위해 간단하게 키보드 A를 누르면 오브젝트를 가져오는 코드를 사용했다. 0번 프리팹에는 미리 만든 Capsule을 넣어놨다. (A를 누르면 캡슐이 0,0,0 위치에 생성됨)
| 캡슐 스폰 및 디스폰 테스트 영상 |
|---|
![]() |