무엇을 찾고 계신가요?
Hero background image
오브젝트 풀링을 사용하여 Unity에서 C# 스크립트 성능 향상하기

Unity 프로젝트에서 일반적인 게임 프로그래밍 디자인 패턴을 구현하면 깔끔하고 체계적이며 가독성 높은 코드베이스를 효율적으로 빌드하고 유지 관리할 수 있습니다. 디자인 패턴은 리팩터링과 테스트 시간을 줄여줄 뿐만 아니라 온보딩 및 개발 프로세스의 속도를 높여 게임, 개발팀, 비즈니스 성장을 위한 탄탄한 기반이 됩니다.

디자인 패턴은 코드에 복사하여 붙여넣을 수 있는 완성된 솔루션이 아니라 올바르게 사용하면 더 크고 확장 가능한 애플리케이션을 구축하는 데 도움이 되는 추가 도구라고 생각하세요.

이 페이지에서는 오브젝트 풀링과 이를 통해 게임 성능을 개선하는 방법에 대해 설명합니다. 여기에는 프로젝트에 Unity의 빌트인 오브젝트 풀링 시스템을 구현하는 방법에 대한 예제가 포함되어 있습니다.

이 콘텐츠는 무료 전자책을 기반으로 합니다, 게임 프로그래밍 패턴으로 코드 수준 높이기에서 잘 알려진 디자인 패턴을 설명하고 Unity 프로젝트에 사용할 수 있는 실용적인 예제를 공유합니다.

Unity 게임 프로그래밍 패턴 시리즈의 다른 글은 Unity 베스트 프랙티스 허브에서 확인하거나 다음 링크를 클릭하세요:

4-2 계층 구조
Unity에서의 오브젝트 풀링 기능 알아보기

객체 풀링은 반복적인 생성 및 삭제 호출을 실행하는 데 필요한 CPU의 처리 능력을 줄여 성능을 최적화할 수 있는 디자인 패턴입니다. 대신 오브젝트 풀링을 사용하면 기존 게임 오브젝트를 반복해서 재사용할 수 있습니다.

오브젝트 풀링의 핵심 기능은 오브젝트를 필요에 따라 생성하고 소멸하는 것이 아니라 미리 생성하여 풀에 저장하는 것입니다. 개체가 필요할 때 풀에서 가져와서 사용하고, 더 이상 필요하지 않으면 파기하지 않고 풀로 돌려보냅니다.

위 이미지는 오브젝트 풀링의 일반적인 사용 사례인 포탑에서 발사체를 발사하는 경우를 보여줍니다. 이 예제를 단계별로 풀어보겠습니다.

오브젝트 풀 패턴은 생성 후 소멸하는 대신 비활성화된 풀에서 준비 및 대기 상태로 유지되는 초기화된 오브젝트 집합을 사용합니다. 그런 다음 패턴은 게임플레이 전에 특정 순간에 필요한 모든 오브젝트를 미리 인스턴스화합니다. 풀은 로딩 화면과 같이 플레이어가 끊김 현상을 알아차리지 못할 적절한 시기에 활성화해야 합니다.

풀의 게임 오브젝트를 사용한 후에는 비활성화되어 게임에서 다시 필요할 때 사용할 수 있습니다. 객체가 필요할 때 애플리케이션에서 먼저 인스턴스화할 필요가 없습니다. 대신 풀에서 요청하여 활성화 및 비활성화한 다음 파기하지 않고 풀로 반환할 수 있습니다.

이 패턴은 다음 섹션에서 설명하는 것처럼 가비지 컬렉션을 실행하는 데 필요한 메모리 관리 비용을 줄일 수 있습니다.

Unity 프로파일링 전자책
메모리 할당

객체 풀링을 활용하는 방법에 대한 예를 살펴보기 전에 객체 풀링이 해결하는 데 도움이 되는 근본적인 문제를 간략히 살펴보겠습니다.

풀링 기술은 인스턴스화 및 파기 작업에 소요되는 CPU 사이클을 줄이는 데만 유용한 것이 아닙니다. 또한 메모리를 할당 및 할당 해제하고 생성자와 소멸자를 호출해야 하는 객체 생성 및 소멸의 오버헤드를 줄여 메모리 관리를 최적화합니다.

Unity의 관리형 메모리

