본문 바로가기
게임개발/유니티 엔진

오브젝트 풀링(Object Pooling)

by do_ng 2024. 3. 6.

 

오브젝트 풀링 개념

 

LOL에서 게임이 끝나기 전까지 몇 초마다 미니언이 계속해서 나온다.

미니언이라는 오브젝트를 계속해서 생성하게 되면 메모리의 사용량과 CPU의 연산이 점차 커지게 된다. 

(미니언을 매번 인스턴스화해서 메모리에 올리고 해당 미니언의 위치를 씬에 배치하는 작업이 계속 수행된다 하면 메모리 사용량과 CPU 연산이 점차 커질 수밖에 없음)

그리고 미니언이 죽게되면 메모리에서 해제가 되는 방식으로 구현되었다. 

 

출처:https://whiny.tistory.com/17

이런 식으로 객체를 생성하고 객체의 사용이 끝나면 메모리에서 아예 날려버리는 방식으로 구현되니까 실시간으로 돌아가는 대규모 게임 같은 경우 성능이 크게 저하될 수 있다.  

 

이러한 문제를 해결하기 위한 최적화 기법을 오브젝트 풀링(Object Pooling)이라고 한다. 

즉, Pool을 만들고 객체가 필요할 때는 Pop(Pool에서 객체를 가져옴)을 해서 사용하고 객체가 필요 없어지면 Push(Pool에 넣음)를 통해서 최적화를 할 수 있다. 

 

 

실습

 

1. 각각의 풀링 대상 오브젝트에 대한 정보를 담을 클래스 

 

각 스테이지 별로 생성되는 몬스터A, 몬스터B에 대해서 따로 각각 풀을 만들어서 관리하고 스테이지 별로 생성되는 몬스터 개수가 다르기 때문에 풀의 오브젝트 개수를 유동적으로 변경한다.

public class PoolObjectData
{  
    public int PoolObjectCount { get; private set; }
    public GameObject Original { get; private set; }
    public Transform Root { get; set; }

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

    public void Init(GameObject original,int poolObjectCount)
    {
        Original = original;
        PoolObjectCount = poolObjectCount;
        // Hierarchy에 명시적으로 어떤 몬스터를 풀링하고 있는지 네이밍 처리
        Root = new GameObject().transform;
        Root.name = $"{original.name}_Pool";

        for (int i = 0; i < PoolObjectCount; 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);        

        _poolStack.Push(poolable);
    }

    public Poolable Pop()
    {
        Poolable poolable;

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

        poolable.gameObject.SetActive(true);
        poolable.transform.SetParent(null);

        return poolable;
    }
    
}

 

 

2. 풀링 대상

 

해당 클래스를 컴포넌트로 가지고 있는 오브젝트에 한해서 풀링을 적용한다.

public class Poolable : MonoBehaviour
{    
    public void InitBeforeInactive()
    {
        // 풀링 대상인 몬스터인 경우 초기화(왜냐하면 다시 스폰이 될때 죽어있는 상태가 되어 있으면 안되므로)
        // 풀링 대상 몬스터 죽음 -> 해당 몬스터를 풀로 이동 -> 풀에서 해당 몬스터를 꺼내옴 -> 몬스터 활성화 처리 -> 몬스터 정보 초기화
        gameObject.transform.position = new Vector3(0, 0, -15); // 스폰 위치
        gameObject.GetComponent<MonsterController>().State = Defines.State.Wait; // 상태
        gameObject.GetComponent<MonsterController>().Stat.SetStat(Managers.Game.CurrentChpater); // 스탯
    }
}

 

 

 

3. 풀을 생성하고 풀에서 오브젝트를 꺼내고 집어넣는 역할을 관리하는 풀 매니저 클래스

 

풀 매니저의 주요 기능은 3가지 이다.

1. CreatePool : 풀링을 적용할 게임 오브젝트 별로 각각 풀을 만든다.

2. Pop : 해당되는 풀에서 오브젝트를 꺼내온다.

3. Push : 오브젝트를 해당 풀에 집어 넣는다. 

public class PoolManager
{    
    // 여러개의 풀을 관리하기 위한 컨테이너
    Dictionary<string, PoolObjectData> _pool = new Dictionary<string, PoolObjectData>();
    
    Transform _root;

    public void Init()
    {
        if(_root == null)
        {
            _root = new GameObject { name = "@Pool_Root" }.transform;
        }
    }
    
    public void CreatePool(GameObject original)
    {
        int poolObjectCount = 0;
        if (Managers.Game.CurrentChpater == 1)
            poolObjectCount = 10;
        else if (Managers.Game.CurrentChpater == 2)
            poolObjectCount = 15;
        else
            poolObjectCount = 20;

        PoolObjectData pool = new PoolObjectData();
        pool.Init(original, poolObjectCount);
        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); // Pool이 없으니까 오브젝트를 그냥 메모리에서 해제
            return;
        }

        _pool[name].Push(poolable);
    }  

    // 풀에서 오브젝트를 꺼내옴
    public GameObject Pop(GameObject original, Transform parent = null)
    {
        if (_pool.ContainsKey(original.name) == false)
            CreatePool(original);

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

    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();
    }
}

 

스테이지가 시작되면 해당 스테이지에 설정된 몬스터 개수만큼 몬스터를 풀에서 가져오고 몬스터가 죽으면 풀에 넣어서 다시 재생성되기를 기다린다. 

public void MakeMonsterSpawn()
{
    GameObject go = new GameObject { name = "MonsterSpawn" };
    monsterSpawning = go.GetOrAddComponent<MonsterSpawn>();
    if (Managers.Game.CurrentChpater == 1)        
        monsterSpawning.SetKeepMonsterCount(10);
    else if (Managers.Game.CurrentChpater == 2)
        monsterSpawning.SetKeepMonsterCount(15);
    else
        monsterSpawning.SetKeepMonsterCount(20);
}
void Update()
{
    while(_reserveCount + _monsterCount < _keepMonsterCount)
    {
        StartCoroutine("ReserveSpawn");
    }
}

 

 


참고 : https://rito15.github.io/posts/unity-object-pooling/#3-%EC%B0%B8%EA%B3%A0%EC%82%AC%ED%95%AD

'게임개발 > 유니티 엔진' 카테고리의 다른 글

Time.DeltaTime  (0) 2024.03.09
Coroutine(코루틴)  (0) 2024.03.07
UI 이벤트 시스템  (0) 2024.03.04
UI 자동화  (0) 2024.03.03
UI 앵커 사용법  (0) 2024.02.27