티스토리 뷰
Object Pool은 사전에 미리 만들어둔 오브젝트를 불러오면서, 다 쓰면 재활용 하는 구성의 디자인 패턴을 말한다.
일반적으로 인스턴스를 생성하고 파괴하는 것 보다 재활용하는 것이 더 시스템 부담이 적이 때문에 사용하는 구조로, 자료구조를 이용해서 구성하거나 유니티 상에서 지원하는 Object Pool을 사용해서 제작한다.
다만, 어떤 자료구조로 하는가에 따라서 장단점이 다르기 때문에, 이에 대해서 정리하고자 한다.
우선, 본 게시글에서 설명할 Object Pool의 종류는 다음과 같다.
- Queue를 이용한 Object Pool
- Stack을 이용한 Object Pool
- List를 이용한 Object Pool
- UnityEngine.ObjectPool을 이용한 Object Pool
Queue를 이용한 Object Pool
public class ObjectPoolQueue : MonoBehaviour
{
Queue<GameObject> pool = new Queue<GameObject>();
int poolAmount;
private void Start()
{
for(int i = 0; i < poolAmount; i++)
pool.Enqueue(CreatePoolObject());
}
public GameObject GetObject()
{
GameObject returnObj = pool.Dequeue();
pool.Enqueue(returnObj);
return returnObj;
}
}
가장 간단한 형태의 Object Pool이다. 그런 만큼 활용도는 가장 낮은 형태.
그래도 일단 만들기만 하면 반환부 없이 독립적으로 운용이 되기 때문에, 슈팅게임의 플레이어 탄막 같이 복잡한 구성이 필요없는 경우에 유용하다.
대신에 반환부가 없고, 오브젝트가 내부에서 순환을 도는 구조의 특성 상 '사용 중인데도 가져가는' 상황이 발생할 가능성이 높다. 때문에 사용량이 일정한 요소에 적합한 형태.
Stack을 이용한 Object Pool
public class ObjectPoolStack : MonoBehaviour
{
Stack<GameObject> pool = new Stack<GameObject>();
int poolAmount;
private void Awake()
{
for(int i = 0; i < poolAmount; i++)
pool.push(CreatePoolObject());
}
public GameObject GetPool()
{
if(pool.Count <= 0)
pool.push(CreatePoolObject());
return pool.pop();
}
public void ReleasePool(GameObject releaseObj)
{
pool.push(releaseObj);
}
}
Queue와는 달리 반환부가 생겼다. 당연한 것이, 선입후출인 Stack으로 반환부 없이 돌았다가는 첫 항목 만 계속 사용될테니까. 때문에 Pool에서 나가는 항목들은 아예 Pool에서 빼낸 다음에 반환부로 다시 받아서 채우는 방식으로 구성되어 있다.
이 방식의 특징은 반환부가 들어온 항목은 무조건 넣는 방식이다 보니 다른 종류의 GameObject를 받아서 Pool에 넣어버릴 가능성이 있다는 것. 이걸 활용해서 무엇을 만들지는 각자의 역량이겠지만, 일반적으로 Pool은 동일 항목 제어용으로 쓰이는 탓에 오히려 유의해야 하는 점이라고 봐야한다.
다르게 말하면, '이걸 고려할 필요가 없는 경우'에는 매우 유용하다. 대표적으로 플랫포밍 게임의 장해물 발사기.
여담으로, Queue와는 달리, 부족하면 새로 생성해서 보내주고, 그렇게 받아오는 것들도 다시 Stack에 추가되는데, 이후 코드 전개를 추가로 해서 이를 막을 수도 있고, 반대로 초과분 만 날려버리는 것도 가능하다.
List를 이용한 object Pool
public class ObjectPoolList : MonoBehaviour
{
List<GameObject> pool = new List<GameObject>();
int poolAmount;
int poolIndex = 0;
private void Awake()
{
for(int i = 0; i < poolAmount; i++)
pool.Add(CreatePoolObject());
}
public GameObject GetPool()
{
if(poolIndex >= pool.Count)
pool.Add(CreatePoolObject());
GameObject resultObj = pool[poolIndex];
poolIndex++;
return resultObj;
}
public GameObject ReleasePool(GameObject releaseObj)
{
int targetIndex = pool.FindIndex(idx => ReferenceEquals(releaseObj, idx));
GameObject targetObj = pool[targetIndex];
pool.Add(targetObj);
pool.RemoveAt(targetIndex);
}
}
내부 데이터의 순번을 참고 가능한 만큼, 가장 활용 폭이 넓은 형태. 정말로 코딩하는 방법에 따라서 무궁무진하게 종류가 확장된다.
본문에 작성한 것은 그 중에서 List의 '위치에 관계 없이 확인이 가능하다'는 특성을 활용한 구조로, Pool에서 사용 중인 항목도 보유하고 있다가 반환할 때에 재배치 하는 구성으로 되어 있다.
이렇게 구성하면 Pool의 갯수도 유지할 수 있는 데다가, 본문에는 작성되어 있지 않지만, 현재 Pool의 구성 인원이 아닌 항목에 대한 방어 코드를 작성할 수 있기 때문에, 이래저래 고려해 볼 것이 많은 형태.
대신 Pool에서 받아올 때에도 현재 index에 해당하는 변수를 더해줘야 하고, 반환 받을 때에도 주소 비교 연산이 추가로 들어가는 등, 다른 형태 보다 시스템 부하가 높은 편이다.
물론, 반복형으로 부하가 생기는 것은 아니기 때문에 무시해도 큰 문제 없다.
UnityEngine.ObjectPool을 이용한 Object Pool
public class ObjectPool_SetDestroy_ObjectPool : MonoBehaviour
{
private ObjectPool<GameObject> pool;
private int minSize; // 초기 풀 크기
private int maxSize; // 풀 최대 크기
private bool isDuplicationCheckOn; // 중복 검사 여부
void Awake()
{
pool = new ObjectPool<GameObject>
(
createFunc: CreateObject,
actionOnGet: OnTakeFromPool,
actionOnRelease: OnReturnedToPool,
actionOnDestroy: OnDestroyPoolObject,
collectionCheck: isDuplicationCheckOn,
defaultCapacity: minSize,
maxSize: maxSize
);
}
// 풀 내부 요소 생성 함수.
private GameObject CreateObject()
{
return new GameObject();
}
// 풀에서 받아오는 함수.
public GameObject GetObject()
{
return pool.Get();
}
// 풀에 반환하는 함수.
public void ReleaseObject(GameObject obj)
{
pool.Release(obj);
}
// 풀에 반환(최대치 이하)될 경우의 동작 [pool에서의 동작이 아님에 주의!]
void OnReturnedToPool(GameObject obj)
{
obj.SetActive(false);
}
// 풀에서 대여 될 경우의 동작 [pool에서의 동작이 아님에 주의!]
void OnTakeFromPool(GameObject obj)
{
obj.SetActive(true);
}
// 풀에 반환(최대치 초과)될 경우의 동작 [pool에서의 동작이 아님에 주의!]
void OnDestroyPoolObject(GameObject obj)
{
Destroy(obj);
}
}
Unity에서 지원하는 Object Pool이다.
완벽하게 고정된 형태이기 때문에 응용해서 특이한 구성을 만든다던지 하는 것은 무리지만, 반대로 Pool에 해당하는 행동 별로 해당 Object에 함수를 적용할 수 있어서 기능 제작 용으로는 가장 편한 형태.
상술한 구성들은 다양한 기능으로 변형할 수 있다는 게 장점이라면, 이 쪽은 '어떻게 작성할 지가 명확하다'는 것이 장점이다.
참고로, 원래의 인수 명으로는 기능을 이해하기가 어려워서 일부로 함수나 변수 명을 조금 다르게 작성하였다.
양쪽을 모두 참고하고, 해당 요소 위나 옆에 적어놓은 주석을 참고하면 각 요소가 어떤 역할을 하는 지 알 수 있을 것이다.
Object Pool은 대량 생산이 될 수 있는 요소 전반에서 유효하게 활용할 수 있는 디자인 패턴이다.
특히 탄막 슈팅이나 타워 디펜스 처럼 동일 항목이 대량으로 나오는 경우에는 더더욱 그러하다.
물론, 이런 경우 이외에도 쓰임직 한 것이 Object Pool이니, 어떤 형태던 능숙하게 만들 수 있도록 대비해 두는 것이 좋을 거 같다.
'스파르타 내일배움캠프 > Today I Learned' 카테고리의 다른 글
Today I Learned - Day 31 [움직이는 플랫폼과 유닛의 동시이동] (0) | 2024.10.28 |
---|---|
Today I Learned - Day30 [확장 메서드] (0) | 2024.10.25 |
Today I Learned - Day 28 [Raycast] (1) | 2024.10.23 |
Today I Learned - Day 27 [제너릭 싱글톤] (1) | 2024.10.22 |
Today I Learned - Day 26 [AddComponent] (0) | 2024.10.21 |