유니티의 C# 스크립팅 환경은 관리형 메모리 시스템을 제공합니다. 메모리 릴리스를 관리하는 데 도움이 되므로 코드를 통해 수동으로 요청할 필요가 없습니다. 메모리 관리 시스템은 메모리 액세스를 보호하여 더 이상 사용하지 않는 메모리를 확보하고 코드에 유효하지 않은 메모리에 대한 액세스를 방지하는 데도 도움이 됩니다.

유니티는 가비지 컬렉터를 사용하여 애플리케이션과 유니티가 더 이상 사용하지 않는 오브젝트에서 메모리를 회수합니다. 그러나 관리되는 메모리를 할당하는 데 CPU의 시간이 많이 걸리고 가비지 컬렉션 (GC)으로 인해 CPU가 작업을 완료할 때까지 다른 작업을 중단할 수 있으므로 런타임 성능에도 영향을 미칩니다.

Unity에서 새 오브젝트를 생성하거나 기존 오브젝트를 삭제할 때마다 메모리가 할당되고 할당 해제됩니다. 여기서 오브젝트 풀링이 중요한 역할을 합니다: 가비지 수집 급증으로 인해 발생할 수 있는 버벅거림을 줄여줍니다. 메모리 할당으로 인해 많은 수의 오브젝트를 생성하거나 삭제할 때 GC 스파이크가 발생하는 경우가 많습니다. 이 프로세스는 가비지 컬렉션을 조기에 수집하는 것 외에도 메모리 조각화를 유발하여 인접한 메모리 영역을 찾기 어렵게 만들 수 있습니다.

기존의 동일한 오브젝트를 비활성화 및 활성화하여 재활용하면 실제로는 단순히 비활성화했다가 재활용하는 것만으로 화면 밖에서 수백 발의 총알을 발사하는 것과 같은 효과를 만들 수 있습니다.

메모리 관리에 대한 자세한 내용은 고급 프로파일링 가이드에서 자세히 알아보세요.

오브젝트 풀 샘플 프로젝트
Using UnityEngine.Pool

오브젝트 풀링을 구현하는 커스텀 시스템을 직접 만들 수도 있지만, 프로젝트에서 이 패턴을 효율적으로 구현하는 데 사용할 수 있는 내장된 ObjectPool 클래스가 Unity에 있습니다(Unity 2021 LTS 이상에서 사용 가능).

Github에서 제공되는 이 샘플 프로젝트를 통해 UnityEngine.Pool API를 사용하여 빌트인 오브젝트 풀링 시스템을 활용하는 방법을 살펴보겠습니다. Github 페이지에서 자산>7 오브젝트 풀>스크립트>예제2021로 이동하여 파일을 찾습니다.

참고: Unity Learn의튜토리얼을 통해 이전 버전의 Unity에서 오브젝트 풀링의 예시를 확인할 수 있습니다.

이 예제는 마우스 버튼을 누르면 포탑이 발사체를 빠르게 발사(기본적으로 초당 10발로 설정됨)하는 것으로 구성되어 있습니다. 각 발사체는 화면을 가로질러 이동하며 화면을 벗어날 때 파괴해야 합니다. 오브젝트 풀링이 없으면 이전 섹션에서 설명한 것처럼 CPU 및 메모리 관리에 상당한 부하가 발생할 수 있습니다.

오브젝트 풀링을 사용하면 실제로는 수백 개의 총알이 화면 밖에서 발사되는 것처럼 보이지만, 실제로는 단순히 비활성화된 후 반복해서 재활용됩니다.

예제 스크립트의 코드는 풀 크기가 동시에 활성화된 개체를 표시할 수 있을 만큼 충분히 큰지 확인하여 동일한 개체가 지속적으로 재사용되고 있다는 사실을 위장하는 데 도움이 됩니다.

유니티의 파티클 시스템을 사용해 본 적이 있다면 오브젝트 풀을 직접 경험해 보셨을 것입니다. 파티클 시스템 컴포넌트에는 최대 파티클 수에 대한 설정이 포함되어 있습니다. 사용 가능한 파티클을 재활용하여 이펙트가 최대 개수를 초과하지 않도록 합니다. 오브젝트 풀도 비슷하게 작동하지만 원하는 게임 오브젝트를 사용할 수 있습니다.

Unpacking RevisedGun.cs

의 코드를 살펴보겠습니다. RevisedGun.cs 의 코드를 살펴보겠습니다. 이 코드는 자산>7 오브젝트 풀>스크립트>예제2021을 통해 Github 데모에 있습니다.

가장 먼저 눈에 띄는 것은 풀 네임스페이스가 포함되었다는 점입니다:

