[Unity] (2024_10_15) 2D 플랫 포머 게임 개발일지(Pooling)

2024. 10. 17. 00:51·Unity

저번에 말했던 맞을 때 생길 이펙트를 넣을 예정인데.... 중요한 점이 있다. Effect 같은 자주 생성되고 자주 쓰이는 객체를 계속 Instantiate로 생성해주는 게 맞을까? 유니티에도 자세히 다루는 내용인데. 생성과 삭제는 큰 메모리를 소비한다는 내용이다. 이게 왜 그렇냐 하면 유니티는 Object들에 많은 Component들과 정보들이 있다 이 객체를 메모리에 할당하고 실제 비주얼로 만들어주는 작업은 굉장히 많은 메모리 낭비가 있다는 것을 알 수 있다. 삭제도 똑같은 이유이다.

 

따라서 생성을 달리 해줄 필요가 있는데 이때 등장하는 개념이 Pooling이다.

풀링이 뭐냐? 객체를 미리 만들어둬서 필요할 때 꺼내쓰고 삭제 대신 반납을 하는 알고리즘을 말한다. 이렇게 하면?? 굳이 생성, 삭제를 할 필요가 없다.

게임 만들면서 처음 나오는 최적화 부분인데. 메니저로 처리해줄 생각이여서 같이 Monosingleton도 설명하겠다.

 

Script : MonoSingleton

using UnityEngine;

public class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance = null;
    private static bool IsDestroyed = false;

    public static T Instance
    {
        get
        {
            if (IsDestroyed == true)
            {
                _instance = null;
            }
            if (_instance == null)
            {
                _instance = FindObjectOfType(typeof(T)) as T;
                if (_instance == null)
                {
                    Debug.LogError(typeof(T).Name + " could not be found");
                }
                else
                {
                    IsDestroyed = false;
                }
            }
            return _instance;
        }
    }

    private void OnDestroy()
    {
        IsDestroyed = true;
    }
}

MonoSingleton은 Single패턴을 간단하게 만들기 위해서 만든 녀석이다. Singleton 패턴을 사용하고 싶다면 MonoSingleton을 상속받고 제네릭으로 타입을 정해주면 된다. 여튼 코드를 설명하겠다.

public class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour

제네릭 선언부분이다. where로 T를 MonoBehaviour타입만 T가 될 수 있게 해준다.

private static T _instance = null;
private static bool IsDestroyed = false;

T 타입 인스턴스와 현재 파괴되었는지 확인하는 불변수이다.

public static T Instance
{
    get
    {
        if (IsDestroyed == true)
        {
            _instance = null;
        }
        if (_instance == null)
        {
            _instance = FindObjectOfType(typeof(T)) as T;
            if (_instance == null)
            {
                Debug.LogError(typeof(T).Name + " could not be found");
            }
            else
            {
                IsDestroyed = false;
            }
        }
        return _instance;
    }
}

다른 스크립트에서 불러올때 사용할 프로퍼티다. 현재 파괴 되었다면 인스턴스가 없는 것이니 null을 반환해주고

그게 아니고 인스턴스가 null이라면 (인스턴스가 없음)

하이라키 창에서 T타입의 오브젝트를 찾아서(메니저는 한개니까)

인스턴스에 넣어준다. 그런데도 null이면 못 찾은 것이니 현재 T타입 스크립트를 가지고 있는 게임 오브젝트 없다고 Log를 출력해준다. 인스턴스가 null이 아니라면 IsDestoryed를 false로 해준다. (그리고 instance 반환)

Set은 없어도 됨 어차피 안바뀌기 때문임.

private void OnDestroy()
{
    IsDestroyed = true;
}

 

인스턴스 파괴 될때 IsDestroyed를 True로 해준다.

 

이러면 MonoSingleton은 끝이다.

본격적으로 Pooling으로 넘어가겠다.

 

Pooling

풀링은 객체들을 Stack으로 저장해둔다. 왜냐하면 계속해서 객체를 빼내면서 부족하다면 또 생성해주고 다시 반환해주는 역할을 하기 때문에 FIFO인 Stack이 가장 적합하다고 판단하였다.

 

Script : PoolableMono

using UnityEngine;

namespace ObjectPooling
{
    public abstract class PoolableMono : MonoBehaviour
    {
        public PoolingType type;
        public abstract void ResetItem();
    }
}

풀링을 하기 위해서 꼭 상속 받아야하는 클래스이다. 현재 item에 poolType과

namespace ObjectPooling
{
    public enum PoolingType
    {
        SwordHitEffect,
    }
}

 Item을 꺼냈을 때 할 함수인 ResetItem을 만들어줘야 한다.

Script : Pool

using System.Collections.Generic;
using UnityEngine;

