오브젝트풀링
- 유니티 어셈블에서 제공하는 풀링 시스템을 이용한 코드 리팩토링
- 기존 풀필 시스템은 풀링의 갯수를 정해두고 미리 오브젝트를 풀링 해둔 상태로, 리스트에 담아서 하나하나 꺼내 쓰는 방법이었다면, 새로운 풀링은 동적으로 필요에 따라 생성하고 추가적으로 더 필요하다면 런타임에서 풀을 생성하여 계속해서 재활용하는 방법입니다.
기존 풀링 시스템
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ObstaclePool : MonoBehaviour
{
private List<GameObject> pooledObstacles;
public List<GameObject> obstaclePrefabs;
private int pooledAmount = 50;
void Start()
{
InitializePool();
}
private void InitializePool()
{
pooledObstacles = new List<GameObject>();
for (int i = 0; i < pooledAmount; i++)
{
int prefabIndex = Random.Range(0, obstaclePrefabs.Count);
GameObject obstacle = Instantiate(obstaclePrefabs[prefabIndex]);
obstacle.name = obstaclePrefabs[prefabIndex].name;
obstacle.SetActive(false);
obstacle.transform.SetParent(transform);
obstacle.transform.position = new Vector2(obstacle.transform.position.x, obstacle.transform.position.y);
pooledObstacles.Add(obstacle);
}
}
public GameObject GetPooledObstacle(int prefabIndex)
{
if (prefabIndex < 0 || prefabIndex >= obstaclePrefabs.Count)
{
return null;
}
for (int i = 0; i < pooledObstacles.Count; i++)
{
if (pooledObstacles[i] != null && !pooledObstacles[i].activeInHierarchy && pooledObstacles[i].name.Contains(obstaclePrefabs[prefabIndex].name))
{
return pooledObstacles[i];
}
}
GameObject newObj = Instantiate(obstaclePrefabs[prefabIndex]);
newObj.name = obstaclePrefabs[prefabIndex].name;
newObj.SetActive(false);
newObj.transform.position = new Vector2(newObj.transform.position.x, newObj.transform.position.y);
pooledObstacles.Add(newObj);
return newObj;
}
public void ReturnToPool(GameObject obstacle)
{
if (obstacle != null)
{
obstacle.SetActive(false);
}
}
public void ResetPool()
{
foreach (GameObject obstacle in pooledObstacles)
{
if (obstacle != null)
{
Destroy(obstacle);
}
}
pooledObstacles.Clear();
InitializePool();
}
}
- 위의 방법은 장애물을 풀링할때 얼마나 쓰일지 모르는 오브젝트들을 일괄적으로 50개씩 만들어 두어 하나하나 사용하는 방법으로 불필요한 리소스 낭비가 발생을 하는 문제가 있습니다.
개선된 풀링 방식
using Manager;
using UnityEngine;
using UnityEngine.Pool;
namespace Objects
{
public class Pool
{
private readonly GameObject _prefab;
private readonly IObjectPool<GameObject> _pool;
private Transform _root;
private Transform Root
{
get
{
if (_root != null) return _root;
GameObject obj = new()
{
name = $"[Pool_Root] {_prefab.name}"
};
Transform baseObstacleTransform = ServiceLocator.GetService<ObstacleManager>().BaseObstacle.transform;
obj.transform.SetParent(baseObstacleTransform, false);
_root = obj.transform;
return _root;
}
}
public Pool(GameObject prefab)
{
_prefab = prefab;
_pool = new ObjectPool<GameObject>(OnCreate, OnGet, OnRelease, OnDestroy);
}
public GameObject Pop()
{
return _pool.Get();
}
public void Push(GameObject obj)
{
if (obj == null) return;
_pool.Release(obj);
}
private GameObject OnCreate()
{
GameObject obj = Object.Instantiate(_prefab, Root, true);
obj.name = _prefab.name;
return obj;
}
private void OnGet(GameObject obj)
{
if (obj == null) return;
obj.SetActive(true);
}
private void OnDestroy(GameObject obj)
{
Object.Destroy(obj);
}
private void OnRelease(GameObject obj)
{
if (obj == null) return;
obj.SetActive(false);
}
}
}
-
using UnityEngine.Pool
을 사용하여 풀 구성을 합니다. -
private readonly IObjectPool<GameObject> _pool
인터페이스로 정의된 메서드로 풀 시스템을 만듭니다.IObjectPool
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
namespace UnityEngine.Pool
{
/// <summary>
/// <para>A stack based Pool.IObjectPool_1.</para> /// </summary> 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 => this.CountAll - this.CountInactive;
public int CountInactive => this.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(nameof (createFunc));
if (maxSize <= 0)
throw new ArgumentException("Max Size must be greater than 0", nameof (maxSize));
this.m_List = new List<T>(defaultCapacity);
this.m_CreateFunc = createFunc;
this.m_MaxSize = maxSize;
this.m_ActionOnGet = actionOnGet;
this.m_ActionOnRelease = actionOnRelease;
this.m_ActionOnDestroy = actionOnDestroy;
this.m_CollectionCheck = collectionCheck;
}
public T Get()
{
T obj;
if (this.m_List.Count == 0)
{
obj = this.m_CreateFunc();
++this.CountAll;
}
else
{
int index = this.m_List.Count - 1;
obj = this.m_List[index];
this.m_List.RemoveAt(index);
}
Action<T> actionOnGet = this.m_ActionOnGet;
if (actionOnGet != null) actionOnGet(obj);
return obj;
}
public PooledObject<T> Get(out T v)
{
return new PooledObject<T>(v = this.Get(), (IObjectPool<T>) this);
}
public void Release(T element)
{
if (this.m_CollectionCheck && this.m_List.Count > 0)
{
for (int index = 0; index < this.m_List.Count; ++index)
{
if ((object) element == (object) this.m_List[index])
throw new InvalidOperationException("Trying to release an object that has already been released to the pool.");
}
}
Action<T> actionOnRelease = this.m_ActionOnRelease;
if (actionOnRelease != null) actionOnRelease(element);
if (this.CountInactive < this.m_MaxSize)
{
this.m_List.Add(element);
}
else
{
Action<T> actionOnDestroy = this.m_ActionOnDestroy;
if (actionOnDestroy != null) actionOnDestroy(element);
}
}
public void Clear()
{
if (this.m_ActionOnDestroy != null)
{
foreach (T obj in this.m_List)
this.m_ActionOnDestroy(obj);
}
this.m_List.Clear();
this.CountAll = 0;
}
public void Dispose() => this.Clear();
}
}
- 미리
UnityEngine.Pool
네임스페이스에 선언된 Pool 시스템으로 가져와 사용합니다.
Get()
- 풀 리스트에 저장된 오브젝트를 불러옵니다.
- 이때 Pool 시스템의 Get을 통해 pool 리스트 내부의 오브젝트에 접근합니다.
Release
- 사용중이던 오브젝트를 비활성화 하여 다시 Get() 을 통해 사용가능하도록 준비합니다.
PoolManager
using System.Collections.Generic;
using Objects;
using UnityEngine;
namespace Manager
{
public class PoolManager
{
private readonly Dictionary<string, Pool> _pools = new();
public GameObject Pop(GameObject prefab)
{
if (!_pools.ContainsKey(prefab.name))
{
CreatePool(prefab);
}
return _pools[prefab.name].Pop();
}
public bool Push(GameObject obj)
{
if (!_pools.ContainsKey(obj.name)) return false;
_pools[obj.name].Push(obj);
return true;
}
private void CreatePool(GameObject prefab)
{
Pool pool = new(prefab);
_pools.Add(prefab.name, pool);
}
}
}
- 실제 스크립트 상에서 사용되는 Pool Manager 입니다.
-
Pop
,Push
를 사용하여 오브젝트가_pools
에 존재하지 않는다면 추가로 생성하고 풀에 넣어 꺼내씁니다. - Push 로는 사용한 오브젝트를 반환합니다.
리소스매니져와의 연결
public GameObject InstantiateObject(string key, Transform parent = null, bool pooling = false)
{
GameObject resource = Load<GameObject>($"{key}.prefab");
return pooling ? ServiceLocator.GetService<PoolManager>().Pop(resource) : Utility.InstantiateObject(resource, parent);
}
- 리소스 매니져와 연결하여 인스턴스를 생성시에 pooling의 bool 여부에 따라서 인스턴스를 생성 또는 pool 매니저에 넘겨 처리 합니다.
오브젝트 반환
protected virtual IEnumerator DeactivateAfterDelay()
{
yield return new WaitForSeconds(Delay);
ServiceLocator.GetService<PoolManager>().Push(gameObject);
}
- 사용을 다한 오브젝트는 Pool manager에서 Push로 해당 오브젝트를 반환합니다.
마치며
이미 내부적으로 구현이 완료가 되어있기 때문에 별도의 스크립작성 분량이 많지 않아서 빠르게 구현이 가능하며, 인터페이스로 구현이 되어 있어, 팀 협업시에도 풀링 작성시 통일된 스크립트를 제공할 수 있습니다.