본문 바로가기

Unity/Study

Unity C#) 디자인 패턴 가이드 #3. 오브젝트 풀(Object pool)

본 글은 유니티에서 발간한 전자책 'Level up your programming with game programming patterns'을 정리한 글입니다. 원문은 다음 링크에서 확인하실 수 있습니다.

https://unity.com/kr/resources/level-up-your-code-with-game-programming-patterns

 

 

1. Object pool

 

유니티에서 런타임 중에 기본적으로 실행되는 가비지 컬렉션(Garbage Collection) 프로세스는 메모리에서 사용되지 않는 객체를 정리하는 프로세스이다. 많은 양의 오브젝트가 동적으로 생성되거나 파괴될 경우 이러한 가비지 컬렉션 프로세스가 갑작스럽게 발생하여 CPU 및 시스템 리소스 사용량이 급격하게 증가하는 GC 스파이크가 발생할 수 있으며, 이에 따라 게임 실행이 갑작스럽게 멈추거나 지체되는 스터터링(stuttering)이 발생할 수 있다.

 

오브젝트 풀 패턴은 게임 오브젝트를 생성하고 파괴할 때의 이러한 메모리 부담을 줄이기 위한 최적화 기술이다.

 

오브젝트 풀 패턴은 초기화된 오브젝트의 집합을 비활성화된 풀(pool)에 준비 및 대기시킨다. 오브젝트가 필요할 때 오브젝트를 생성하는 대신에, 풀로부터 오브젝트를 가져와 활성화시킨다. 오브젝트 사용이 끝나면 오브젝트를 파괴하는 대신 오브젝트를 비활성화하고 풀에 반납한다.

 

오브젝트 풀 패턴을 통해 GC 스파이크로부터 발생하는 스터터링을 줄일 수 있다. 로딩 화면과 같은 적절한 타이밍에 오브젝트 풀을 미리 생성하여 유저가 스터터링을 눈치채지 못하게 할 수도 있을 것이다.

 

 

2. Example: Simple pool system

 

간단한 오브젝트 풀 패턴을 구현하기 위해 다음과 같은 컴포넌트들을 구현해보자.

- ObjectPool: 오브젝트 풀을 관리하는 컴포넌트

- PooledObject: 풀 오브젝트의 프리팹에 붙을 컴포넌트, 풀에 대한 참조를 담음

 

 

ObjectPool은 풀의 사이즈(initPoolSize), 풀에 저장할 풀 오브젝트의 프리팹(objectToPool), 오브젝트의 집합(stack)을 필드로 가진다. SetupPool()은 새로운 스택을 만들고 initPoolSize만큼 objectToPool의 인스턴스를 만들어 스택에 채운다.

 

이어서 ObjectPool에는 풀에서 오브젝트를 가져오는 메서드와 다시 오브젝트를 풀에 반납하는 메서드가 필요할 것이다.

 

GetPooledObject()는 풀이 비어있으면 새로운 PooledObject를 생성하고, 그렇지 않으면 풀에서 오브젝트를 꺼내 반환한다.

 

풀 오브젝트에 붙을 PooledObject는 다음과 같다.

 

멤버 변수를 통해 풀을 참조하고 Release()를 호출하여 풀에 오브젝트를 반납할 수 있다.

 

 

3. Common usage: Creating projectiles

 

오브젝트 풀의 기본적인 예시 중 하나는 총알을 발사하는 것이다. 예시의 전체 코드는 다음과 같다.

https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/7%20Object%20Pool/Scripts/ExampleUsage

 

오브젝트 풀 이용 예시: 총알 발사

 

오브젝트 풀을 이용하여 총알 생성을 구현할 경우, 게임에서 수많은 총알들을 발사하는 것처럼 보이지만 실제로는 단순히 풀에 있는 총알들을 비활성화하고 재활용한다.

 

 

4. Object pool in ParticleSystem

 

만약 당신이 유니티에서 제공하는 ParticleSystem을 이용해봤다면, 오브젝트 풀을 이미 한번 경험해본 것이다.

 

