새소식

⇥ 2D Game/Unity

Unity 게임 만들기 프로젝트 - PoolManager

  • -
반응형

1.Pool Manager

Unity 에서 Object 가 필요할 때 마다 Load 로 불러오면 엄청난 과부하와 함께 성능의 하락을 가져올 수 있다.
그래서 필요하거나 동일한 Object의 Pool 을 생성하고 일정 개수를 Pool 이 가지고 있도록 하여 꺼내쓰면 Load 를 반복하는 횟수가 적어져서 성능의 향상을 가져올 수 있다.

# Poolable.cs

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

public class Poolable : MonoBehaviour
{
    public bool IsUsing;
}

단순히 Pooling 대상인지 확인할 수 있는 Component, 사용될 수 있으니 IsUsing 이라는 boolean 값도 하나 추가되어 있다.

 

# PoolManager.cs

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

public class PoolManager
{
#region Pool
    class Pool
    {
        public GameObject Original { get; private set; }
        public Transform Root { get; set; }

        Stack<Poolable> _poolStack = new Stack<Poolable>();

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

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

        }

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

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

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

            _poolStack.Push(poolable);
        }

        public Poolable Pop(Transform parent)
        {
            Poolable poolable;

            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop();
            else
                poolable = Create();

            poolable.gameObject.SetActive(true);

            //DontDestoryOnLoad 방지 . 
            if (parent == null)
                poolable.transform.parent = Managers.Scene.CurrentScene.transform;

            poolable.transform.parent = parent;


            poolable.IsUsing = true;

            return poolable;
        }
    }
#endregion

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

    public void Init()
    {
        if (_root == null)
        {
            _root = new GameObject { name = "@Pool_Root" }.transform;
            Object.DontDestroyOnLoad(_root);
        }
    }

    public void CreatePool(GameObject original, int count = 5)
    {
        Pool pool = new Pool();
        pool.Init(original, count);
        pool.Root.parent = _root;

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

    public void Push(Poolable poolable)
    {
        string name = poolable.gameObject.name;

        if (_pool.ContainsKey(name) == false)
        {
            GameObject.Destroy(poolable.gameObject);
            return;
        }

        _pool[name].Push(poolable);
    }

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

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

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

        return _pool[name].Original;
    }

    public void Clear()
    {
        foreach (Transform child in _root)
            GameObject.Destroy(child.gameObject);

        _pool.Clear();
    }
}

내부엔 PoolManager 가 관리할 Pool 이라는 class 가 있다.
이 Pool Class 에는 Original 이라는 원본 객체와 Root 라는 Pool 객체들이 모일 부모 컴포넌트를 가지고 있다.
그리고 _poolStack 이라는 Poolable 타입을 저장하는 Stack 구조가 선언되어 있다.

- Init 함수에선 original 을 입력받아서, original_Root 라는 GameObject 를 만들고 입력받은 Count만큼 생성하여 Create -> Push
- Create 함수는 Original 을 통해 Game Object 를 생성하고 이름을 동일하게 지정해준다 (Clone 제거)
그리고 여기서 생성된 GameObject 는 Pool 대상이기 때문에 Poolable 컴포넌트를 생성 및 연결하고 Poolable 을 반환한다.
- Push 함수는 Poolable 을 확인하여 아니라면 return, 맞다면 Root 의 자식 컴포넌트로 연결, SetActive, IsUsing 을 비활성화

- Pop 함수는 _poolStack 에 존재한다면 꺼내고, 없다면 Create 수행 SetActive 를 true 로 바꿔준다.
DontDestroyLoad 로 한번 가면 넣었다가 꺼낼 때 DDL 로 가기 때문에 Scene 으로 한번 갔다가 가도록 코드가 추가됨
매개변수로 받은 parent 를 부모로 설정하여 객체를 받아온다.


이제 PoolManager 는 위의 Pool 클래스와 매서드를 이용하여 관리하는 방안을 구현한다.

