Unity

[Unity] Pooling에서 Queue와 Stack 의 선택 (지시어 사용)

박도치 2024. 11. 19. 18:52

오브젝트 풀링에서 최초에 Stack을 통한 오브젝트 풀링을 사용하고 있었다. Stack을 사용하는 방식과 Queue를 사용하는 방식의 차이점에 대해서 설명 및 왜 이러한 선택을 했는지에 대해 아래에 서술하겠다.

 

1. Stack과 Queue의 주요한 차이점

StackLIFO 으로 나중에 들어온게 처음 나가는 형식이다. 택배 상자를 쌓는다는 느낌으로 보면된다. 가장 아래에 있는걸 꺼내려면 이전에 넣었던 것들을 먼저 꺼내야 하기 떄문이다.

 

QueueFIFO 으로 처음 들어온게 먼저 나가는 형식이다. 이는 양 쪽이 뚫린 기다란 원통에 당구 큣대로 공을 하나씩 밀어넣는 느낌 으로 생각하면 된다. 그러면 입구에 처음 들어간 당구공이 반대쪽 출구에 먼저 나가기 때문이다.

 

Stack의 경우 마지막으로 생성된 객체가 가장 먼저 풀링되어야 할 경우에 많이 사용되고, Queue는 반복적으로 등장하는 경우에 많이 사용된다.

 

2. Queue로 변경한 이유

디펜스 게임에서 정해진 Count의 몬스터를 생성하는데, 몬스터가 중간에 죽을 경우 죽었던 몬스터가 다시 생성되면서 A* 알고리즘을 못찾는 문제가 생겼다.

 

Destroy된 몬스터를 다시 Reset해주면서 A* 경로도 초기화 해주는 방식을 채택하였으나, 너무 빨리 죽거나 어떠한 이유로 버그가 가끔 터지는 불안정성이 생겼다.

 

그래서 Queue를 통해 정해진 몬스터를 시작시에 대기시켜두고, 시작하면 찾아가면서 풀링하도록 이를 해결하였다.

 

