오브젝트 풀링

woollim·2024년 10월 25일
1

Unity개념

목록 보기
3/8

■ 오브젝트 풀링 정리

○ 주요 개념

  • 메모리 할당 최소화 : 오브젝트를 매번 생성하고 파괴하면 메모리 할당과 해제가 빈번하게 발생하여 성능이 저하 됨. 오브젝트 풀링은 이러한 문제를 해결하기 위해 미리 필요한 수의 오브젝트를 생성해둠

  • 재사용(Reusing) : 한 번 생성된 오브젝트는 풀 안에서 재사용됨. 새 오브젝트를 만들지 않고, 사용이 끝난 오브젝트를 다시 활용하므로 성능을 크게 향상시킴

  • 풀 관리 : 풀에서 오브젝트를 필요로 할 때는 사용 가능한 오브젝트를 꺼내 사용하고, 사용이 끝나면 다시 풀에 반환함. 이 과정을 통해 효율적인 메모리 관리를 유지할 수 있음

○ 장점

  • 성능 향상 : 매번 오브젝트를 새로 생성하는 것보다 빠르며, 특히 자주 반복적으로 생성되는 오브젝트에 효과적
  • 메모리 안정성 : 메모리 누수와 과도한 가비지 컬렉션을 방지함
  • 부하 감소 : CPU 부하를 줄여 애플리케이션의 성능을 최적화함

○ 단점

  • 초기 메모리 사용량 : 처음에 많은 오브젝트를 생성해야 하므로 초기 메모리 사용량이 높을 수 있음
  • 복잡한 관리 : 풀링된 오브젝트가 너무 많아지면 이를 효율적으로 관리하기 어려울 수 있음

○ 오브젝트 풀 자료구조

  • 유니티의 오브젝트 풀링에서 사용하는 자료구조는 특정한 요구 사항에 맞춰 선택할 수 있으며, 특정 자료구조로 고정되어 있지는 않음

  • 리스트(List) : 간단하고 직관적인 자료구조로, 오브젝트를 순차적으로 관리하기에 좋음. 삽입과 제거가 빠르지 않지만, 적은 수의 오브젝트를 관리할 때는 적합함

  • 큐(Queue) : 선입선출(FIFO) 방식으로 오브젝트를 재사용할 때 유용함. 오브젝트가 필요할 때 앞에서 꺼내고, 사용이 끝난 오브젝트는 다시 뒤로 넣는 방식

  • 스택(Stack) : 후입선출(LIFO) 방식으로 사용됨. 가장 최근에 사용한 오브젝트를 다시 사용하고 싶은 경우 적합

  • 딕셔너리(Dictionary) or 해시맵(HashMap) : 특정 키를 기준으로 오브젝트에 접근해야 할 때 유용함. 오브젝트 풀링에서 오브젝트의 종류나 상태에 따라 구분해야 할 때 유용할 수 있음


○ 일반적으로 사용하는 형태

  • Object Type (오브젝트 타입)

    • 풀링할 오브젝트의 타입을 지정
    • 예를 들어, 미사일, 적 캐릭터, 총알 등 특정 오브젝트의 유형을 미리 정의할 수 있음
  • Initial Pool Size (초기 풀 크기)

    • 풀에 미리 생성해둘 오브젝트의 수를 지정함
    • 이 값은 애플리케이션의 성능 요구에 따라 달라질 수 있음. 너무 적으면 새로운 오브젝트를 만들어야 할 수 있고, 너무 많으면 메모리 낭비가 발생할 수 있음
  • Max Pool Size (최대 풀 크기)

    • 풀링된 오브젝트의 최대 수를 지정함
    • 풀의 크기가 너무 커지는 것을 방지하기 위해 설정하는 값
    • 풀의 크기가 이 값을 초과하면 새로운 오브젝트가 생성되지 않고 기존 오브젝트를 재사용함
  • Auto Expand (자동 확장)

    • 초기 풀 크기가 부족할 경우, 자동으로 풀의 크기를 확장할지 여부를 설정함
    • 필요에 따라 오브젝트를 더 생성할 수 있도록 설정할 수 있음
  • Recycle Objects (오브젝트 재활용)

    • 사용된 오브젝트를 풀에 반환할 때 어떤 방식으로 재활용할지를 정의함
    • 일반적으로 오브젝트의 상태를 초기화한 후 풀에 다시 넣음
  • Factory Method (팩토리 메소드)

    • 오브젝트를 생성할 때 사용하는 메소드
    • 특정한 초기화가 필요한 경우 이 메소드를 통해 오브젝트를 생성하고 풀에 넣을 수 있음