using UnityEngine.Pool;

UnityEngine.Pool API를 사용하면 스택 기반의 ObjectPool 클래스를 사용하여 오브젝트 풀 패턴으로 오브젝트를 추적할 수 있습니다. 필요에 따라 컬렉션풀 클래스(목록, 해시셋, 사전 등)를 사용할 수도 있습니다.

그런 다음 스폰할 프리팹을 포함하여 총기 발사 특성에 대한 특정 설정을 적용합니다(RevisedProjectile 유형의 projectilePrefab으로 이름 지정).

ObjectPool 인터페이스는 다음 섹션에서 설명하는 RevisedProjectile.cs에서 참조되고 Awake 함수에서 초기화됩니다.

private void Awake()

{
objectPool = 새 ObjectPool<개정된프로젝트>(CreateProjectile,

OnGetFromPool, OnReleaseToPool,

OnDestroyPooledObject,collectionCheck, defaultCapacity, maxSize);
}

ObjectPool<T0> 생성자를 살펴보면 언제 로직을 설정하는 데 유용한 기능이 포함되어 있음을 알 수 있습니다:

먼저 풀을 채울 풀링된 항목을 만듭니다.

풀에서 아이템 가져오기

풀에 아이템 반환하기

풀링된 객체 삭제하기(예: 최대 한도에 도달한 경우)

기본 제공 ObjectPool 클래스에는 기본 풀 크기와 최대 풀 크기(후자는 풀에 저장되는 최대 항목 수)에 대한 옵션도 포함되어 있습니다. 릴리즈를 호출할 때 트리거되며 풀이 가득 차면 대신 소멸됩니다.

코드 예제에서 특정 사용 사례에 따라 Unity가 오브젝트 풀링을 효율적으로 처리하는 방법을 지정하는 몇 가지 작업을 수행하는 방법을 살펴보겠습니다.

먼저 풀이 비어 있을 때 새 인스턴스를 생성하는 데 사용되는 createFunc가 전달되며, 이 경우 새 프로파일 프리팹을 인스턴스화하는 CreateProjectile()이 전달됩니다.

private RevisedProjectile CreateProjectile()

{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;

return projectileInstance;
}

게임 오브젝트의 인스턴스를 요청할 때 OnGetFromPool이 호출되므로 기본적으로 풀에서 가져오는 게임 오브젝트를 활성화합니다.

private void OnGetFromPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(true);
}

온릴리스투풀은 게임 오브젝트가 더 이상 필요하지 않아 풀로 반환될 때 사용되며, 이 예제에서는 단순히 다시 비활성화하기만 하면 됩니다.

private void OnReleaseToPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(false);
}

허용된 풀링된 아이템의 최대 개수를 초과하면 OnDestroyPooledObject가 호출됩니다. 풀이 이미 가득 차면 개체가 파괴됩니다.

private void OnDestroyPooledObject(RevisedProjectile 풀링된 객체)

{
Destroy(pooledObject.gameObject);
}

컬렉션체크는 IObjectPool을 초기화하는 데 사용되며 이미 풀 매니저에 반환된 게임 오브젝트를 해제하려고 하면 예외가 발생하지만 이 검사는 에디터에서만 수행됩니다. 이 기능을 끄면 CPU 사이클을 일부 절약할 수 있지만 이미 다시 활성화된 개체가 반환될 위험이 있습니다.

이름에서 알 수 있듯이 defaultCapacity는 요소를 포함할 스택/리스트의 기본 크기이며, 따라서 미리 커밋할 메모리 할당량입니다. maxPoolSize는 스택의 최대 크기이며, 생성된 풀링된 게임 오브젝트는 이 크기를 초과하지 않아야 합니다. 즉, 가득 찬 풀에 아이템을 반환하면 해당 아이템이 대신 파괴됩니다.

그러면 FixedUpdate()에서 총알을 발사하는 로직을 실행할 때마다 새 발사체를 인스턴스화하는 대신 풀링된 오브젝트를 얻게 됩니다.

RevisedProjectile bulletObject = objectPool.Get();

간단합니다.

RevisedProjectile.cs 풀기

이제 RevisedProjectile.cs 스크립트를 살펴보겠습니다.

오브젝트를 풀에 다시 릴리스하는 것을 더 편리하게 해주는 오브젝트풀에 대한 참조를 설정하는 것 외에도 몇 가지 흥미로운 세부 사항이 있습니다.