3. 테스팅을 위해 지시어 (#define)을 활용

C언어 에는 지시어가 존재하는데, 그중에 #define을 이용하면 #define 로 조건부 컴파일을 할 수 있다.

 

#define 지시어 로 #if 와 else를 편리하게 관리할 수 있다.

 

이를 통해 Queue와 Stack의 차이를 실시간으로 체크할 수 있으며, 굳이 전체를 주석으로 하면서 불편하게 하지않고 한 줄을 통해 테스트가 가능하다.

 

// #define USE_STACK // 스택 사용 활성화. 주석 처리하면 큐 방식으로 변경.

using System.Collections.Generic;
using UnityEngine;

public class PoolManager : MonoBehaviour
{
    #region Pool
    class Pool
    {
        public GameObject Original { get; protected set; }
        public Transform Root { get; protected set; }

#if USE_STACK
        protected Stack<Poolable> _poolStack = new Stack<Poolable>(); // 스택
#else
        protected Queue<Poolable> _poolQueue = new Queue<Poolable>(); // 큐
#endif

        public virtual void Init(GameObject original, int count = 20)
        {
            Original = original;
            Root = new GameObject().transform;
            Root.name = $"@{original.name}_Root";

            for (int i = 0; i < count; i++)
                Push(Create());
        }

        protected Poolable Create()
        {
            GameObject go = Object.Instantiate<GameObject>(Original);
            go.name = Original.name;

            return go.GetOrAddComponent<Poolable>();
        }

        public virtual void Push(Poolable poolable)
        {
            if (poolable == null) return;

            poolable.transform.SetParent(Root);
            poolable.gameObject.SetActive(false);
            poolable.IsUsing = false;

#if USE_STACK
            _poolStack.Push(poolable); // 스택으로 Push
#else
            _poolQueue.Enqueue(poolable); // 큐로 Push
#endif
        }

        public virtual Poolable Pop(Transform parent)
        {
            Poolable poolable;

#if USE_STACK
            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop(); // 스택에서 Pop
            else
                poolable = Create();
#else
            if (_poolQueue.Count > 0)
                poolable = _poolQueue.Dequeue(); // 큐에서 Pop
            else
                poolable = Create();
#endif

            poolable.gameObject.SetActive(true);

            if (parent == null)
                poolable.transform.parent = GameManager.Instance.Scene.CurrentScene.transform;

            poolable.transform.SetParent(parent);
            poolable.IsUsing = true;

            return poolable;
        }
    }
    #endregion

    #region UI Pool
    class UIPool : Pool
    {
        public void Init(GameObject original, int instanceCount, int count = 20)
        {
            Original = original;
            Root = new GameObject().transform;
            Root.name = $"@{original.name}_Root{instanceCount}";

            for (int i = 0; i < count; i++)
                Push(Create(instanceCount));
        }

        public Poolable Create(int instanceCount)
        {
            GameObject go = Object.Instantiate<GameObject>(Original);
            go.name = $"{Original.name}_{instanceCount}";

            return go.GetOrAddComponent<Poolable>();
        }

        public override void Push(Poolable poolable)
        {
            base.Push(poolable);
        }

        public override Poolable Pop(Transform parent)
        {
            return base.Pop(parent);
        }
    }
    #endregion

    Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
    Dictionary<string, UIPool> _uiPool = new Dictionary<string, UIPool>();
    Transform _root;

    public void Init()
    {
        _root = gameObject.transform;
    }

    public void CreatePool(GameObject original, int count = 20, Transform parent = null)
    {
        Pool pool = new Pool();
        pool.Init(original, count);

        if (parent != null)
            pool.Root.parent = parent;
        else
            pool.Root.parent = _root.transform;

        _pool.Add(original.name, pool);
    }

    public void CreateUIPool(GameObject original, int instanceCount, int count = 20, Transform parent = null)
    {
        UIPool uiPool = new UIPool();
        uiPool.Init(original, instanceCount, count);

        if (parent != null)
            uiPool.Root.parent = parent;
        else
            uiPool.Root.parent = _root.transform;

        string originName = $"{original.name}_{instanceCount}";

        _uiPool.Add(originName, uiPool);
    }

    public void Push(Poolable poolable)
    {
        string name = poolable.gameObject.name;
        string customKey = poolable.CustomKey;
        if (_pool.ContainsKey(name))
        {
            _pool[name].Push(poolable);
        }
        else if (_uiPool.ContainsKey(customKey))
        {
            _uiPool[customKey].Push(poolable);
        }
        else
        {
            GameObject.Destroy(poolable.gameObject);
        }
    }

    public Poolable Pop(GameObject original, int count = 20, Transform parent = null, Transform rootParent = null)
    {
        if (_pool.ContainsKey(original.name) == false)
            CreatePool(original, count, rootParent);

        return _pool[original.name].Pop(parent);
    }

    public Poolable PopUI(GameObject original, int count = 20, Transform parent = null, Transform rootParent = null)
    {
        string poolKey = original.name;
        int instanceCount = 0;

        Transform topParent = original.transform;
        while (topParent.parent != null)
        {
            topParent = topParent.parent;
        }

        bool hasMonsterController = topParent.GetComponent<MonsterController>() != null;

        while (!hasMonsterController && _uiPool.ContainsKey($"{poolKey}_{instanceCount}"))
        {
            instanceCount++;
        }

        if (!hasMonsterController && !_uiPool.ContainsKey($"{poolKey}_{instanceCount}"))
            poolKey = $"{poolKey}_{instanceCount}";

        if (_uiPool.ContainsKey(poolKey) == false)
            CreateUIPool(original, instanceCount, count, rootParent);

        Poolable poolable = _uiPool[poolKey].Pop(parent);
        poolable.CustomKey = poolKey;

        return poolable;
    }

    public GameObject GetOriginal(string name)
    {
        if (_pool.ContainsKey(name) == false)
            return null;

        return _pool[name].Original;
    }
}

 

 

Stack을 사용하고 싶으면 #define의 주석만 풀면 되고, 반대로 Queue를 사용하고 싶으면 저 한 줄만 주석처리하면 보다 편리하게 테스팅을 진행할 수 있다.