ParticleSystem은 오브젝트 풀 패턴을 사용하여 파티클들을 관리한다. ParticleSystem 컴포넌트에서 파티클의 최대 개수를 설정할 수 있으며, 파티클들은 설정된 최대 개수를 넘지 않게 재사용된다.

 

 

5. Improvements

 

위 예시는 간단한 오브젝트 풀 패턴을 구현한 것이다. 실제 프로젝트에 쓸 오브젝트 풀을 구현할 때 다음과 같은 조정 사항을 고려해 볼 수 있다.

 

- 오브젝트 풀을 static 혹은 싱클톤으로 만들 수 있다.

다양한 소스로부터 풀의 오브젝트를 꺼내려한다면 오브젝트 풀을 static으로 만드는 것을 고려해라. 이와 비슷하게 오브젝트 풀 패턴을 싱글톤 패턴과 결합하여 사용할 수 있다.

 

- 여러 풀들을 관리하기 위해 딕셔너리를 사용할 수 있다.

여러 가지 다른 프리팹을 풀링하려는 경우, 각각을 별도의 풀에 저장하고 어떤 풀을 조회해야 하는지 알 수 있도록 key-value 쌍을 저장해라. 프리팹의 InstanceID를 고유의 key로 사용할 수 있다.

 

- 사용되지 않은 오브젝트들을 효과적으로 비활성화해라.

오브젝트 풀을 효과적으로 활용하는 것의 일부는 사용되지 않는 객체를 숨기고 풀에 반환하는 것이다. 오브젝트를 비활성화하는 모든 기회를 활용해라 (예: 화면 밖, 폭발로 가려진 경우 등).

 

- 에러를 체크해라.

이미 풀에 있는 오브젝트를 반환하는 오류를 피해라. 런타임 중에 에러가 발생할 수 있다.

 

- 풀의 최대 사이즈를 도입할 수 있다.

많은 풀 오브젝트는 메모리를 소비한다. 풀의 오브젝트가 특정 개수를 넘으면 오브젝트를 삭제하여 풀이 지나치게 많은 리소스를 사용하지 않도록 할 수 있다.

 

 

6. UnityEngine.Pool

 

오브젝트 풀 패턴은 너무나 널리 사용되어 이제 2021 버전 이상의 유니티는 자체 UnityEngine.Pool API를 지원한다. 해당 API는 스택 기반의 오브젝트 풀 패턴을 제공하며, 여러 자료형의 객체를 풀링하고 싶을 경우 CollectionPool을 사용할 수 있다.

 

해당 API를 사용하면 커스텀 풀 컴포넌트를 더이상 사용하지 않아도 된다. 대신에 UnityEngine.Pool의 네임스페이스를 사용하여 해당 라이브러리에서 제공하는 패턴으로 풀을 생성할 수 있다.

 

다음은 UnityEngine.Pool을 사용한 Gun 스크립트의 예시이다.

 

위에서 구현한 오브젝트 풀과 비슷하나 ObjectPool의 생성자는 다음과 같은 로직을 포함한다.

- 풀 오브젝트 생성하기(풀을 채울 때)

- 풀에서 오브젝트 꺼내기

- 풀에 오브젝트 반납하기

- 풀 오브젝트 파괴하기(풀 오브젝트 개수가 풀의 최대 제한을 넘길 때)

 

각각에 대응하는 메서드를 구현하고 ObjectPool의 생성자에 넘겨주어야 한다.

 

ObjectPool은 그 이외에도 풀에 들어있는 오브젝트를 반환할 때 Exception을 발생시킬 것인지의 여부와 기본 풀 사이즈, 최대 풀 사이즈를 옵션으로 지정할 수 있으며, 이들은 모두 기본값을 가진다.

 

Projectile 스크립트는 다음과 같이 작성할 수 있다.

 

전체 코드는 다음에서 확인할 수 있다.

https://github.com/Unity-Technologies/game-programming-patterns-demo/tree/main/Assets/7%20Object%20Pool/Scripts/ExampleUsage2021

 

UnityEngine.Pool API를 통해 오브젝트 풀들을 빠르고 편리하게 세팅할 수 있으며, 패턴을 직접 구현하지 않아도 된다.