[Unity] Object Pooling

이정석·2023년 7월 3일
0

Unity

목록 보기
11/22

Object Pooling

Obejct Pooling은 여러 개의 Object를 미리 생성해 놓고 필요할 때마다 생성한 객체를 Pool에서 꺼내 사용하는 방법이다. Object Pooling을 사용함으로 새로운 Object를 생성하거나 불필요한 삭제를 삭제한느 시간을 줄여 성능을 최적화 할 수 있다.

  • 다음 Scene에 사용할 GameObject여도 생성은 해두고 비활성화 시키는 방식을 사용할 수 있다.
  • 이미 사용한 GameObject를 Destroy하는 것이 아닌 비활성화 시켜 나중에 사용할 때 Create과정을 생략한다.

    Pooling할 Object를 어떻게 구분할까?

1. Pooling Component

Pooling할 GameObject를 구분하기 위해 Pooling Component를 추가할 수 있다. Object Pooling을 구현할 때 Pooling할 GameObject를 Pooling Component를 가지고 있는지 없는지로 Pooling 대상에 포함할 수 있다.

2. 개선점

아래 코드는 기존의 prefab을 하나 생성하는 코드인데 크게 두 부분으로 나눌 수 있다.

  • Load<GameObject>(): 메모리로 가져오는 부분
  • Object.Instantiate(): GameObject를 생성하는 부분
        GameObject prefab = Load<GameObject>($"Prefabs/{path}");
        GameObject go = Object.Instantiate(prefab, parent);
        go.name = prefab.name;
        return go;

결국 Object Pooling의 목표중 하나는 Load(GameObject)Object.Instantiate()를 최소한으로 사용하는 것이다.

  • 이미 한 번 Load된 prefab를 저장하는 방법
  • 이미 생성되고 비활성화 되어 있는 GameObject를 재사용하는 방법

위 단계들을 구현하기 위해 기존의 코드에서 다음과 같은 내용을 추가해야 한다.

  • Load(GameObject)하기 전에 이미 Load된 prefab을 확인해야 한다.
  • Object.Instantiate()하기 전에 이미 Pooling된 GameObject를 확인해야 한다.

확인하는 기능을 담당하는 새로운 클래스를 만들어 위임하자

3. Destroy

이미 생성된 GameObject를 삭제하기 위해 기존의 방법은 다음과 같은 함수를 통해 Destroy를 하였다.

    public void Destroy(GameObject go)
    {
        if (go == null)
            return;
        Object.Destroy(go);
    }

위에서 Pooling Component를 가진 GameObject는 바로 삭제하는 것이 아닌 비활성화 상태로 바꿔줘야 한다.

  • Object.Destroy()하기 전에 Pooling Component를 가진 GameObject인지 확인한다.
  • Pooling Component를 가진 GameObject라면 Destroy가 아닌 비활성화 시킨다.

    이 기능 역시 새로운 클래스에 위임하자


Pool Management

Object Pooling을 담당할 클래스를 PoolManager라는 이름으로 구현하고자 한다. 코드를 구현하기 전에 Scene의 Pool구조를 먼저 정해야 한다.

  • 전체 Pool를 하나의 Empty GameObject(Pool Root) 아래에 둔다.
  • Pool Root는 Pooling하고자 하는 GameObject의 이름으로 Pool을 매핑해야 한다.
  • Pool Root는 가능한 return되는 GameObject나 Component에 대한 신뢰성을 보장해야한다.

    신뢰성을 보장한다는 말은 return null;을 최대한 적게 사용하면 좋다는 뜻!

Object Pooling을 위한 전체 GameObject구조는 다음과 같다.

Pool_Root
	ㄴPool1
    	ㄴGameObject1
    	ㄴGameObject2
    	ㄴGameObject3
	ㄴPool2
    	ㄴGameObject1

1. Pool

Pool_Root의 산하의 Pool을 나타내기 위한 Pool 클래스를 정의할 필요가 있다. 위의 구조에는 Pool1 -> GameObject에 대한 Create, Push, Pop 기능을 구현해야 한다.

  • variable, Init()
        public GameObject Original { get; private set; }
        public Transform Root { get; set; }
        Stack<Poolable> _poolStack = new Stack<Poolable>();

        public void Init(GameObject original, int count = 5)
        {
            Original = original;
            Root = new GameObject().transform;
            Root.name = $"{original.name}_Root";

            for(int i=0; i < count; i++)
            {
                Push(Create());
            }
        }

Pool 클래스에 필요한 변수는 다음과 같다.

  • Original: 생성시 Instantiate를 위한 원본 GameObejct
  • Root: Pool에 존재하는 GameObject를 하나의 그룹으로 관리하기 위한 부모 GameObject
  • poolStack: 생성되었지만 Pool에 존재하는 GameObject를 저장할 자료구조, 꼭 Stack이 아니어도 괜찮다.