namespace ObjectPooling
{
    public class Pool<T> where T : PoolableMono
    {
        private Stack<T> _pool = new Stack<T>();
        private T _prefab;
        private Transform _parent;

        private PoolingType _type;

        public Pool(T prefab, PoolingType type, Transform parent, int count)
        {
            _prefab = prefab;
            _type = type;
            _parent = parent;

            for(int i = 0; i < count; i++)
            {
                T obj = GameObject.Instantiate(_prefab, _parent);
                obj.type = _type;
                obj.gameObject.name = _type.ToString();
                obj.gameObject.SetActive(false);
                _pool.Push(obj);
            }
        }


        public T Pop()
        {
            T obj = null;
            if(_pool.Count <= 0)
            {
                obj = GameObject.Instantiate(_prefab, _parent);
                obj.type = _type;
                obj.gameObject.name = _type.ToString();
            }
            else
            {
                obj = _pool.Pop();
                obj.gameObject.SetActive(true);
            }
            return obj;
        }

        public void Push(T obj)
        {
            obj.gameObject.SetActive(false);
            _pool.Push(obj);
        }
    }

}

마찬가지로 제네릭을 사용한다. 제네릭 부분은 넘어가겠다.

private Stack<T> _pool = new Stack<T>();
private T _prefab;
private Transform _parent;

private PoolingType _type;

변수 선언 부분 PoolItem들 담아주는 Stack과 T타입 변수, 부모 Transfrom, Poolingtype이다.

public Pool(T prefab, PoolingType type, Transform parent, int count)
{
    _prefab = prefab;
    _type = type;
    _parent = parent;

    for(int i = 0; i < count; i++)
    {
        T obj = GameObject.Instantiate(_prefab, _parent);
        obj.type = _type;
        obj.gameObject.name = _type.ToString();
        obj.gameObject.SetActive(false);
        _pool.Push(obj);
    }
}

생성자이다. T 타입 인스턴스를 가지고 오고 type과 부모 그리고 생성할 갯수를 가지고 온다.

_prefab = prefab;
_type = type;
_parent = parent;

초기화 해주고....

for(int i = 0; i < count; i++)
{
    T obj = GameObject.Instantiate(_prefab, _parent);
    obj.type = _type;
    obj.gameObject.name = _type.ToString();
    obj.gameObject.SetActive(false);
    _pool.Push(obj);
}

반복하며 생성해주고 이름 type으로 설정해주고 SetActive꺼주고(씬에서 당장 작동을 안해야하니) Stack에 넣어준다. 받아온 Int값만큼 반복해준다.

public T Pop()
{
    T obj = null;
    if(_pool.Count <= 0)
    {
        obj = GameObject.Instantiate(_prefab, _parent);
        obj.type = _type;
        obj.gameObject.name = _type.ToString();
    }
    else
    {
        obj = _pool.Pop();
        obj.gameObject.SetActive(true);
    }
    return obj;
}

Pop은 꺼내오는 함수이다. 그러므로 T타입 인스턴스를 반환해준다 T타입변수 미리 선언해주고

스택이 비었다?? 그럼 새로 만들어준다. 아니라면 Stack에서 꺼내준다. 그리고 SetActive를 켜서 씬에서 작동하게 해주고 그 T타입 인스턴스를 반환해줌

public void Push(T obj)
{
    obj.gameObject.SetActive(false);
    _pool.Push(obj);
}

Push는 반환하는 함수이다. Stack에 넣어줘야하니 SetActive를 False로 해주고 스택에 넣어주면 끝이다.

 

Script : Pool

using UnityEngine;

namespace ObjectPooling
{
    [CreateAssetMenu(menuName = "SO/Pool/PoolItem")]
    public class PoolingItemSO : ScriptableObject
    {
        public string enumName;
        public string poolingName;
        public string description;
        public int poolCount;
        public PoolableMono prefab;

        private void OnValidate()
        {
            if(prefab != null)
            {
                if(enumName != prefab.type.ToString())
                {
                    prefab = null;
                    Debug.LogWarning("Type mismatch!");
                }
            }
        }
    }
}

ItemSO다 정보들을 SO로 저장할 생각이다. enumName은 파일 이름 poolingName은 pool의 이름, 그 다음 설명, 생성할 갯수, 생성할 Prefab를 변수로 들고 있다.

그리고  값이 바뀔 때 마다. Prefab이 null이 아닐때 enumName과 Prefab의 타입 이름이 다르면  prefab을 null로 하고 Log를 출력해준다. 나중에 자세히 다루고 넘어가겠다.

 

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

public class PoolingTableSO : ScriptableObject
{
    public List<PoolingItemSO> datas = new List<PoolingItemSO>();
}

 

