본문 바로가기

Program Tip

Unity에서 유용하게 사용될 오브젝트 풀링(Object Pooling)

반응형

Unity3D

오브젝트 풀링은 유니티에서 아주 자주 사용하는 로직이다.

매번 오브젝트를 생성/파괴를 하는 것이 아닌 한번 생성했던 오브젝트를 리스트(List) 또는 큐(Queue)에 저장해 두었다가 재사용하고 사용이 완료된 후엔 반납하는 역할을 하는 것이 오브젝트 풀링이다.

여러 차례 게임을 개발하며 오브젝트 풀링 관리하는 방법을 개선하곤 했었는데, 이번에 조금 더 많은 개선이 이루어지며 편리해졌다는 판단을 하게 되었다.

기존에는 오브젝트 풀링을 사용하기 위해서는 여러가지 절차들이 많이 필요했었다.

씬의 빈 공간에 빈 GameObject를 만들어서 거기에 풀링 매니저 스크립트를 넣고, 컨테이너를 만들어서 적용하는 등등 여러 절차들이 필요했었다

이러한 여러 절차들을 자동으로 처리할 수있도록 개선하였으며, 소스 상에서 GameObjectPoolManager.Instance 만 호출하면 모든 것이 자동으로 생성될 수 있도록 처리하였다.

GameObjectPoolManager.Instance 후 GetGameObject(string path, Transform parent) 의 함수를 이용하여 GameObject를 생성할 수 있으며, Prefab의 이름이 유니크 해야만 PoolManager를 고유하게 사용할 수 있다.

아래 소스를 첨부한다.

[GameObjectPoolManager.cs]

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


/// <summary>
/// Code by ForestJ (https://forestj.tistory.com)
/// 미리 GameObject를 생성해둘 필요는 없으며,
/// 외부에서 Instance를 호출하면 Instance 생성 여부에 따라 자동으로 GameObjectPoolMananger를 Sigleton으로 생성해 준다.
/// 필요한 경우 각 씬마다 1개씩 생성하여 사용이 가능
/// DontDestory 모드가 아니기 때문에 씬 변경시 모두 삭제됨
/// </summary>
public class GameObjectPoolManager : MonoBehaviour
{
    private static GameObjectPoolManager _instance = null;
    public static GameObjectPoolManager Instance
    {
        get
        {
            if (_instance == null)
            {
                var obj = FindObjectOfType<GameObjectPoolManager>();
                if (obj != null)
                {
                    _instance = obj;
                }
                else
                {
                    var new_obj = new GameObject();
                    _instance = new_obj.AddComponent<GameObjectPoolManager>();
                    new_obj.name = _instance.GetType().Name;
                    _instance.CreateRecycledObjectContainer();
                    _instance.Is_Alive = true;
                }

            }
            return _instance;
        }
    }

    public bool Is_Alive { get; private set; } = false;

    /// <summary>
    /// 회수된 오브젝트를 보관 할 컨테이너
    /// </summary>
    Transform Recycled_Object_Container;


    /// <summary>
    /// Object Pool 리스트
    /// </summary>
    Dictionary<string, PoolManager> Pool_List = new Dictionary<string, PoolManager>();


    /// <summary>
    /// 회수 후 보관할 컨테이너 생성
    /// </summary>
    void CreateRecycledObjectContainer()
    {
        if (Recycled_Object_Container == null)
        {
            var container = new GameObject();
            container.transform.SetParent(this.transform);
            container.SetActive(false);
            container.name = nameof(Recycled_Object_Container);
            Recycled_Object_Container = container.GetComponent<Transform>();
        }
    }
    /// <summary>
    /// path의 prefab을 GameObject로 생성 반환.
    /// path를 이용하여 name을 찾고, name 별로 PoolManager를 보관하고 있으며,
    /// 해당 PoolManager에 GameObject가 존재할 경우 찾아서 반환
    /// PoolManager가 없거나 GameObject가 없을 경우 새로 생성하여 반환한다
    /// PoolManager를 새로 생성할 때는 name을 Key로 사용하여 Map에 저장하여 재사용 할 수 있도록 한다.
    /// </summary>
    /// <param name="path">Resources 아래의 Prefab Path</param>
    /// <param name="parent">GameObject 의 상위 Transform</param>
    /// <returns></returns>
    public GameObject GetGameObject(string path, Transform parent)
    {
        if (!Is_Alive)
        {
            return null;
        }
        string name = System.IO.Path.GetFileNameWithoutExtension(path);
        if (string.IsNullOrEmpty(name))
        {
            return null;
        }
        PoolManager p = null;
        if (Pool_List.ContainsKey(name))
        {
            p = Pool_List[name];
        }
        else
        {
            p = new PoolManager(Recycled_Object_Container);
            Pool_List.Add(name, p);
        }

        if (p != null)
        {
            return p.Pop(path, parent);
        }
        return null;
    }
    /// <summary>
    /// path의 prefab을 GameObject로 생성하여 재사용 컨테이너에 저장해둔다.
    /// 게임내에서 사용하기 전에 미리 생성해두는 것으로, 실시간 생성시 노드가 발생할 수 있으니,
    /// Loading 화면에서 미리 로드할 수있는 GameObject를 알고 있다면 미리 로드하는 것도 좋다.
    /// </summary>
    /// <param name="path"></param>
    public void PreloadGameObject(string path)
    {
        if (!Is_Alive)
        {
            return;
        }
        string name = System.IO.Path.GetFileNameWithoutExtension(path);
        if (string.IsNullOrEmpty(name))
        {
            return;
        }
        PoolManager p = null;
        if (Pool_List.ContainsKey(name))
        {
            p = Pool_List[name];
        }
        else
        {
            p = new PoolManager(Recycled_Object_Container);
            Pool_List.Add(name, p);
        }
        if (p != null)
        {
            //  todo
            p.PreLoad(path);
        }
    }

