본문 바로가기

카테고리 없음

디자인 패턴) 오브젝트 풀 패턴 Object Pool Pattern _ Unity C#

https://github.com/PacktPublishing/Game-Development-Patterns-with-Unity-2021-Second-Edition/tree/main/Assets/Chapters/Chapter08/Optimizing with the Object Pool

 

 

프레임 속도를 유지하면서 CPU에 부담을 주지 않으려면, 자주 생성되는 요소를 위해 일부 메모리를 예약해두고, 반복 활용하는 것이 좋습니다.

메모리에서 없애는 대신, 다시 사용할 수 있도록 Object Pool에 추가합니다. 새로운 인스턴스를 로드하는 초기의 초기화 비용이 들지 않을 수 있으며 다시 사용할 수 있는 엔티티를 파괴하지 않아 GC의 청소 주기를 낭비하지 않습니다.


오브젝트 풀 패턴 이해하기

queue 혹은 stack 형식의 풀은 초기화된 오브젝트 목록을 메모리에 남겨둡니다. 클라이언트는 오브젝트 풀에 특정 종류의 오브젝트 인스턴스를 요청합니다.

요청한 오브젝트 인스턴스가 사용 가능하면, 풀에서 가져와 클라이언트에게 제공합니다. 풀 내의 인스턴스가 충분하지 않아, 주어진 시간 내에 가져올 수 없으면 새로운 인스턴스가 동적으로 생성되고 최대 개수까지는 풀에 추가됩니다.

풀에서 가져온 인스턴스는,  클라이언트가 다 사용하고 나면  다시 풀로 되돌아가려고 합니다. 풀에 여유 공간이 없으면 인스턴스를 파괴합니다.

풀은 지속적으로 채워지며 일시적으로 비워질 수 있지만, 넘치지는 않습니다. 따라서, 메모리 사용량이 일관적입니다.


장점

  • 메모리 사용량이 예측 가능합니다. 특정한 종류의 객체 인스턴스를 특정한만큼 오브젝트 풀을 사용하여 유지하기에, 예측 가능한 만큼의 메모리를 할당할 수 있습니다.
  • 이미 메모리에서 객체들은 초기화되기에, 새로운 객체의 초기화에 드는 로딩 비용이 필요 없습니다.

단점

  • c#과 같은 최신 관리 프로그래밍 언어는 이미 메모리 할당을 최적으로 관리하기에 대부분 오브젝트 풀 패턴이 불필요하다고 말하기도 합니다. C#의 자동 메모리 관리 덕분에 대부분의 경우 오브젝트 풀링은 불필요할 수 있습니다. 그러나, 고성능 애플리케이션에서는 메모리 할당과 해제를 줄이기 위해 여전히 오브젝트 풀링이 유용할 수 있습니다.
  • 잘못 처리한 경우, 초기화된 상태가 아닌 다른 상태로 풀에 되돌아오는 예측 불가능한 객체 상태가 발생할 수 있습니다. 풀에 들어온 객체가 손상되거나 파괴될 수 있는 경우에 문제가 됩니다. ex) 플레이어가 방금 죽은 적 객체가 체력회복 없이 풀로 반환되고, 오브젝트풀이 클라이언트에게 해당 인스턴스를 전달하면 씬에 죽은 상태로 리스폰 됩니다.

사용하는 경우

총알, 파티클, 적 처럼 게임 플레이 흐름에서 자주 생성되고 파괴되는 엔티티에 풀링을 적용하면 생성과 파괴 같은 반복적인 수명 함수의 호출을 줄여 CPU 부담을 완화할 수 있습니다.


오브젝트 풀 패턴 구현하기

유니티 API를 이용하여 구현해볼 것입니다. (https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Pool.ObjectPool_1.html )

유니티 API코드

IObjectPool<T> 인터페이스

CountInactive 프로퍼티와 Get, Release, Clear 메소드로 풀을 이용합니다.

namespace UnityEngine.Pool
{
    public interface IObjectPool<T> where T : class
    {
        int CountInactive { get; }

        T Get();

        PooledObject<T> Get(out T v);

        void Release(T element);

        void Clear();
    }
}

