본문 바로가기

Design Pattern/유니티에 적용해보기

C#(Unity) - 다중 오브젝트 풀링 (Multiple Object Pooling)

1. 개요

게임에서 적이나 총알 오브젝트를 관리를 한다고 할 때, 적군에게는 잡병1,2,3... 중간 클래스1,2,3... 등등.. 총알에는 라이플탄, 스나이퍼탄, 샷건탄 등등.. 한 가지 종류가 아닌 경우가 많습니다. 이것들을 위해 각각의 풀링매니저를 생성하기에는 비효율적이니, 열거형인 Enum을 통해 타입을 구분하고 이 타입을 통해 다중 오브젝트 풀링을 구현하겠습니다. 


2. 스크립트 구현

3가지 종류가 있는 Enemy를 풀링으로 구현해보는 것이 목표입니다.

 

※ EnemyType을 열거형으로 선언.

public enum EnemyType
{
    Square,
    Sphere,
    Capsule,
}

 

※ 풀링할 Enemy의 오브젝트의 정보

[System.Serializable]
public class EnemyPoolingInfo
{
    public EnemyType type;			// 적 타입
    public GameObject prefab;			// 생성할 적 프리팹
    public Transform container;			// 부모로 설정할 Transform
    public int createCount;			// 생성 개수
    
    // 관리를 위한 queue
    public Queue<GameObject> queue = new Queue<GameObject>();	
}

아래의 EnemyManager에서 이 EnemyPoolingInfo를 List로 가지고 있기 때문에, 컴포넌트로 붙이게 되면 Inspector 창에서 이런 형식을 가지게 됩니다.

 

PoolList의 사이즈 만큼 풀링 정보를 담고 있습니다.


※ Enemy의 풀링을 담당하는 매니저

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

public class EnemyManager : MonoBehaviour
{
    // 다중 오브젝트 풀링을 위한 List
    public List<EnemyPoolingInfo> poolList = new List<EnemyPoolingInfo>();
    
    // 초기 생성
    private void Start()
    {
        for (int i = 0; i < poolList.Count; i++)
        {
            FillStack(poolList[i]);
        }
    }

    // 저장소에 지정한 프리팹으로 채워넣는다.
    public void FillStack(EnemyPoolingInfo info)
    {
        for (int i = 0; i < info.createCount; i++)
        {
            GameObject pulledObject = Instantiate(info.prefab, info.container);
            pulledObject.SetActive(false);

            info.queue.Enqueue(pulledObject);
        }
    }

    // 타입을 받아와 Peek()과 Dequeue로 빼준다.
    public GameObject Pop(EnemyType type)
    {
        for (int i = 0; i < poolList.Count; i++)
        {
            if (type.Equals(poolList[i].type))
            {
                // 개수가 0 이하면 다시 채워준다.
                if (poolList[i].queue.Count <= 0)
                {
                    FillStack(poolList[i]);
                }

                // 오브젝트를 켜주고 넘겨준다.
                poolList[i].queue.Peek().SetActive(true);

                return poolList[i].queue.Dequeue();
            }
        }

        return null;
    }

    // 반환 받는 함수.
    public void ReturnToStack(EnemyType type, GameObject enemy)
    {
        // 리스트를 검색하여
        for (int i = 0; i < poolList.Count; i++)
        {
            // 넘어온 오브젝트의 타입과 같으면
            if (type.Equals(poolList[i].type))
            {
                // 그 타입을 관리하는 queue에 추가하고, 오브젝트를 꺼준다.
                poolList[i].queue.Enqueue(enemy);
                enemy.SetActive(false);
            }
        }
    }
}

 

※ Enemy

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

public class Enemy : MonoBehaviour
{
	//Scene에서 설정할 타입.
    [SerializeField] EnemyType enemyType;

    // 켜질 때(Pop()) 부터 반환 코루틴 시작.
    private void OnEnable()
    {
        StartCoroutine(ReturnToStack());
    }

    // 2초 후 반환
    IEnumerator ReturnToStack()
    {
        var returnTime = new WaitForSeconds(2.0f);

        yield return returnTime;

        EnemyManager.Instance.ReturnToStack(enemyType, gameObject);
    }
}

3. Scene 적용

※ Enemy

ㆍ생성해둔 프리팹에 Enemy 스크립트를 컴포넌트로 추가하여 Type들을 설정.

※ EnemyManager


4. 플레이 확인

1을 누르면 정육면체, 2를 누르면 구, 3을 누르면 캡슐 Enemy 생성

private void Update()
{
    if(Input.GetKeyDown(KeyCode.Alpha1))
    {
        Pop(EnemyType.Square);
    }
    else if(Input.GetKeyDown(KeyCode.Alpha2))
    {
        Pop(EnemyType.Sphere);
    }
    else if (Input.GetKeyDown(KeyCode.Alpha3))
    {
        Pop(EnemyType.Capsule);
    }
}

Pop()으로 꺼내오는 것, 2초 후 저장소로 ReturnToStack()이 작동되는 것, Pop()해줄 오브젝트가 없다면 다시 채워주는 것, 모두 작동이 잘 되는 것을 확인할 수 있으니 마무리 하겠습니다.