🗨 개인적인 공부 기록용으로 정리한 내용입니다. 잘못된 내용에 대한 피드백은 언제나 감사합니다 :)
⭐ 오브젝트 풀은 반복적으로 생성/소멸되는 객체(`오브젝트`)를 풀(`Pool`)에 미리 생성 및 저장해 두고 필요시 꺼내 쓰고, 사용이 끝나면 다시 반환하여 관리하는 디자인 패턴이다.
게임 개발을 하다 보면 특정 객체(`Object`)는 생성하고 소멸시키는 작업을 자주 해야 하는 상황이 생긴다.
특히 총알, 적(Enemy), 파티클 이펙트처럼 짧은 시간에 생성과 소멸이 반복되는 오브젝트들은 성능 저하의 주요 원인이 된다.
이때, 생성/소멸의 부담을 크게 줄여 성능을 최적화시키는 패턴이 바로 오브젝트 풀(Object Pool) 패턴이다.
📌 오브젝트 풀이란?
- 오브젝트를 미리 생성하여 풀(`Pool`)에 저장해 두고, 필요하면 풀에서 꺼내 쓰고, 사용이 끝나면 다시 풀로 돌려보낸다.
- 많은 수의 오브젝트를 생성하고 파괴할 때 CPU의 부담을 줄이는 최적화 기법이다.
- 로딩 시간에 오브젝트를 미리 만들어 풀에 담아두고, 런타임에는 사용하기만 하는 식으로 구현한다.
📌 오브젝트 풀을 왜 사용하는가?
오브젝트 풀은 주로 성능과 메모리 관리 최적화를 위해 사용한다.
특히 Unity에서 오브젝트 풀을 FPS, 슈팅 게임, 그리고 다양한 장르에서 성능을 최적화하는 필수적인 도구로 쓰인다.
- 성능 최적화
- 객체를 자주 생성하고 파괴하는 작업은 메모리 할당 및 해제에 따른 오버헤드가 크다. 이는 성능 저하의 주요 원인이다.
- 오브젝트 풀을 사용하면 객체를 재사용함으로써 성능을 개선할 수 있다.
- 가비지 컬렉션(GC) 부담 감소
- 빈번한 메모리 할당 및 해제는 가비지 컬렉션을 유발하여 프레임 드랍이 발생할 수 있다.
- 오브젝트 풀로 가비지 컬렉션 급증으로 인해 발생할 수 있는 밑줄 끊김 현상(Hiccup, GC spike)을 예방할 수 있다.
- 일정한 메모리 사용
- 한 번에 생성할 수 있는 오브젝트의 개수를 제한할 수 있다.
- (반대로, 생성하고 사용하지 않을 경우 메모리 낭비가 되는 단점이 있다.)
📌 오브젝트 풀의 구조와 구현
오브젝트 풀의 기본 구성 요소는 Pool, Pool Manager(관리자), Client(사용자)로 나눌 수 있다.
- Pool
- 미리 생성된 객체를 저장
- 일반적으로 `List`, `Queue`, 혹은 `Stack` 자료 구조를 사용한다. (아래 예시코드는 `Stack` 사용)
- Pool Manager(관리자)
- Pool에서 객체를 가져오거나 반환하는 역할 담당
- 필요한 경우 새로운 객체를 생성하거나 Pool에 반환
- Client(사용자)
- 오브젝트 풀에 객체를 요청해서 사용 후 반환
🔖 Object Pool
Unity에서 사용하는 기본적인 오브젝트 풀(Object Pool)의 구현 예시이다.
예시 코드 | 오브젝트 풀(Object Pool)
🌞 유니티에서 배포한 코드입니다.
/* 오브젝트 풀 */
public class ObjectPool : MonoBehaviour
{
[SerializeField] private uint initPoolSize; // 풀(Pool) Size 설정
[SerializeField] private PooledObject objectToPool;
private Stack<PooledObject> stack; // 풀링된 오브젝트들을 담는 스택
private void Start()
{
SetupPool(); // 게임 플레이 중 한 번 실행
}
/* 풀 생성 */
private void SetupPool()
{
stack = new Stack<PooledObject>();
PooledObject instance = null;
for (int i = 0; i < initPoolSize; i++) // 위에서 정한 initPoolSize만큼 생성
{
instance = Instantiate(objectToPool);
instance.Pool = this;
instance.gameObject.SetActive(false); // 비활성화 🔴 (준비 단계이기 때문에 비활성 해놓기)
stack.Push(instance);
}
}
/* 풀에서 첫 번째 액티브 게임 오브젝트를 반환 */
public PooledObject GetPooledObject()
{
/* 풀이 비어있으면(=더이상 꺼낼 오브젝트가 없으면), 새로운 PooledObjects를 인스턴스화*/
if (stack.Count == 0)
{
PooledObject newInstance = Instantiate(objectToPool);
newInstance.Pool = this;
return newInstance; // 일회성 인스턴스
}
/* 그렇지 않으면, 풀에서 다음 요소(인스턴스)를 꺼냄 */
PooledObject nextInstance = stack.Pop();
nextInstance.gameObject.SetActive(true); // 활성화 🔵
return nextInstance;
}
/* 사용이 끝난 오브젝트를 풀에 반환할 때 호출 */
public void ReturnToPool(PooledObject pooledObject)
{
stack.Push(pooledObject);
pooledObject.gameObject.SetActive(false); // 비활성화 🔴
}
}
풀링된 오브젝트(Client)는 ObjectPool.cs을 참조하기 위해 아래 PooledObject 컴포넌트를 가진다.
/* Object Pool의 관리 대상인 오브젝트 */
public class PooledObject : MonoBehaviour
{
private ObjectPool pool;
public ObjectPool Pool { get => pool; set => pool = value; }
/* 게임 오브젝트가 비활성화되고 풀로 반환하는 함수 */
public void Release()
{
pool.ReturnToPool(this);
}
}
🔖 UnityEngine.Pool (유니티 API)
유니티 2021 버전부터 자체적으로 오브젝트 풀(스택 기반) API을 제공한다. 🔗 Unity Doc
UnityEngine.Pool 은 유니티에서 제공하는 기본 Object Pooling 라이브러리다.
`using UnityEngine.Pool`로 사용이 가능하며, 기본적인 인터페이스가 구현되어 있다.
오브젝트 풀을 처음부터 만들 필요가 없어 시간을 단축할 수 있다.
🧩 제공하는 기능
- 오브젝트 생성하여, `Pool`을 채운다.
- `Pool`에서 가져오기
- `Pool`로 반환하기
- 풀링 된 오브젝트 파괴하기
- 주로 최대한도에 도달한 경우 사용한다.
예시 코드 | UnityEngine.Pool API 사용
using UnityEngine.Pool;
public class RevisedGun : MonoBehaviour
{
…
// Unity 2021 이상 버전에서 사용 가능한 스택 기반 ObjectPool
private IObjectPool<RevisedProjectile> objectPool;
// 이미 풀에 있는 기존 항목을 반환하려 할 때 예외를 반환
[SerializeField] private bool collectionCheck = true;
// 풀의 용량과 최대 크기를 제어하는 추가 옵션
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxSize = 100;
private void Awake()
{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjecti
le,
OnGetFromPool, OnReleaseToPool, OnDestroyPooledObject,
collectionCheck, defaultCapacity, maxSize);
}
// 오브젝트 풀을 채울 항목을 만들 때 호출됨
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance =
Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
return projectileInstance;
}
// 오브젝트 풀로 항목을 반환할 때 호출됨
private void OnReleaseToPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(false);
}
// 오브젝트 풀에서 다음 항목을 검색할 때 호출됨
private void OnGetFromPool(RevisedProjectile pooledObject)
{
pooledObject.gameObject.SetActive(true);
}
// 풀링된 항목의 최대 개수를 초과할 때 호출됨(풀링된 오브젝트 파괴)
private void OnDestroyPooledObject(RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}
private void FixedUpdate()
{
…
}
}
📌 오브젝트 풀의 단점
오브젝트 풀의 문제점으로는 메모리 낭비 가능성이 있다.
- 필요 이상의 객체를 미리 생성하면 메모리가 낭비될 수 있다.
- 사용되지 않는 객체도 메모리를 점유한다.
Thread-Safe 하지 않다.
- 멀티스레드에서 사용하기 위해선 동기작업이 필요하다.
참고
Object pool pattern - Wikipedia
오브젝트 풀링을 사용하여 Unity에서 C# 스크립트 성능 향상하기
'👻 Unity' 카테고리의 다른 글
[Unity] 싱글톤 패턴(Singleton Pattern): 오직 하나뿐인 객체 (0) | 2024.11.27 |
---|---|
[Unity] 유니티 설치 실패: Validation Failed 원인 및 해결 방법 (0) | 2024.08.20 |