본문 바로가기
스파르타 내배캠

스파르타 내배캠 Unity 3기 37일차

by LemongO 2024. 2. 14.

끝이 보인다~~(아님)

 

 


PoolManager - 2

 

어제는 PoolManager의 내부 클래스 Pool 에 대해 작성했다.

그렇다면 PoolManager 자체에선 어떤 역할을 하는지 오늘 작성해보자.

 

일단 내부클래스에 Pool 이 있다는것은... PoolManager 에서 각 오브젝트에 대해 모든 Pool 을 관리한다는 뜻이고

이 Pool 들을 관리하기 위해선 어떠한 자료구조가 필요한데 Dictionary를 쓰도록한다.

 

private Dictionary<string, Pool> _poolDict = new Dictionary<string, Pool>();

 

Key 로 string을 사용한다.

그럼 string은 어떤것이 들어가는 것일까?

 

바로 프리팹 원본의 이름이다.

 

public void CreatePool(GameObject origin, int count = 5)
{
    Pool pool = new Pool();
    pool.Init(origin, count);
    pool.Root.parent = _root;

    _poolDict.Add(origin.name, pool);
}

 

최초에 Pool 이 생성이 될 때, 원본이 되는 프리팹 origin 과 Pool 에 미리 담아놓을 5개의 count를 받아 Pool 생성을 한다.

 

Pool 을 생성하고 Init 함수로 Pool 을 만들어 준다.

Pool 의 Root 의 parent 는

Pool Root에 하위에 대기중인 미리 생성된 프리팹 들이 있다.

그리고 그 Pool Root 또한 어떤 부모의 하위에 있다.

그 parent 를 의미하고 이는 _root 이다.

 

이부분이다.

 

마지막으로 Dictionary 에 Pool을 저장한다. Key 는 origin.name이다.

 

그렇다면 _root 는 어디서 나온걸까?

 

private Transform _root;

public void Init()
{
    if(_root == null)
    {
        _root = new GameObject("@Pool_Root").transform;
        Object.DontDestroyOnLoad(_root);
    }
}

 

바로 매니저가 처음 만들어지고 실행되는 Init 함수에서 생성을 한다.

 

Pooling 자체는 어느 씬에서나 적용이 될 수 있기 때문에 _root 가 비어있으면 새로운 Root 오브젝트를 만들어주고

DontDestoryOnLoad를 통해 씬을 넘어가도 파괴 되지 않도록 한다.

 

 

다음은 실제로 Scene 에서 쓰였던 오브젝트를 Pool 에 반환을 할 때, 어제 작성한 내부클래스 Pool 에 접근하려면

반드시 PoolManager의 어떤 함수를 거쳐서 반환을 하게 된다.

 

이는 Push 함수를 이용한다.

 

public void Push(Poolable poolable)
{        
    if (_poolDict.ContainsKey(poolable.name) == false)
    {
        Object.Destroy(poolable.gameObject);
        return;
    }

    _poolDict[poolable.name].Push(poolable);
}

 

먼저 Push를 할 Poolable을 넘겨준다.

Pooling 대상은 모두 Poolable 스크립트를 컴포넌트로 들고 있으니 직관적으로 받아올 수 있다.

 

그 다음 Dictionary 에 poolable.name 의 키, 즉 보관할 오브젝트의 이름을 확인해 해당하는 키가 Dictionary에 있는지

체크한다. 

 

참고로 이번에 쓰인 오브젝트들은 Instantiate 로 생성되었지만 이름을 수정했기 때문에 (clone) 이 없는 상태로,

원본 프리팹과 이름이 동일하다.

 

만약 Poolable 이 있지만 해당 오브젝트 이름의 키가 없다면 바로 삭제 시키도록 한다.

하지만 이런 경우는 거의 없다고 보면 된다.

 

해당하는 키가 있다면 Pool 에 Push를 해줘서 보관하도록 한다.

 

 

다음은 PoolManager를 이용해 오브젝트를 꺼내쓰는 작업이다.

Pop 함수를 이용한다.

 

public Poolable Pop(GameObject origin, Transform parent = null)
{
    if(_poolDict.ContainsKey(origin.name) == false)
        CreatePool(origin);

    return _poolDict[origin.name].Pop();
}

 

Poolable 을 반환하는 Pop 함수는 실제로 꺼내 쓸 오브젝트의 원본과, 부모가 될 Transform을 받는다.

딱히 부모를 지정할 필요가 없을 수도 있으니 null 을 기본값으로 둔다.

 

만약 Pop을 했는데 씬에 있지도 않은 오브젝트를 꺼내 쓰려고 하면 해당 오브젝트에 대한 새로운 Pool 을 생성한다.

 

만약 Pool을 새로 생성했다면 origin 의 name 키로 Dictionary 에 추가 되었으니

바로 Dictionary 의 Pool에서 꺼내 쓰면 된다.

 

 

다음은 Resources.Load 남용을 피하기 위한 함수 GetOrigin 이다.

 

public GameObject GetOrigin(string name)
{
    if (_poolDict.ContainsKey(name) == false)
        return null;

    return _poolDict[name].Origin;
}

 

Pop을 실행 시키기 전 실제 원본 프리팹을 가져올 때,

이미 Pool 이 존재한다면 Resources.Load 를 이용해 매번 프리팹을 새로 불러올 필요 없이

Dictionary 에 있는 Pool 에서 원본을 가져오면 된다.

 

 

이 코드는 ResourceManager 와 연계되는데

 

public GameObject Instantiate(string path, Transform parent = null)
{
    GameObject origin = Load<GameObject>($"Prefabs/{path}");

    if (origin == null)
    {
        Debug.Log($"오브젝트 불러오기에 실패했습니다. : {path}");
        return null;
    }

    if(origin.GetComponent<Poolable>() != null)
        return Managers.Pool.Pop(origin, parent).gameObject;

    GameObject go = Object.Instantiate(origin, parent);
    go.name = origin.name;

    return go;
}

 

ResourceManager (이하 RM) 에서 Instantiate 를 통해 GameObject를 생성할 때,

원본 GameObject 를 Load 라는 함수를 통해(Resources.Load 가 아니다.)  받아와

 

이후 절차를 진행하게 되는데

 

여기서 Load 함수를 통해 Pooling이 되어있는 오브젝트인지 확인 후 불필요한 Load를 줄여준다.

public T Load<T>(string path) where T : UnityEngine.Object
{
    if (typeof(T) == typeof(GameObject))
    {
        string name = path;
        int index = name.LastIndexOf('/');
        if (index >= 0)
            name = name.Substring(index + 1);

        GameObject go = Managers.Pool.GetOrigin(name);
        if (go != null)
            return go as T;
    }

    return Resources.Load<T>(path);
}

 

GameObject go 가 PoolManager 의 GetOrigin 함수를 통해 불러올 수 있는 오브젝트이면 바로 PoolManager 에서 꺼낸다.

 

즉 불필요한 소요를 줄이는 함수 GetOrigin 이다.

 

 

마지막으로 Pool_Root 의 모든 자식들을 지우는 Clear 함수이다.

 

public void Clear()
{
    foreach (Transform child in _root)
        Object.Destroy(child.gameObject);

    _poolDict.Clear();
}

 

Clear 를 해주는 이유는

씬을 전환했을 때 DontDestoryOnLoad 에 의해 Pool_Root의 자식이 남아있으면

A 씬에서 쓰던 오브젝트가 B 씬에서는 안 쓰일 수도 있지만 그대로 남아있게 되므로

씬이 넘어갈 때 모든 하위 Root 들을 지워준다.

그 후 Dictionary도 Clear 로 말끔히 지워준다.