Unity 오브젝트 풀링 시스템 완전 가이드

CokeBear·2025년 8월 6일
0

Unity 오브젝트 풀링 시스템 완전 가이드

게임 개발에서 성능 최적화는 매우 중요한 요소입니다. 특히 총알, 적, 이펙트 등 많은 수의 오브젝트를 빈번하게 생성하고 파괴해야 하는 경우, 오브젝트 풀링(Object Pooling) 시스템은 필수적인 기술입니다. 이번 글에서는 Unity에서 효율적인 오브젝트 풀링 시스템을 구현하는 방법을 자세히 알아보겠습니다.

오브젝트 풀링이란?

오브젝트 풀링은 미리 정해진 수의 게임 오브젝트를 생성해두고 재사용하는 시스템입니다. 매번 Instantiate()Destroy()를 호출하는 대신, 이미 생성된 오브젝트를 활성화/비활성화하여 사용함으로써 성능을 크게 향상시킬 수 있습니다.

장점

  • 성능 향상: 가비지 컬렉션 빈도 감소
  • 메모리 효율성: 일정한 메모리 사용량 유지
  • 프레임 드롭 방지: 실시간 생성/파괴로 인한 끊김 현상 제거

시스템 구조 개요

오브젝트 풀링 시스템은 두 개의 핵심 클래스로 구성됩니다:

  1. ObjectPool: 풀링 시스템의 메인 매니저
  2. PooledObject: 각 풀링 오브젝트에 붙는 헬퍼 컴포넌트

Dictionary와 Key-Value Pair의 이해

풀링 시스템을 이해하기 전에, C#의 Dictionary 자료구조를 알아야 합니다.

Key-Value Pair란?

Key-Value Pair(키-값 쌍)은 고유한 식별자(키)와 그에 연결된 특정 값으로 구성된 데이터 구조입니다.

실생활 예시:

  • 사전에서 '단어(키)'와 '뜻(값)'
  • 전화번호부에서 '이름(키)'와 '전화번호(값)'

Dictionary in C#

Dictionary<TKey, TValue>

오브젝트 풀링에서의 적용:

  • Key (TKey): GameObject 프리팹 (총알, 적, 이펙트 등)
  • Value (TValue): Queue<GameObject> (해당 프리팹의 오브젝트들을 담는 큐)
// 예시: 각 프리팹별로 오브젝트 큐를 관리
Dictionary<GameObject, Queue<GameObject>> poolDictionary;

ObjectPool 클래스 상세 분석

핵심 변수들

public class ObjectPool : MonoBehaviour
{
    // 싱글톤 패턴으로 전역 접근 가능
    public static ObjectPool instance;
    
    // 각 타입별 기본 풀 크기
    [SerializeField] private int poolSize = 10;
    
    // 핵심: 프리팹별 오브젝트 큐를 관리하는 딕셔너리
    private Dictionary<GameObject, Queue<GameObject>> poolDictionary;
}

주요 메서드들

1. Awake() - 싱글톤 초기화

private void Awake()
{
    // 싱글톤 패턴 구현
    if (instance == null)
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
        poolDictionary = new Dictionary<GameObject, Queue<GameObject>>();
    }
    else
    {
        Destroy(gameObject);
    }
}

2. GetObject() - 풀에서 오브젝트 가져오기

public GameObject GetObject(GameObject prefab)
{
    // 해당 프리팹의 풀이 존재하지 않으면 새로 생성
    if (!poolDictionary.ContainsKey(prefab))
    {
        InitializeNewPool(prefab);
    }
    
    // 풀이 비어있으면 새 오브젝트 생성
    if (poolDictionary[prefab].Count == 0)
    {
        CreateNewObject(prefab);
    }
    
    // 풀에서 오브젝트 가져와서 활성화 후 반환
    GameObject objectToSpawn = poolDictionary[prefab].Dequeue();
    objectToSpawn.SetActive(true);
    
    return objectToSpawn;
}

3. ReturnObject() - 오브젝트를 풀에 반환

public void ReturnObject(GameObject objectToReturn, float delay = 0.001f)
{
    StartCoroutine(DelayReturn(delay, objectToReturn));
}