시간 초과 지연은 발사체가 "사용"된 후 게임 풀로 다시 반환될 수 있는 시점을 추적하는 데 사용되며, 기본적으로 3초 후에 발생합니다.

Deactivate() 함수는 objectPool.Release(this)를 사용하여 발사체를 풀로 다시 릴리스할 뿐만 아니라 움직이는 리지드바디 속도 파라미터도 재설정하는 DeactivateRoutine(float delay)이라는 코루틴을 활성화합니다.

이 프로세스는 과거에 사용되었으나 바람직하지 않은 상태로 인해 초기화해야 하는 '더티 아이템'의 문제를 해결합니다.

이 예제에서 볼 수 있듯이, 특별한 사용 사례가 없는 한 패턴을 처음부터 다시 빌드할 필요가 없으므로 UnityEngine.Pool API를 사용하면 오브젝트 풀을 효율적으로 설정할 수 있습니다.

게임 오브젝트에만 국한되지 않습니다. 풀링은 게임 오브젝트, 인스턴스화된 프리팹, C# 사전 등 모든 유형의 C# 엔티티를 재사용하기 위한 성능 최적화 기술입니다. 유니티는 사전을 지원하는 DictionaryPool<T0,T1>과 해시셋을 지원하는 HashSetPool<T0> 등 다른 엔티티를 위한 몇 가지 대체 풀링 클래스를 제공합니다. 이에 대한 자세한 내용은 문서에서 확인하세요.

LinkedPool은 링크된 목록을 사용하여 재사용할 객체 인스턴스 컬렉션을 보관하므로 실제로 풀에 저장된 요소에 대해서만 메모리를 사용하기 때문에 (경우에 따라) 메모리 관리가 더 쉬워질 수 있습니다.

이를 단순히 C# 스택과 그 아래에 C# 배열을 사용하므로 연속적인 메모리 덩어리를 포함하는 ObjectPool과 비교해 보세요. 단점은 기본 크기와 최대 크기를 활용하여 필요에 따라 구성할 수 있는 ObjectPool보다 LinkedPool에서 이 데이터 구조를 관리하는 데 항목당 더 많은 메모리와 CPU 사이클을 사용해야 한다는 점입니다.

블로그 표지
객체 풀링을 구현하는 다른 방법

오브젝트 풀을 사용하는 방법은 애플리케이션에 따라 다르지만, 앞의 예시와 같이 무기가 여러 발사체를 발사해야 할 때 일반적으로 나타나는 패턴입니다.

가비지 컬렉션이 급증할 위험이 있으므로 많은 수의 객체를 인스턴스화할 때마다 코드를 프로파일링하는 것이 좋습니다. 게임플레이가 끊길 위험이 있는 심각한 스파이크가 감지되면 오브젝트 풀 사용을 고려하세요. 객체 풀링은 풀의 여러 수명 주기를 관리해야 하므로 코드베이스에 복잡성을 더할 수 있다는 점만 기억하세요. 또한, 게임플레이에 꼭 필요하지 않은 메모리를 너무 많이 생성하여 불필요한 메모리를 확보하게 될 수도 있습니다.

앞서 언급했듯이 이 문서에 포함된 예제 외에도 객체 풀링을 구현하는 몇 가지 다른 방법이 있습니다. 한 가지 방법은 필요에 맞게 사용자 지정할 수 있는 자체 구현을 만드는 것입니다. 하지만 유형 및 스레드 안전과 사용자 정의 객체 할당/할당 해제의 복잡한 문제를 염두에 두어야 합니다.

다행히도 Unity 에셋 스토어는 시간을 절약할 수 있는 몇 가지 훌륭한 대안을 제공합니다.

Unity 프로그래밍을 위한 고급 리소스

전자책, 게임 프로그래밍 패턴으로 코드 수준 높이기에서는 간단한 커스텀 오브젝트 풀 시스템에 대한 보다 자세한 예제를 제공합니다. Unity 학습에서는 오브젝트 풀링에 대한 소개와 2021 LTS의 새로운 빌트인 오브젝트 풀링 시스템 사용에 대한 전체 튜토리얼도 제공합니다( 여기에서 확인할 수 있습니다).

모든 고급 기술 전자책과 문서는 다음 에서 확인할 수 있습니다. Unity 베스트 프랙티스 허브에서 확인할 수 있습니다. 전자책은 에서도 확인할 수 있습니다. 고급 모범 사례 페이지에서도 확인할 수 있습니다.

이 콘텐츠가 마음에 드셨나요?