    /// <summary>
    /// GameObject 회수
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="is_out_move">화면밖으로 강제 이동 시킬 것인지 여부. 기본값은 true</param>
    public void UnusedGameObject(GameObject obj, bool is_out_move = true)
    {
        if (!Is_Alive)
        {
            return;
        }
        PoolManager p = null;
        string name = obj.name;
        if (string.IsNullOrEmpty(name))
        {
            return;
        }
        if (Pool_List.ContainsKey(name))
        {
            p = Pool_List[name];
        }
        else
        {
            p = new PoolManager(Recycled_Object_Container);
            Pool_List.Add(name, p);
        }
        if (p != null)
        {
            p.Push(obj, is_out_move);
        }
        else
        {
            Debug.Assert(false);
        }
        
    }

    [ContextMenu("ToStringGameObjectPoolManager")]
    public void ToStringGameObjectPoolManager()
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        sb.AppendFormat("Pool_List Count [{0}]", Pool_List.Count).AppendLine();
        sb.AppendLine("==========");
        foreach (var item in Pool_List)
        {
            var p = item.Value;
            sb.AppendFormat("[{0}] => {1}", item.Key, p.ToString()).AppendLine();
            sb.AppendLine("==========");
        }

        Debug.Log(sb.ToString());
    }

    public void OnDestroy()
    {
        foreach (var item in Pool_List)
        {
            var p = item.Value;
            p.Dispose();
        }
        Pool_List.Clear();
        Is_Alive = false;
        Resources.UnloadUnusedAssets(); //  Resources로 호출되었던 사용하지 않는 에셋만 해제
    }

    

}
반응형

[PoolManager.cs]

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

/// <summary>
/// Code by ForestJ (https://forestj.tistory.com)
/// PoolManager는 프래팹의 이름으로 구분한다.
/// 각 프리팹의 이름을 유니크하게 설정하여 사용하면 중복되지 않는다.
/// </summary>
public class PoolManager : IDisposable
{
    Transform Recycled_Object_Container;

    /// <summary>
    /// 매번 같은 프리팹 리소스를 불러 오는 것이 아니라 한번 가져왔던 prefab 리소스는 저장해 두었다가,
    /// 재사용 요청시 해당 prefab 리소스를 재사용 한다.
    /// </summary>
    GameObject Object_Prefab;

    /// <summary>
    /// 재사용할 게임 오브젝트를 담아두는 곳
    /// </summary>
    Stack<GameObject> Recycled_Objects = new Stack<GameObject>();

