내일배움캠프/TIL

[내배캠][Unity6기][TIL] ObjectPool

binary는 호남선 2024. 11. 27. 22:28

생성과 삭제가 빈번한 오브젝트는 풀링이 반드시 필요하므로 인간을 만들기 전에 ObjectPool 프레임워크부터 설계했다.

오브젝트 풀은 어떻게 사용할지에 따라 구현 방법이 다양해지기 때문에 여기저기에서 많은 레퍼런스를 참고하고 우리 게임에 최적화하는 방식으로 설계하고자 했다.

 

[ 고려했던 사항 ]

1. Unity에서 제공하는 Pool 사용 or 직접 구현?

=> 직접 구현!

 Unity Pool 활용하려면 내부 코드를 잘 알아야 유연하게 활용할 수 있는데 내부 코드 분석이 아직은 어려워서 직접 구현한 ObjectPool을 사용하는 것이 더 낫다고 판단했다. 무엇보다 문제가 발생했을 때 디버깅이 어려울 것 같았다! Unity 제공 Pool은 나중에 더 공부해서 개인 프로젝트에 활용해보거나 테스트 해볼 생각이다.

 

2. 다른 클래스에서 ObjectPool 바로 사용 or PoolManager 통해 관리?

=> PoolManager!

 ObjectPool 을 바로 사용하는 것보다는 PoolManager를 통해 여러 풀을 한 곳에서 관리할 수 있도록 설계했다. 입문 주차와 숙련 주차 2D 강의(TopDownShooting)에서 사용했던 오브젝트풀 코드를 사실 그대로 사용하고 싶었지만... PoolManager를 활용하려면 어쩔 수 없이 변경해야하는 부분들이 있었다.

 

이유?

1) 꾸준 실습 ObjectPool 활용에서 게임 볼륨이 커질수록 PoolManager를 통해 관리하는 것이 좋다고 피드백 받았음

2) 몬스터 풀, 인간 풀, 사운드 풀 등... 여기저기에서 따로 관리하면 유지보수가 어려움

 

3. 풀 확장 가능 or 불가능?

=> 확장 불가, 제한 사이즈 풀

 

인간: 각 스테이지의 웨이브마다 이미 나올 인간의  레벨 디자인을 통해 정해짐.

몬스터: 배치하기 위해 스테이지당 제공되는 재화가 정해짐. 인간을 최대로 처치해서 얻을 수 있는 재화도 정해짐.

=> 플레이어의 외부 입력으로 계속해서 생성하는 경우 X, 게임 밸런스와 최적화를 적절히 고려하여 풀 사이즈를 미리 정해놓고 정해진 풀 내에서만 생성하도록 설계

 

4. PoolManager에 필요한 메서드?

=> 풀 생성, 풀링 객체 캐싱, 풀에 반환, 풀 삭제

 

다양한 상황에서 유연하게 활용할 수 있도록 설계하고자 했다.

 

InitializePools: Inspector에서 설정한 풀 정보를 가져와 풀 초기화

CreatePool: 풀 설정 데이터 기반으로 풀 생성

SpawnFromPool: 풀에서 GameObject를 반환, 위치 설정 여부에 따라 2가지 버전

ReturnPool: 사용 후 풀에 오브젝트 반환

DeletePool: 특정 풀 삭제

DeleteAllPools: 모든 풀 삭제

 

위 사항들을 고려하여 작성된 초기 코드이다. 이후 진행하면서 변경이 필요하다면 수정하며 사용할 예정이다.

ObjectPool.cs

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    public class Pool
    {
        public string Tag;
        public GameObject Prefab;
        public int Size;
    }
    
    private Dictionary<string, Queue<GameObject>> _poolDictionary;  // 각 풀을 관리하는 딕셔너리

    public void Initialize(Pool pool)
    {
        if (_poolDictionary == null)
        {
            _poolDictionary = new Dictionary<string, Queue<GameObject>>();
        }

        // 풀 딕셔너리에 이미 해당 태그와 일치하는 풀 있으면 리턴(중복 풀 생성 방지)
        if (_poolDictionary.ContainsKey(pool.Tag))
        {
            Debug.LogAssertion($"Pool with tag {pool.Tag} already exists.");
            return;
        }
        
        Queue<GameObject> objectPool = new Queue<GameObject>();
        for (int i = 0; i < pool.Size; i++)
        {
            // 게임오브젝트 프리팹에서 생성하고 비활성화
            GameObject obj = Instantiate(pool.Prefab, transform);
            obj.SetActive(false);
            objectPool.Enqueue(obj);    // 큐 구조의 오브젝트풀에 생성된 게임오브젝트 추가
        }
        
        _poolDictionary.Add(pool.Tag, objectPool);  // 새로 만든 풀을 풀 딕셔너리에 추가
    }

    public GameObject SpawnFromPool(string tag)
    {
        // 풀 딕셔너리에 해당 태그와 일치하는 풀이 있는지 확인
        if (!_poolDictionary.ContainsKey(tag))
        {
            Debug.LogAssertion($"Pool with tag {tag} doesn't exist.");
            return null;
        }

        GameObject obj = _poolDictionary[tag].Dequeue();    // 풀에서 가장 오래된 오브젝트 가져오기
        _poolDictionary[tag].Enqueue(obj);  // 다시 풀에 넣기(최신 오브젝트로 갱신)
        // 오브젝트 활성화하여 반환
        obj.SetActive(true);
        return obj;
    }
}

