비트를 쪼개는 개발자

allen321@naver.com

Unity

Unity [디자인 패턴] - 오브젝트 풀링 패턴

MozarTnT 2024. 4. 30. 13:03
728x90
반응형

 

 

 

오브젝트 풀링이란?

 

 

Unity에서 빈번하게 사용하는 객체 및 오브젝트에 대하여 매번 새로 생성하고 삭제하는 것이 아닌

재사용이 가능하도록 특정 자료구조에 모아놓는 방식이다.

 

유니티에서 오브젝트를 생성하거나 삭제할때에는 Instantiate()와 Destroy() 함수를 사용하는데,

이 두 함수는 오브젝트의 리소스를 새로 할당하는 과정에서 많은 시간과 메모리를 잡아먹는 함수이다.

 

또한 오브젝트를 파괴하고 난 뒤에도 파괴 이후에 발생하는 가비지 컬렉팅으로 인하여 프레임 드랍이 발생하기도 하고 

반복적인 파괴 이후에는 메모리 단편화가 일어나기도 한다.

 

 

 

오브젝트 풀링의 장점은?

 

오브젝트를 미리 한번에 여러개를 생성시켜 놓기 때문에 게임 플레이 도중에 오브젝트 생성으로 인한 프레임 드랍이나 리소스를 잡아먹는 일을 미리 방지할 수 있다.

 

또한 오브젝트를 파괴하는 것이 아니라 일시적으로 비활성화와 다시 활성화를 반복하기 때문에 메모리 단편화 문제가 발생하지 않는다. 

 

 

오브젝트 풀링의 단점은?

 

다만 오브젝트 풀링을 한다는것은 결국 미리 수많은 오브젝트를 생성해 놓는 것이기 때문에 

 

이 오브젝트들은 필연적으로 메모리를 잡아먹고 있게 된다.

 

따라서 오브젝트 사용 여부와 관계없이 메모리를 잡아먹을 것을 미리 계산하고 오브젝트 풀의 크기를 

 

너무 크지도 않지만 모자라지도 않게 잘 관리해야 하는것이 핵심이다 !

 

 

  

 

 

간단한 슈팅 2D게임에서 플레이어의 총알을 Pooling으로 구현해보도록 하자.

 

우선 자료구조중 Queue를 활용해서 구현해 볼 텐데

 

Bullet 이라는 발사할 총알에 해당하는 Prefab을 만들어보자.

 

 

간단한 Sprite Renderer와 충돌을 구현할 Collider 그리고 위로 발사시키는데 필요한 이동 명령을 내릴 스크립트를 하나 넣어준다.

 

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

public class Bullet : MonoBehaviour
{
    enum Direction
    {
        UP,
        DOWN,
    }
    public float Speed { get; set; } = 1.0f;
    public int Power { get; set; } = 1;
    [SerializeField] private Direction dir;
    
    void Update()
    {
        if (GameUI.Instance.gameState != GameState.Play)
            return;

        if (dir == Direction.UP)
        {
            transform.Translate(Vector2.up * Time.deltaTime * Speed);
        }
        else
        {
            transform.Translate(Vector2.down * Time.deltaTime * Speed);
        }
    }
}

 

스크립트는 Direction 으로 위로 발사할지, 밑으로 발사할지만 정해주고

 

Update에서 Vector값과 Speed 값을 곱해 tranform.Translate로 계속 이동시켜주면 간단하게 총알은 움직인다.

 

이제 이 Bullet Prefab을 Instantiate와 Destroy 해줄 Pooling에 관련한 작업을 해 보자.

 

 

 

 

 

 

하이어라키 창에 Pooling을 전담해줄 Pool 이라는 Object를 만들고 Pool을 관리할 스크립트를 넣어준다.

 

 

 

 

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

public class Pooling : Singleton<Pooling>
{
    #region 플레이어 총알 Pooling
    [SerializeField] private Bullet pBullet;
    [SerializeField] private Transform pBulletParent;

    Queue<Bullet> pBulletQ = new Queue<Bullet>();

    public Bullet GetPBullet()
    {
        Player p = GameManager.instance.p;
        Bullet qBullet = null;
      
        if(pBulletQ.Count == 0) 
        {
            Bullet b = Instantiate(pBullet, p.transform.GetChild(0).position, Quaternion.identity);
            b.transform.SetParent(pBulletParent);
            pBulletQ.Enqueue(b);

            qBullet = pBulletQ.Dequeue();   
        }
        else
        {
            qBullet = pBulletQ.Dequeue();
            qBullet.transform.position = p.transform.GetChild(0).position;
            qBullet.gameObject.SetActive(true);
        }

        return qBullet;
    }
}

 

 

 

Pooling 을 관리해줄 인스턴스는 하나만 만들 예정이니까 싱글톤으로 선언해주고,


만든 Bullet Prefab과 이를 모아놓을 빈 GameObject를 각각 [SerializeField]로 Inspector에서 연결 해준다.

Bullet을 담아줄 Queue 역시 하나 새로 선언해주자.

 

 

 

GetPBullet()이 Bullet 을 Instantiate 해주는 역할을 수행할텐데 


Queue에 남은 Bullet이 없다면 Bullet 새로 Instantiate 한 후 Enqueue, Dequeue 해주고.


Bullet이 있으면 바로 Queue에 담긴 Bullet 하나를 Dequeue 해 주는 방식으로 구현했다.


return 값으로는 Queue 에서 뽑아낸 Bullet을 넘겨주면 쉽게 구현이 끝난다.

public void AddpBullet(Bullet b)
{
    b.gameObject.SetActive(false);
    pBulletQ.Enqueue(b);
}

 

 

AddpBullet은 Bullet 이 특정 범위를 넘어가거나 적 Prefab에 충돌한 후 사라지게 하고 싶을때,



즉 평소라면 Destroy 처리를 해줄 상황에서 사용해준다. 



매개변수로 받아온 Bullet을 SetActive(false) 로 비활성화 한 후 조금 전에 만들어놓은  



Queue 에 넣어주면 끝 !

 

 

 

게임 플레이 상황에서 잘 돌아가는지 확인해보면

 

정상적으로 PBullet이 생성 및 SetActive가 반복적으로 활성화 및 비활성화 되는것 을 볼 수 있다 !

 

꼭 Queue 가 아니더라도 Unity 에서 제공하는 Pooling 시스템을 사용해도 되고, 다른 자료구조를 이용해서

 

Pooling은 다양한 방법으로도 구현이 가능하다.

 

 

 

 

 

 

 

이미지 출처 : https://sj-jason-liu.medium.com/game-design-pattern-object-pooling-game-dev-series-135-fd3cfedd852

728x90
반응형