private IEnumerator DelayReturn(float delay, GameObject objectToReturn)
{
    yield return new WaitForSeconds(delay);
    ReturnToPool(objectToReturn);
}

4. ReturnToPool() - 실제 풀 반환 로직

private void ReturnToPool(GameObject objectToReturn)
{
    // PooledObject 컴포넌트에서 원본 프리팹 정보 가져오기
    PooledObject pooledComponent = objectToReturn.GetComponent<PooledObject>();
    GameObject originalPrefab = pooledComponent.originalPrefab;
    
    // 오브젝트 비활성화 및 부모 설정
    objectToReturn.SetActive(false);
    objectToReturn.transform.parent = transform;
    
    // 해당 프리팹의 풀에 다시 추가
    poolDictionary[originalPrefab].Enqueue(objectToReturn);
}

5. InitializeNewPool() - 새 풀 초기화

private void InitializeNewPool(GameObject prefab)
{
    // 새 프리팹을 위한 큐 생성
    poolDictionary[prefab] = new Queue<GameObject>();
    
    // 기본 크기만큼 오브젝트 미리 생성
    for (int i = 0; i < poolSize; i++)
    {
        CreateNewObject(prefab);
    }
}

6. CreateNewObject() - 새 오브젝트 생성

private void CreateNewObject(GameObject prefab)
{
    // 새 오브젝트 생성
    GameObject newObject = Instantiate(prefab);
    
    // PooledObject 컴포넌트 추가 및 원본 프리팹 참조 설정
    PooledObject pooledComponent = newObject.AddComponent<PooledObject>();
    pooledComponent.originalPrefab = prefab;
    
    // 비활성화 후 풀에 추가
    newObject.SetActive(false);
    poolDictionary[prefab].Enqueue(newObject);
}

PooledObject 클래스

public class PooledObject : MonoBehaviour
{
    // 원본 프리팹 참조를 저장하는 프로퍼티
    public GameObject originalPrefab { get; set; }
}

이 간단한 헬퍼 클래스는 각 풀링된 오브젝트가 어떤 프리팹에서 생성되었는지 기억하여, 올바른 풀로 반환될 수 있도록 돕습니다.

실제 사용 예시

총알 발사 시스템

public class Gun : MonoBehaviour
{
    [SerializeField] private GameObject bulletPrefab;
    [SerializeField] private Transform firePoint;
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Fire();
        }
    }
    
    void Fire()
    {
        // 풀에서 총알 오브젝트 가져오기
        GameObject bullet = ObjectPool.instance.GetObject(bulletPrefab);
        bullet.transform.position = firePoint.position;
        bullet.transform.rotation = firePoint.rotation;
        
        // 3초 후 풀에 반환
        ObjectPool.instance.ReturnObject(bullet, 3f);
    }
}

적 스폰 시스템

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private GameObject enemyPrefab;
    [SerializeField] private Transform[] spawnPoints;
    [SerializeField] private float spawnInterval = 2f;
    
    void Start()
    {
        InvokeRepeating(nameof(SpawnEnemy), 0f, spawnInterval);
    }
    
    void SpawnEnemy()
    {
        Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
        
        GameObject enemy = ObjectPool.instance.GetObject(enemyPrefab);
        enemy.transform.position = spawnPoint.position;
        enemy.transform.rotation = spawnPoint.rotation;
    }
}

시스템 작동 플로우

1. 오브젝트 요청 과정

GetObject(prefab) 호출
    ↓
프리팹의 풀이 존재하는가?
    ↓ (No)
InitializeNewPool() 실행
    ↓
풀에 사용 가능한 오브젝트가 있는가?
    ↓ (No)
CreateNewObject() 실행
    ↓
Dequeue()로 오브젝트 가져오기
    ↓
SetActive(true) 후 반환

2. 오브젝트 반환 과정

ReturnObject() 호출
    ↓
DelayReturn() 코루틴 시작
    ↓
지정된 시간 후 ReturnToPool() 실행
    ↓
PooledObject에서 originalPrefab 확인
    ↓
SetActive(false) 및 부모 오브젝트로 이동
    ↓
Enqueue()로 해당 풀에 다시 추가

성능 최적화 팁