Item들 담는 TableSO이다. 

Script : PoolManager

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

public class PoolManager : MonoSingleton<PoolManager>
{
    private Dictionary<PoolingType, Pool<PoolableMono>> _pools 
                        = new Dictionary<PoolingType, Pool<PoolableMono>>();

    public PoolingTableSO listSO;

    private void Awake()
    {
        foreach (PoolingItemSO item in listSO.datas)
        {
            CreatePool(item);
        }
    }

    private void CreatePool(PoolingItemSO item)
    {
        var pool = new Pool<PoolableMono>(item.prefab, item.prefab.type, transform, item.poolCount);
        _pools.Add(item.prefab.type, pool);
    }

    
    public PoolableMono Pop(PoolingType type)
    {
        if(_pools.ContainsKey(type) == false)
        {
            Debug.LogError($"Prefab does not exist on pool : {type.ToString()}");
            return null;
        }

        PoolableMono item = _pools[type].Pop();
        item.ResetItem();
        return item;
    }

    public void Push(PoolableMono obj, bool resetParent = false)
    {
        if (resetParent)
            obj.transform.SetParent( transform );
        _pools[obj.type].Push(obj);
    }
}

PoolManager는 Pool를 관리하고 다른 스크립트에서 Pop과 Push를 할 수 있도록 해주는 스크립트다. 싱글톤 패턴을 사용한다.

private Dictionary<PoolingType, Pool<PoolableMono>> _pools 
                    = new Dictionary<PoolingType, Pool<PoolableMono>>();

public PoolingTableSO listSO;

변수 선언 부분이다. 딕셔너리로 Pool을 관리 할 것이다.

Table도 들고 온다.

private void Awake()
{
    foreach (PoolingItemSO item in listSO.datas)
    {
        CreatePool(item);
    }
}

private void CreatePool(PoolingItemSO item)
{
    var pool = new Pool<PoolableMono>(item.prefab, item.prefab.type, transform, item.poolCount);
    _pools.Add(item.prefab.type, pool);
}

Table에 담겨있는 Pool들을 Awake할때 만들어줌
CreatePool함수는 매개변수로 item들고 오고 풀 만들어주고 type과 함께 딕셔너리에 넣어준다.

public PoolableMono Pop(PoolingType type)
{
    if(_pools.ContainsKey(type) == false)
    {
        Debug.LogError($"Prefab does not exist on pool : {type.ToString()}");
        return null;
    }

    PoolableMono item = _pools[type].Pop();
    item.ResetItem();
    return item;
}

Pop은 반환으로 PoolableMono(PoolItem)을 반환해준다. type을 매개변수로 들고 와서 딕셔너리에서 꺼낼 수 있게 한다.

먼저 딕셔너리에 이 poolType이 존재하지 않는다면 Error띄워주고 null 반환해준다.

있다면 딕셔너리에 꺼낸 뒤에 Pop함수써서 변수에 넣어주고 ResetItem함수 실행시켜주고 반환해준다.

public void Push(PoolableMono obj, bool resetParent = false)
{
    if (resetParent)
        obj.transform.SetParent( transform );
    _pools[obj.type].Push(obj);
}

Push는 반환해줄 item하고 item의 부모를 바꿀지 여부를 매개변수로 들고온다.

resetParent가 true라면 자기 자신으로 부모 바꿔주고

딕셔너리에 찾아서 push해준다.

 

사용하는 방법 다음 파트에서 다루도록 하겠다.

'Unity' 카테고리의 다른 글

[엔진 프로젝트]Enemy Walking Around with BT  (0) 2024.12.03
[엔진 프로젝트]Enemy Walking Around with BT  (0) 2024.11.01
[Unity] (2024_09_30) 2D 플랫 포머 게임 개발일지  (0) 2024.09.30
[Unity] (2024_09_25) 2D 플랫 포머 게임 개발일지 FSM Part 2  (0) 2024.09.26
[Unity] (2024_09_25) 2D 플랫 포머 게임 개발일지 FSM Part 1  (3) 2024.09.26
'Unity' 카테고리의 다른 글
  • [엔진 프로젝트]Enemy Walking Around with BT
  • [엔진 프로젝트]Enemy Walking Around with BT
  • [Unity] (2024_09_30) 2D 플랫 포머 게임 개발일지
  • [Unity] (2024_09_25) 2D 플랫 포머 게임 개발일지 FSM Part 2
HK1206
HK1206
고3 게임 개발자의 개발 일지
  • HK1206
    GGM-LJS
    HK1206
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • Unity (16)
      • Shader (1)
      • Editor (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
HK1206
[Unity] (2024_10_15) 2D 플랫 포머 게임 개발일지(Pooling)
상단으로

티스토리툴바