UnityEngine.Pool

○ 개념

  • 유니티에서 제공하는 객체 풀링 시스템을 다루는 네임스페이스

  • Object Pooling (객체 풀링)
    자주 생성되고 파괴되는 객체를 미리 생성하여 풀에 저장해 두었다가 필요할 때 다시 사용함. 이렇게 하면 매번 객체를 새로 생성하고 메모리를 할당하는 비용을 줄일 수 있음

  • Generic ObjectPool 클래스
    일반적인 풀링 기능을 제공. 풀에서 객체를 빌리고, 사용이 끝난 객체를 다시 반환하는 구조를 제공함

  • Collection Pooling
    리스트나 딕셔너리 같은 컬렉션 객체들을 풀링하여, 큰 리스트를 반복적으로 생성하거나 파괴하는 상황에서 메모리 사용을 최적화함

// CollectionPool을 사용하여 List<GameObject>를 필요할 때마다 풀에서 가져와 사용한 뒤, 다시 풀로 반환하는 방식
  
using UnityEngine;
using UnityEngine.Pool;
using System.Collections.Generic;

public class CollectionPoolingExample : MonoBehaviour
{
    public GameObject prefab;

    void Start()
    {
        // 오브젝트를 생성 및 관리할 리스트를 풀에서 가져옴
        List<GameObject> pooledObjects = CollectionPool<List<GameObject>, GameObject>.Get();

        // 리스트에 오브젝트 생성 후 추가
        for (int i = 0; i < 10; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.transform.position = new Vector3(i * 2.0f, 0, 0); // 오브젝트 위치 설정
            pooledObjects.Add(obj);
        }

        // 풀에서 가져온 리스트의 오브젝트 사용 (예: 위치 업데이트)
        foreach (var obj in pooledObjects)
        {
            obj.transform.position += Vector3.up;
        }

        // 리스트 사용이 끝나면 풀에 반환하여 메모리 해제
        CollectionPool<List<GameObject>, GameObject>.Release(pooledObjects);
    }
}
  • PooledObject
    구조체 풀에 반환될 객체의 생명주기를 관리함. 객체가 사용 중일 때 자동으로 풀에 반환되지 않도록 하거나, 사용 후 다시 풀로 돌려보내는 등의 기능을 제공함
// PooledObject를 사용하여 적 캐릭터를 자동으로 풀에 반환하는 방법
  
using UnityEngine;
using UnityEngine.Pool;

public class Enemy : MonoBehaviour
{
    public void OnSpawn()
    {
        // 적이 활성화될 때 필요한 초기화 코드
        Debug.Log("Enemy Spawned");
    }

    public void OnDespawn()
    {
        // 적이 비활성화되거나 풀로 돌아갈 때 필요한 정리 코드
        Debug.Log("Enemy Despawned");
    }
}

public class GameManager : MonoBehaviour
{
    // ObjectPool을 선언하여 적 객체 풀링 관리
    private ObjectPool<Enemy> enemyPool;

    public Enemy enemyPrefab; // 적 프리팹 연결

    void Start()
    {
        enemyPool = new ObjectPool<Enemy>(
            createFunc: () => Instantiate(enemyPrefab),  // 생성 함수
            actionOnGet: (enemy) => enemy.OnSpawn(),      // 풀에서 가져올 때 실행할 작업
            actionOnRelease: (enemy) => enemy.OnDespawn(),// 풀로 반환할 때 실행할 작업
            actionOnDestroy: (enemy) => Destroy(enemy)    // 객체 파괴할 때 작업
        );
    }

    public void SpawnEnemy(Vector3 position)
    {
        // PooledObject 사용하여 자동으로 풀에 반환되도록 함
        using (var pooledEnemy = enemyPool.Get(out var enemy))
        {
            enemy.transform.position = position;
            enemy.gameObject.SetActive(true);

            // 적이 특정 시간 후 비활성화되며 자동으로 풀로 반환됨
            Invoke(nameof(DeactivateEnemy), 3.0f);
        }
    }

    // 적을 비활성화하는 메서드
    private void DeactivateEnemy()
    {
        // 이 메서드에서는 적을 비활성화만 해도 자동으로 풀에 반환됨
    }
}