_pool 이라는 딕셔너리는 string key 값과 Pool 객체를 value 로 가지는 Dictionary 이다.
그리고 _root 라는 Transform 이 선언되어 있다.

- Init 함수는 _root 가 지정된 객체가 없다면 @Pool_Root 객체를 생성한다. 그리고 DontDestoryOnLoad 로 설정한다.
이제는 모든 Pool 객체는 @Pool_Root 밑으로 모이도록 설정한다.

- CreatePool 함수는 GameObject 와 Count 를 받고, 새로운 Pool 객체 생성, Pool의 Init 을 수행한다.
그리고 위에서 설정한 _root 를 부모로 설정하면 {original_name}_Root 라는 객체 밑에 5개의 Poolable 객체가 생성되고,
여기서 생성된 객체는 @Pool_Root 밑으로 모이게 된다.

Push 함수는 Poolable 을 매개변수로 받아서, 해당 GameObject 의 name 을 저장, 만약 _pool 에 없다면 해당 GameObject 를 삭제한다. 존재한다면 Pool 의 Push 를 사용하여 다시 Pooling 하게 된다.

Pop 함수는 Pool 에 존재한다면 Pop 을 이용해서 꺼낸 다음 객체를 반환하고, 존재하지 않는다면 Pool 을 생성한다.

- GetOriginal 함수는 pool에 없다면 null 반환, 존재한다면 Original 을 반환한다.

- Clear 함수는 모든 pool을 제거한다.

즉 _pool 이라는 Dictionary 는 string, Pool 객체로 이루어지고, Pool 객체 내부엔 Original 객체와 poolStack 을 가지고 있어서,
언제든 꺼내거나 다시 담을 수 있게 해준다.


이제 기존에 ResourceManager 에서 무조건 생성하던 부분이 Pool 에 있다면 꺼내쓰고, 없다면 생성하도록 수정되어야 한다. 

public T Load<T>(string path) where T : Object
    {
        if (typeof(T) == typeof(GameObject))
        {
            string name = path;
            int index = name.LastIndexOf('/');
            if (index >= 0)
                name = name.Substring(index + 1);

            GameObject go = Managers.Pool.GetOriginal(name);
            if (go != null)
                return go as T;
        }

        return Resources.Load<T>(path);
    }

단순히 로드하던 부분이, GameObject 일 때 Pool 에 존재한다면 GetOriginal 을 받아오도록 수정되었다.
Resources.Load 메서드는 부하가 있기 때문에 최소한으로 수행할 수 있도록 Pool에 있는 값을 가져온다.

public GameObject Instantiate(string path, Transform parent = null)
    {
        GameObject original = Load<GameObject>($"Prefabs/{path}");
        if (original == null)
        {
            Debug.Log($"Failed to load prefab : {path}");
            return null;
        }

        if (original.GetComponent<Poolable>() != null)
            return Managers.Pool.Pop(original, parent).gameObject;

        GameObject go = Object.Instantiate(original, parent);
        go.name = original.name;

        return go;
    }

Poolable 컴포넌트가 없다면 Pool 대상이 없기 때문에 기존과 동일하게 동작. 존재한다면 Pooling 대상 오브젝트이므로, 
Pool 의 Pop 메서드를 통해서 GameObject 를 반환해준다. 이 역시도 Instantiate 를 최소화 하여 기존 객체를 재활용한다.

public void Destroy(GameObject go)
    {
        if(go == null)
            return;

        // 풀링이 필요하면 풀링 매니저한테 위탁
        Poolable poolable = go.GetComponent<Poolable>();
        if (poolable != null)
        {
            Managers.Pool.Push(poolable);
            return;
        }

        Object.Destroy(go);
    }

Destroy 도 poolable 이 적용된 오브젝트라면 Push 를 통해 오브젝트를 삭제하지 않고 Pool 에 Push 하여 관리한다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.