[TIL] ObjectPool 오브젝트 풀링

Dreamer·2024년 10월 22일

1. 오늘 주제

오늘 주제도 역시 수준별 학습 문제중 하나인 Pooling 에 대해 작성해 보려한다.
Object Pooling은 유니티 말고도 많은 게임 개발에 사용되는 일종의 패턴 방식이다. 실제로 오브젝트를 생성하고 파괴하는 일은 게임 세계에서는 엄청난 리소스를 잡아먹는데, 이런 리소스 소모를 최소화 하기 위해 생겨난 기법이라 할 수 있다.

2. Code

이제 바로 코드를 살펴보면 ObjectPool 에는 인스턴스 생성당시 Prefab을 등록하고 Pooling 방식을 담당하는 클래스이다.

public class ObjectPool
{
    private IObjectPool<GameObject> pool;

    private string name;
    private GameObject prefab;
    private Transform parent;

    public ObjectPool (string name, GameObject prefab, Transform parent = null, int defaultCapacity = 10, int maxSize = 100)
    {
        this.name = name;
        this.prefab = prefab;
        this.parent = parent;
        // 유니티에서 자체적으로 Pooling 할 수 있는 ObjectPool을 사용했다.
        this.pool = new ObjectPool<GameObject>(
            createFunc: CreateObject,
            actionOnGet: GetObject,
            actionOnRelease: ReleaseObject,
            actionOnDestroy: DestoryObject,
            defaultCapacity: defaultCapacity,
            maxSize: maxSize
            );
    }

    public GameObject Spawn()
    {
    	// 외부에서는 Spawn 함수로 풀링 대상을 가져온다.
        return pool.Get();
    }

    public void Despawn(GameObject go)
    {
    	// 외부에서는 사용하지 않는 오브젝트를 비활성 시킨다.
        pool.Release(go);
    }
 
    private GameObject CreateObject()
    {
        GameObject go = GameObject.Instantiate(prefab, parent);
        go.name = name;
        return go;
    }

    private void GetObject(GameObject obj)
    {
        obj.gameObject.SetActive(true);
    }

    private void ReleaseObject(GameObject obj)
    {
        obj.gameObject.SetActive(false);
    }

    private void DestoryObject(GameObject obj)
    {
        GameObject.Destroy(obj);
    }
}

PoolManager에서는 Pool 대상이 되는 Prefab 이름을 받아서 ObjectPool을 Dictionary에 등록하고 대상유무를 확인하여 오브젝트를 Pooling 하여 사용할 수 있도록 해준다.

public class PoolManager : Singleton<PoolManager>
{
    private const int defaultCapacity = 50;
    private const int maxSize = 300;

    Dictionary<string, ObjectPool> pools = new Dictionary<string, ObjectPool>();

    public ObjectPool CreatePool(string poolName, GameObject prefab)
    {
        if (pools.ContainsKey(poolName))
        {
            Debug.LogWarning("Pool with name " + poolName + " already exists");
            return null;
        }

        var pool = new ObjectPool(poolName, prefab, transform, defaultCapacity, maxSize);
        pools.Add(poolName, pool);

        return pool;
    }

    public GameObject Spawn(string name, Vector3 pos)
    {
        if (pools.TryGetValue(name, out ObjectPool pool) == false)
        {
            
            GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>($"Assets/Prefabs/{name}.prefab");
            // 풀링 대상이 없으면 생상허여 Dictionary에 등록해놓는다.
            pool = CreatePool(name, prefab);
        }

        GameObject go = pool.Spawn();
        go.transform.position = pos;
        return go;
    }

    public void Despawn(GameObject go)
    {
        if (pools.TryGetValue(go.name, out ObjectPool pool) == false)
        {
            Debug.LogWarning("Pool with name " + go.name + " does not exist");
            return;
        }
		// 실제 Pool 대상을 Despawn 한다.
        pool.Despawn(go);
    }
}

이제 MyCircle에서 스페이스바를 누르면 작성된 PoolManager를 통해서 랜덤으로 사격형 아니면 삼각형 도형을 생성한다.

public class MyCircle : MonoBehaviour
{
    public void Start()
    {
        AudioManager.Instance.PlayBgm();
    }

    public void OnSpace()
    {
        ShootShape();
    }

    public void ShootShape()
    {
        string name = Random.Range(0, 2) > 0 ? "Triangle" : "Square";

        GameObject go = PoolManager.Instance.Spawn(name, transform.position);
        go.GetComponent<Shape>()?.Init();

    }
}

각 도형들은 일정 시간이 지나면 파괴 되도록 하는데, 실제 파괴가 아니라 비활성 상태가 되면서 화면에서 사라지게 된다.

public class Shape : MonoBehaviour
{
    [SerializeField] private float speed = 1f;
    [SerializeField] private float lifeTime = 3f;
    [SerializeField] private float rotateSpeed = 10f;
    private Vector3 direction = Vector3.zero;
    private float time = 0;
    private bool isLeftRotate = false;
    private SpriteRenderer spriteRenderer;

    private void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }
    void Update()
    {
        Timer();
        Move();
    }

    private void Move()
    {
        transform.position += direction * speed * Time.deltaTime;
        Vector3 rot = isLeftRotate ? Vector3.forward : -Vector3.forward;
        transform.Rotate(rot * rotateSpeed * Time.deltaTime, 1f);
    }

    private void Timer()
    {
        time += Time.deltaTime;
        if (time >= lifeTime)
        {
        	// 실제 Pool 대상을 없앤다.
            PoolManager.Instance.Despawn(gameObject);
        }
    }

    public void Init()
    {
        time = 0;
        float randomAngle = Random.Range(0, 360);
        isLeftRotate = Random.Range(0, 2) > 0;
        direction = new Vector3(Mathf.Cos(randomAngle), Mathf.Sin(randomAngle), 0);
        direction = direction.normalized;
        spriteRenderer.color = new Color(Random.Range(0f, 2f), Random.Range(0f, 2f), Random.Range(0f, 2f));
    }
}

이제 결과물을 보면

이런식으로 원을 중심으로 파티클 형태로 나타나서 주변에 방사 되다가 일정 시간 뒤에 파괴되는 것이 아니라 비활성상태로 변경되고, 다시 도형들을 생성할때 비활성 된 오브젝트들을 찾아서 활성상태로 변경하여 처음과 같은 방식으로 주변에 방사되는 것을 볼 수 있다.

profile
새로운 시작

0개의 댓글