Poolable은 Object Pooling을 할 GameObject에 붙는 Component의 이름이다.

  • Create()
        Poolable Create()
        {
            GameObject go = UnityEngine.Object.Instantiate(Original);
            go.name = Original.name;
            return go.GetOrAddComponent<Poolable>();
        }

Original에 해당하는 GameObject를 생성하는 함수이다. Create는 다른 함수에서 Pool에 존재하는 사용가능한 GameObject가 존재하지 않을때 최종적으로 호출되는 함수이다.

객체 생성시 Original을 기반으로 생성한다는 점에서 프로토타입패턴과 비슷하다고 생각한다.

  • Push()
        public void Push(Poolable poolable)
        {
            if (poolable == null)
                return;

            poolable.transform.parent = Root;
            poolable.gameObject.SetActive(false)
            
            //poolable의 상태전환

            _poolStack.Push(poolable);
        }

해당 Pool에 Poolable Component를 Push하는 함수이다.

  • 입력받은 Component를 Pool의 하위 GameObject로 설정한다.
    Component에서 transform에 접근가능하다!

  • 입력받은 Component를 비활성화 상태로 바꾼다.

  • 스택에 넣는다.

  • Pop()

        public Poolable Pop(Transform parent)
        {
            Poolable poolable;

            if (_poolStack.Count > 0)
                poolable = _poolStack.Pop();
            else
                poolable = Create();

            poolable.gameObject.SetActive(true);

            if (parent == null)
                poolable.transform.parent = Managers.Scene.CurrentScene.transform;
            
            poolable.transform.parent = parent;

            return poolable;
        }

위에서 신뢰성에 대한 얘기를 했는데 Pop에서 return null;이 없는 것을 알 수 있다. 즉, 스택이 비었을 때 새로운 GameObject를 생성해 return하는 방식을 사용한다.

Pop함수는 구현하기 나름이다. pool에 있는 Object를 return할 수도 있으며 위 코드와 같이 parent를 입력받아 parent의 설정도 해줄 수 있다.

parent==null일 때, parent를 바꾸는 것은 Pool ManagerDontDestroyOnLoad에 있기 때문에 밖으로 꺼내주기 위함이다.

2. variable, Init()

    Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
    Transform _root;

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

Pool Manager는 생성할 GameObject의 이름으로 Pool을 매핑해야하기 때문에 Dictonary를 이용해 이를 구현한다. 여러개의 Pool은 Pool Root라는 GameObject 밑에 둘 것이며 DontDestroyOnLoad로 Scene이 이동해도 유지되도록 한다.

3. CraetePool()

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

        _pool.Add(original.name, pool);
    }

새로운 Pool을 만드는 함수이며 Pool이 만들 원본 Original과 초기 생성 개수를 입력받는다. 새로운 Pool을 생성 후 구조를 설정, Dictonary에 추가한다.

4. Push()

    public void Push(Poolable poolable)
    {
        string name = poolable.gameObject.name;
        if (_pool.ContainsKey(name) == false)
        {
            GameObject.Destroy(poolable.gameObject);
            return;
        }

        _pool[name].Push(poolable);
    }

입력받은 Poolable Component에 맞는 Pool을 찾아 해당하는 Pool에 Component를 Push한다. name에 해당하는 Pool을 찾지 못한경우에는 바로 삭제한다.

Pool Manager에 Push 요청이 왔다는 것은 전달받은 GameObject가 Scene에서 사용되지 않아 삭제되는 과정에 있다는 것의 의미하기 때문에 Push에서 Destroy를 해도된다.

5. Pop()

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

        return _pool[original.name].Pop(parent);
    }

Original과 일치하는 Pool에 존재하는 GameObject를 반환하는 함수로 존재하지 않을 때는 새로운 Pool을 생성해 적어도 하나의 GameObject를 반환하도록 구현하였다.

6. GetOriginal()

    public GameObject GetOriginal(string name)
    {
        if (_pool.ContainsKey(name) == false)
            return null;
        return _pool[name].Original;
    }

name과 일치하는 Pool을 찾아 해당하는 Pool의 Original을 반환한다. Dictonary의 key에서 name과 일치하는 Pool이 있는지 찾고 결과를 반환하는 방식이다.

Pool 클래스를 구현했기 때문에 Pool Manager의 코드가 간결해졌다. 실제 저장 자료구조는 Pool클래스가 하고 Pool을 Pool Manager는 Pool의 매핑과 결과를 전달하는 역할을 한다.

profile
게임 개발자가 되고 싶은 한 소?년

0개의 댓글