IObjectPool 인터페이스

  • Unity에서 제공하는 오브젝트 풀링 인터페이스

  • 오브젝트 풀을 관리하는 데 필요한 기본적인 기능들을 정의한 인터페이스

  • 이를 통해 오브젝트 풀링 시스템을 구현하거나 커스터마이징할 수 있음

  • 주요 기능은 풀의 오브젝트를 요청하고 반환하는 과정에 대한 규칙을 설정하는 것

  • IObjectPool 인터페이스는 풀링 시스템을 표준화하여 다양한 종류의 오브젝트 풀링에 동일한 방식으로 접근할 수 있게 해줌

  • Get()

    • 풀에서 오브젝트를 요청할 때 사용하는 메서드
    • 오브젝트 풀에서 사용 가능한 오브젝트를 반환하며, 사용 중인 오브젝트가 없을 경우 새로운 오브젝트를 생성하거나, null을 반환할 수 있음
  • Release(T element)

    • 사용한 오브젝트를 다시 풀에 반환할 때 사용하는 메서드
    • 이 메서드는 오브젝트의 상태를 초기화하고, 다음에 다시 사용할 수 있도록 풀에 저장하는 역할을 함
  • Clear()

    • 풀 안에 있는 모든 오브젝트를 정리하는 메서드
    • 메모리를 해제하거나, 더 이상 필요하지 않은 오브젝트를 삭제할 때 사용
  • CountInactive

    • 풀 안에서 현재 사용하지 않는 오브젝트의 수를 반환
    • 이를 통해 풀의 상태를 모니터링할 수 있음

ObjectPool 내부 코드

public class ObjectPool<T> : IDisposable, IObjectPool<T> where T : class
{
    internal readonly List<T> m_List;

    private readonly Func<T> m_CreateFunc;

    private readonly Action<T> m_ActionOnGet;

    private readonly Action<T> m_ActionOnRelease;

    private readonly Action<T> m_ActionOnDestroy;

    private readonly int m_MaxSize;

    internal bool m_CollectionCheck;

    public int CountAll { get; private set; }

    public int CountActive => CountAll - CountInactive;

    public int CountInactive => m_List.Count;

    public ObjectPool(Func<T> createFunc, Action<T> actionOnGet = null, Action<T> actionOnRelease = null, Action<T> actionOnDestroy = null, bool collectionCheck = true, int defaultCapacity = 10, int maxSize = 10000)
    {
        if (createFunc == null)
        {
            throw new ArgumentNullException("createFunc");
        }

        if (maxSize <= 0)
        {
            throw new ArgumentException("Max Size must be greater than 0", "maxSize");
        }

        m_List = new List<T>(defaultCapacity);
        m_CreateFunc = createFunc;
        m_MaxSize = maxSize;
        m_ActionOnGet = actionOnGet;
        m_ActionOnRelease = actionOnRelease;
        m_ActionOnDestroy = actionOnDestroy;
        m_CollectionCheck = collectionCheck;
    }

    public T Get()
    {
        T val;
        if (m_List.Count == 0)
        {
            val = m_CreateFunc();
            CountAll++;
        }
        else
        {
            int index = m_List.Count - 1;
            val = m_List[index];
            m_List.RemoveAt(index);
        }

        m_ActionOnGet?.Invoke(val);
        return val;
    }

    public PooledObject<T> Get(out T v)
    {
        return new PooledObject<T>(v = Get(), this);
    }

    public void Release(T element)
    {
        if (m_CollectionCheck && m_List.Count > 0)
        {
            for (int i = 0; i < m_List.Count; i++)
            {
                if (element == m_List[i])
                {
                    throw new InvalidOperationException("Trying to release an object that has already been released to the pool.");
                }
            }
        }

        m_ActionOnRelease?.Invoke(element); // 이 이벤트와 연결된 객체의 Relase 관련 함수 실행
        if (CountInactive < m_MaxSize)
        {
            m_List.Add(element);
        }
        else
        {
            m_ActionOnDestroy?.Invoke(element); // 이 이벤트와 연결된 객체의 Destroy 관련 함수 실행
        }
    }

    public void Clear()
    {
        if (m_ActionOnDestroy != null)
        {
            foreach (T item in m_List)
            {
                m_ActionOnDestroy(item);
            }
        }

        m_List.Clear();
        CountAll = 0;
    }

    public void Dispose()
    {
        Clear();
    }
}

0개의 댓글