PoolManager.cs

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

public class PoolManager : SingletonBase<PoolManager>
{
    // Inspector에서 여러 풀 한번에 관리
    [System.Serializable]
    public class PoolConfig
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }

    [SerializeField] private List<PoolConfig> poolConfigs = new List<PoolConfig>();
    private Dictionary<string, ObjectPool> _pools = new Dictionary<string, ObjectPool>();

    protected override void Awake()
    {
        base.Awake();
        InitializePools();
    }

    private void InitializePools()
    {
        foreach (var config in poolConfigs)
        {
            CreatePool(config.tag, config.prefab, config.size);
        }
    }

    private void CreatePool(string tag, GameObject prefab, int size)
    {
        // 풀 딕셔너리에 이미 해당 태그와 일치하는 풀 있으면 리턴(중복 풀 생성 방지)
        if (_pools.ContainsKey(tag))
        {
            Debug.LogAssertion($"Pool with tag {tag} already exists.");
            return;
        }

        /* 계층 구조 생성하여 정리 */
        // PoolManager
        // - Pool_XXX
        // -- GameObject(Clone)
        // -- GameObject(Clone)...
        GameObject poolObject = new GameObject($"Pool_{tag}");  // 풀 관리할 빈 게임오브젝트 생성하고 태그로 이름 구별
        poolObject.transform.SetParent(transform);  // PoolManager의 자식으로 설정

        // Inspector에서 받아온 설정 정보 기반으로 새로운 오브젝트 풀 생성
        ObjectPool objectPool = poolObject.AddComponent<ObjectPool>();
        objectPool.Initialize(new ObjectPool.Pool
        {
            Tag = tag,
            Prefab = prefab,
            Size = size
        });

        _pools.Add(tag, objectPool);    // 풀 딕셔너리에 새로운 오브젝트 풀 추가
    }

    // Transform 설정하는 SpawnFromPool
    public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation)
    {
        // 태그와 일치하는 풀이 있는지 유효성 검사
        if (!_pools.ContainsKey(tag))
        {
            Debug.LogAssertion($"Pool with tag {tag} doesn't exist.");
            return null;
        }

        GameObject obj = _pools[tag].SpawnFromPool(tag);    // 풀에서 오브젝트 가져오기
        // Object 있으면 Transform 설정하고 반환
        if (obj != null)
        {
            obj.transform.position = position;
            obj.transform.rotation = rotation;
        }
        return obj;
    }
    
    // 게임오브젝트만 반환하는 SpawnFromPool
    public GameObject SpawnFromPool(string tag)
    {
        // 태그와 일치하는 풀이 있는지 유효성 검사
        if (!_pools.ContainsKey(tag))
        {
            Debug.LogAssertion($"Pool with tag {tag} doesn't exist.");
            return null;
        }

        // 풀에서 오브젝트 가져와 반환
        GameObject obj = _pools[tag].SpawnFromPool(tag);
        return obj;
    }

    public void ReturnToPool(string tag, GameObject obj)
    {
        // 태그와 일치하는 풀이 있는지 유효성 검사
        if (!_pools.ContainsKey(tag))
        {
            Debug.LogAssertion($"Pool with tag {tag} doesn't exist.");
            return;
        }
        obj.SetActive(false);   // 오브젝트 비활성화
    }
    
    // 특정 태그의 오브젝트 풀을 삭제
    public void DeletePool(string tag)
    {
        // 태그와 일치하는 풀이 있는지 유효성 검사
        if (!_pools.ContainsKey(tag))
        {
            Debug.LogAssertion($"Pool with tag {tag} doesn't exist.");
            return;
        }

        Destroy(_pools[tag].gameObject);    // 오브젝트 풀 삭제
        _pools.Remove(tag); // 풀 목록에서 태그 제거

        // 인자로 들어온 tag가 PoolConfig의 tag와 일치하면 해당 PoolConfig 삭제
        poolConfigs.RemoveAll(config => config.tag == tag);

        Debug.LogAssertion($"Pool with tag {tag} deleted successfully.");
        
        EditorUtility.SetDirty(this);   // Asset 상태 갱신 에디터에 전달
    }

    // 풀 딕셔너리에 등록된 모든 오브젝트 풀 삭제
    public void DeleteAllPools()
    {
        if (_pools == null) return;
        
        // 풀 딕셔너리에 있는 오브젝트 풀로 생성된 게임오브젝트 삭제
        foreach (var pool in _pools.Values)
        {
            Destroy(pool.gameObject);
        }

        _pools.Clear();         // 풀 딕셔너리에서 모든 항목 삭제
        poolConfigs.Clear();    // 풀 설정 리스트에서 모든 항목 삭제

        Debug.LogAssertion("All pools have been deleted.");

        EditorUtility.SetDirty(this);   // Asset 상태 갱신 에디터에 전달
    }
}