본문 바로가기
Study

Unity, 오브젝트 풀링(Object Pooling)

by Client. DJ 2022. 9. 27.
반응형

Object Pooling

게임을 제작을 하다보면 '오브젝트 풀'이라는 것을 접하게됩니다. 먼저 풀(Pool)이라는 것에 대해서 알아보자면, 하나의 거대한 공간을 의미합니다. 또는 단어 의미 자체로만 본다면 수영장의 풀을 의미합니다. 이러한 풀(Pool)에 오브젝트를 담은 것을 '오브젝트 풀(Object Pool)'이라고 합니다.

 

이러한 오브젝트들을 모아두는 이유는 무엇일까요? 정확히 말을 하자면, 사용되지 않은 오브젝트들이 대기하고 있는 것을 의미합니다. 이를 재활용하기 위해서 풀이라는 곳에 담아두고 있습니다. 사용 대기를 하면서 두는 주목적은 리소스 또는 데이터의 재활용이라고 볼 수 있습니다.

예시

"전략 시물레이션 게임(RTS)에서 현재 50 vs 50 마리의 유닛이 서로 전투를 진행하고 있다. 얼마의 시간이 지나지 않아, 100마리 중 40마리가 남겨졌고, 진행 과정에서 유닛을 재생산한다. 새로운 유닛이 만들어질 때마다 새로 리소스로드 하지 않고, 이미 죽은 유닛의 리소스를 불러와 처음과 같은 세팅으로 설정하여 재사용을 한다."

 

예시에서 알 수 있 듯, Resources.Load()를 하지 않고, 이미 죽어서 사용할 일 없는 유닛에게 마치 새로 만들어진 유닛처럼 새로 세팅을 해줍니다. 이러한 방식으로 재활용을 합니다.

Resources.Load()를 반복적으로 사용하면 되지 않나요?

Resources.Load를 사용하는 경우, 사이즈가 큰 유닛(폴리곤이 많거나, 사용되는 데이터의 량이 많은)을 로드하면 때론 일순간 프리징이 걸립니다. 프리징이 걸리는 많큼 비용이 큰 함수를 사용하면 불리합니다. 하지만 게임 로딩하는 과정에서 일정량의 유닛을 미리 생성하여 오브젝트 풀에 넣고, 이후 게임 진행 과정에서 오브젝트 풀링하여 사용합니다. 그러면 큰 비용 없이, 큰 사이즈의 유닛을 사용할 수 있습니다. 또 한 풀링을 한다는 것은 메모리적으로도 유리합니다. 미리 로드가 되었고, 비활성화 된 유닛을 다시 재생성하면서 메모리 측면에서도 유리하게 사용할 수 있습니다. 이는 우리가 주로 캐싱(Caching)하는 과정과 결이 같다고 볼 수 있습니다.

오브젝트 풀링의 예시

조건

  1. 전략 게임에서 전투기를 무한 생성한다.
  2. 일정 거리 이동 뒤에 유닛을 삭제 처리한다.

오브젝트 풀링을 사용하지 않았을 경우 (X)

플레이어에게는 4~5마리가 보이지만, 내부적으로 무한에 가깝게 유닛이 생성되고 있다.

오브젝트 풀링을 사용했을 경우 (O)

실제로 사용되는 개수만큼, 유닛이 재활용되고 있다.

스크립트

ObjectPool.cs

using System;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    // 30초마다 오래 사용하지 않은 오브젝트를 삭제한다.
    private const float REFRESH_TIME_PER_SECONDS = 30f;

    private class PoolItem
    {
        public GameObject gameObject;

        public bool isActive => gameObject.activeSelf;

        public DateTime lastActiveTime = DateTime.Now;
    }

    #region Inspector

    public GameObject original;
    public int count = 10;

    #endregion;

    private List<PoolItem> _pool = new();

    private DateTime _lastRefreshedTime = DateTime.Now;

    private string _originalName = string.Empty;

    private bool _isInit = false;

    private void SetName(string ObjectName)
    {
        _originalName = ObjectName;
    }

    private void Start()
    {
        //original.SetActive(false);
        Initialize();
    }

    private void Update()
    {
        UpdateName();
    }

    private void Initialize()
    {
        if (!_isInit)
        {
            _isInit = true;
            SetName(this.gameObject.name);
            int tempCount = count;
            count = 0;
            for (int i = 0; i < tempCount; i++)
            {
                Create();
            }
        }
    }

    private PoolItem Create()
    {
        GameObject go = Instantiate(original, this.transform);
        go.transform.localPosition = Vector3.zero;
        go.transform.localRotation = Quaternion.identity;
        go.transform.localScale = Vector3.one;
        go.SetActive(false);
        PoolItem item = new PoolItem { gameObject = go };
        _pool.Add(item);
        count++;
        UpdateName();
        return item;
    }

    public GameObject Get()
    {
        if (!_isInit) Initialize();
        var item = _pool.Find(poolItem => !poolItem.gameObject.activeSelf);
        if (item == null) item = Create();
        item.lastActiveTime = DateTime.Now;
        AutoReleaseMemory();
        return item.gameObject;
    }

    public T Get<T>() where T : Component
    {
        if (!_isInit) Initialize();
        var item = _pool.Find(poolItem => !poolItem.gameObject.activeSelf);
        if (item == null) item = Create();
        item.lastActiveTime = DateTime.Now;
        AutoReleaseMemory();
        return item.gameObject.GetComponent<T>();
    }

    private void AutoReleaseMemory()
    {
        var nowTime = DateTime.Now;
        if (_lastRefreshedTime.AddSeconds(REFRESH_TIME_PER_SECONDS) < nowTime)
        {
            _lastRefreshedTime = nowTime;
            for (int i = _pool.Count - 1; i >= 0; --i)
            {
                var item = _pool[i];
                if (!item.gameObject.activeSelf && item.lastActiveTime.AddSeconds(REFRESH_TIME_PER_SECONDS) < nowTime)
                {
                    _pool.RemoveAt(i);
                    Destroy(item.gameObject);
                    count--;
                }
            }
        }
        UpdateName();
    }

    private void UpdateName()
    {
#if UNITY_EDITOR
        this.gameObject.name = $"{_originalName} ({_pool.FindAll(o => o.gameObject.activeSelf).Count}/{count})";
#endif
    }
}

오래동안 사용하지 않는 경우에도 자동으로 정리되는 코드를 삽입했습니다.

예제

Spawner.cs

using System.Collections;
using UnityEngine;

[RequireComponent(typeof(ObjectPool))]
public class Spawner : MonoBehaviour
{
    #region Inspector

    public ObjectPool objectPool;

    #endregion

    private IEnumerator Start()
    {
        var waitForSeconds = new WaitForSeconds(0.5f);
        while (true)
        {
            yield return waitForSeconds;
            var gameObject = objectPool.Get();
            gameObject.SetActive(true);
            gameObject.transform.localPosition = Vector3.left * 10f;
        }
    }
}

스포너 컴포넌트에, 오브젝트 풀 컴포넌트를 아래와 같이 연결해줬습니다.

마무리

유니티의 기본이 되는 풀링의 예제입니다. 캐싱(Caching)의 여러 형태 중, 하나라고 할 수가 있겠습니다. 개인적으로 필요하면 그때그때마다, 템플릿에 맞게 새로 작성하곤 합니다. 위 스크립트는 하나의 예시이며, 개념만 이해를 했다면 얼마든지 응용이 가능합니다.😁

 

반응형

댓글