ObjectPool<T> 클래스

using System;
using System.Collections.Generic;

namespace UnityEngine.Pool
{
    //
    // 요약:
    //     A stack based Pool.IObjectPool_1.
    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);
            if (CountInactive < m_MaxSize)
            {
                m_List.Add(element);
            }
            else
            {
                m_ActionOnDestroy?.Invoke(element);
            }
        }

        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();
        }
    }
}

오브젝트 풀 적용하기

IObjectPool<T> 인터페이스로 선언하여 CountInactive 프로퍼티와 Get, Release, Clear 메소드로 풀을 이용합니다.

Drone 클래스 OnDisable에서 초기화해줘야합니다.

using System.Collections;
using UnityEngine;
using UnityEngine.Pool;

public class Drone : MonoBehaviour
{
    public IObjectPool<Drone> Pool { get; set; }

    [SerializeField]
    private float _currentHealth;
    private float _maxHealth = 100f;

    [SerializeField]
    private float _timeToSelfDestruct = 3f;

    void Start()
    {
        _currentHealth = _maxHealth;
    }

    void OnEnable()
    {
        AttackPlayer();
        StartCoroutine(SelfDestruct());
    }

    public void AttackPlayer()
    {
        Debug.Log("Attack Player!");
    }

    IEnumerator SelfDestruct()
    {
        yield return new WaitForSeconds(_timeToSelfDestruct);
        TakeDamage(_maxHealth);
    }
    public void TakeDamage(float amount)
    {
        _currentHealth -= amount;
        if (_currentHealth <= 0f)
            ReturnToPool();
    }
    private void ReturnToPool()
    {
        Pool.Release(this);
    }

    void OnDisable()
    {
        ResetDrone();
    }

    private void ResetDrone()
    {
        _currentHealth = _maxHealth;
    }
        
}

Drone의 오브젝트 풀

ObjectPool 구현체가 아닌, IObjectPool 인터페이스를 사용할 것!!

using UnityEngine;
using UnityEngine.Pool;

public class DroneObjectPool : MonoBehaviour
{
    public int maxPoolSize = 20; // 풀 최대 크기
    public int stackDefaultCapacity = 10; // 풀 초기 크기 ( maxPoolSize까진 확장 가능)

    private IObjectPool<Drone> _pool; // IObjectPool 인터페이스를 사용할 것!!**
    public IObjectPool<Drone> Pool    // IObjectPool 인터페이스를 사용할 것!!**
    {
        get
        {
            if (_pool == null)
            {
                _pool = new ObjectPool<Drone>(CreatedPooledItem, OnTakeFromPool,
                        OnReturnedToPool, OnDestroyPoolObject,
                        true, stackDefaultCapacity, maxPoolSize );
            }
            return _pool;
        }
    }

    #region Pool Callbacks
    
    private Drone CreatedPooledItem()
    {
        GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
        Drone drone = go.AddComponent<Drone>();
        go.name = "Drone";
        drone.Pool = Pool;

        return drone;
    }

    private void OnTakeFromPool(Drone drone)
    {
        drone.gameObject.SetActive(true);
    }
    private void OnReturnedToPool(Drone drone)
    {
        drone.gameObject.SetActive(false);
    }

    private void OnDestroyPoolObject(Drone drone)
    {
        Destroy(drone.gameObject);
    }
    #endregion

    public void Spawn()
    {
        int ammount = UnityEngine.Random.Range(1, 10);
        for (int i = 0; i < ammount; i++)
        {
            Drone drone = Pool.Get();
            drone.transform.position = UnityEngine.Random.insideUnitCircle * 10;
        }
    }

}

구현 테스트하기

public class ClientObjectPool : MonoBehaviour
{
    private DroneObjectPool _pool;

    void Start()
    {
        _pool = gameObject.AddComponent<DroneObjectPool>();
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Spwan Drones"))
        {
            _pool.Spawn();
        }
    }
}

 

 

MaxPoolSize를 20으로 설정했기에, 최대 20개의 인스턴스까지 Pool이 적용됩니다. 20개 넘게 생성된 그 외의 오브젝트들은 Pool에 반환되지 않고 Destroy됩니다.