1. 적절한 풀 크기 설정

// 게임의 특성에 따라 풀 크기 조정
[SerializeField] private int bulletPoolSize = 50;    // 빈번한 생성
[SerializeField] private int enemyPoolSize = 20;     // 중간 빈도
[SerializeField] private int effectPoolSize = 30;    // 이펙트용

2. 풀 예열(Pre-warming)

void Start()
{
    // 게임 시작 시 자주 사용되는 오브젝트들을 미리 생성
    PrewarmPool(bulletPrefab, 50);
    PrewarmPool(enemyPrefab, 20);
}

void PrewarmPool(GameObject prefab, int count)
{
    for (int i = 0; i < count; i++)
    {
        GameObject obj = ObjectPool.instance.GetObject(prefab);
        ObjectPool.instance.ReturnObject(obj, 0f);
    }
}

3. 풀 크기 모니터링

public void LogPoolStatus()
{
    foreach (var kvp in poolDictionary)
    {
        Debug.Log($"Pool {kvp.Key.name}: {kvp.Value.Count} objects available");
    }
}

주의사항 및 베스트 프랙티스

1. 오브젝트 초기화

풀에서 가져온 오브젝트는 이전 상태를 유지할 수 있으므로, 사용 전 반드시 초기화해야 합니다.

public class Bullet : MonoBehaviour
{
    void OnEnable()
    {
        // 풀에서 활성화될 때마다 초기화
        GetComponent<Rigidbody>().velocity = Vector3.zero;
        transform.localScale = Vector3.one;
        // 기타 초기화 로직...
    }
}

2. 메모리 누수 방지

public void ClearPool(GameObject prefab)
{
    if (poolDictionary.ContainsKey(prefab))
    {
        while (poolDictionary[prefab].Count > 0)
        {
            DestroyImmediate(poolDictionary[prefab].Dequeue());
        }
        poolDictionary.Remove(prefab);
    }
}

3. 동적 풀 크기 조정

public void AdjustPoolSize(GameObject prefab, int newSize)
{
    if (!poolDictionary.ContainsKey(prefab)) return;
    
    Queue<GameObject> pool = poolDictionary[prefab];
    
    while (pool.Count > newSize)
    {
        DestroyImmediate(pool.Dequeue());
    }
    
    while (pool.Count < newSize)
    {
        CreateNewObject(prefab);
    }
}

고급 기능 확장

1. 타입별 풀 관리자

[System.Serializable]
public class PoolInfo
{
    public GameObject prefab;
    public int poolSize;
    public bool expandable = true;
}

public class AdvancedObjectPool : MonoBehaviour
{
    [SerializeField] private PoolInfo[] poolsToCreate;
    
    void Start()
    {
        foreach (var poolInfo in poolsToCreate)
        {
            CreatePool(poolInfo);
        }
    }
}

2. 이벤트 시스템 통합

public class ObjectPool : MonoBehaviour
{
    public System.Action<GameObject> OnObjectSpawned;
    public System.Action<GameObject> OnObjectReturned;
    
    public GameObject GetObject(GameObject prefab)
    {
        // ... 기존 로직 ...
        
        OnObjectSpawned?.Invoke(objectToSpawn);
        return objectToSpawn;
    }
}

결론

오브젝트 풀링 시스템은 Unity 게임의 성능을 크게 향상시킬 수 있는 필수 기술입니다. Dictionary를 활용한 효율적인 자료구조 설계와 Queue를 통한 FIFO(First In, First Out) 관리 방식은 메모리 효율성과 성능 최적화를 동시에 달성할 수 있게 해줍니다.

핵심은 적절한 풀 크기 설정, 올바른 오브젝트 초기화, 그리고 메모리 관리입니다. 이 시스템을 기반으로 게임의 특성에 맞는 커스터마이징을 통해 더욱 효율적인 게임을 개발할 수 있을 것입니다.

오브젝트 풀링은 단순한 최적화 기법을 넘어서, 안정적이고 예측 가능한 게임 퍼포먼스를 보장하는 핵심 아키텍처 패턴입니다. 이를 마스터하여 보다 전문적인 게임 개발자로 성장하시기 바랍니다.

profile
back end developer

0개의 댓글