    /// <summary>
    /// 비활성시 필요에 따라 해당 GameObject의 위치를 화면 밖으로 이동시킨다.
    /// </summary>
    Vector2 Remove_Position = new Vector2(-100000, -100000);

    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                //  관리되는 자원 해제
                Clear();
            }
            disposed = true;
        }
    }

    public PoolManager(Transform recycled_container)
    {
        this.Recycled_Object_Container = recycled_container;
    }
    /// <summary>
    /// GameObject 를 찾아서 반환.
    /// 해당 GameObject가 없을 경우 생성하여 반환한다.
    /// Object_Prefab 도 재사용 가능하도록 추가한다.
    /// </summary>
    /// <param name="path"></param>
    /// <param name="parent"></param>
    /// <returns></returns>
    public GameObject Pop(string path, Transform parent)
    {
        string name = System.IO.Path.GetFileNameWithoutExtension(path);
        GameObject item = null;
        if (Recycled_Objects.Count > 0)
        {
            item = Recycled_Objects.Pop();
            item.gameObject.SetActive(true);
            item.transform.SetParent(parent);
        }

        if (item == null)
        {
            if (Object_Prefab == null)
            {
                Object_Prefab = Resources.Load<GameObject>(path);
            }
            
            item = GameObject.Instantiate<GameObject>(Object_Prefab, parent);
            if (!string.IsNullOrEmpty(name))
            {
                item.name = name;
            }
        }

        var component = item.GetComponent<IPoolableComponent>();
        if (component != null)
        {
            component.Spawned();
        }

        return item;
    }

    public void PreLoad(string path)
    {
        string name = System.IO.Path.GetFileNameWithoutExtension(path);
        GameObject item = null;
        //  prefab info 가 null 이면 새로 생성. 재사용 위해
        if (Object_Prefab == null)
        {
            Object_Prefab = Resources.Load<GameObject>(path);
        }
        //  신규 생성
        item = GameObject.Instantiate<GameObject>(Object_Prefab, Recycled_Object_Container);
        //  GameObject 이름 변경
        if (!string.IsNullOrEmpty(name))
        {
            item.name = name;
        }
        //  재사용 리스트에 등록
        Recycled_Objects.Push(item);
    }

    /// <summary>
    /// 사용하지 않는 GameObject를 반환한다.
    /// </summary>
    /// <param name="obj">반환할 GameObject</param>
    /// <param name="is_out_move">반환시 화면 밖으로 이동 시킬지 여부</param>
    public void Push(GameObject obj, bool is_out_move)
    {
        if (object.ReferenceEquals(obj.transform.parent, Recycled_Object_Container))
        {
            //  already in pool
            return;
        }
        var component = obj.GetComponent<IPoolableComponent>();
        if (component != null)
        {
            component.Despawned();
        }
        obj.transform.SetParent(Recycled_Object_Container);
        if (is_out_move)
        {
            obj.transform.position = Remove_Position;
        }
        obj.gameObject.SetActive(false);
        Recycled_Objects.Push(obj);
    }
    

    /// <summary>
    /// 모든 GameObject 삭제
    /// Prefab Object도 null 처리해준다.
    /// </summary>
    void Clear()
    {
        //  모두 클리어
        //while (Recycled_Objects.Count > 0)
        //{
        //    var obj = Recycled_Objects.Pop();
        //    GameObject.DestroyImmediate(obj);
        //}
        Recycled_Objects.Clear();

        Object_Prefab = null;
    }

    public override string ToString()
    {
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        sb.AppendFormat("PoolManager : [{0}]", Recycled_Objects.Count).AppendLine();

        foreach (var item in Recycled_Objects)
        {
            sb.AppendFormat("[{0}]", item.name).AppendLine();
        }

        return sb.ToString();
    }
}

 

[IPoolableComponent.cs]

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

/// <summary>
/// Prefab으로 사용할 GameObject의 MonoBehavior와 같이 상속 받아서 구현해주면 된다.
/// 해당 Interface를 사용할 경우 생성/삭제시 해당 Spawned() / Despawned()를 자동 호출해주어
/// 초기화 및 삭제시 해야하는 작업을 추가해 줄 수 있다.
/// </summary>
public interface IPoolableComponent
{
    void Spawned();
    void Despawned();
}

 

사용 방법 (GameObject 생성 요청)

var pool = GameObjectPoolManager.Instance;
var obj = pool.GetGameObject("Prefabs/Etc/HPInfoBar", HP_Bar_Container);
var hp = obj.GetComponent<HPInfoBar>();
hp.SetTargetTransform(t, bar_top);

 

사용 방법 (GameObject 반환 요청)

var pool = GameObjectPoolManager.Instance;
pool.UnusedGameObject(this.gameObject);

 

GameObjectPoolManager.Instance 를 호출하기 전

 

GameObjectPoolManager.Instance를 호출한 후

 

아래 그림처럼 Instance를 호출하면 Hierarchy에 자동 생성되어 사용이 가능하다.

GameObjectPoolManager 선택 후 재사용을 위해 저장되어 있는 리스트를 확인할 수 있다.

 

독립적 사용성을 높이기 위해 위와 같이 만들었다.

해당 소스를 보고 문의 사항이 있으면 답글 달아주세요.

 

반응형