프레임 속도를 유지하면서 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됩니다.