본문 바로가기
최적화

오브젝트 풀 (Object Pool)

by JunHDev 2025. 8. 13.

오브젝트 풀


최적화 기법을 이야기할 때, 가장 먼저 떠오르는 것 중 하나가 바로 **오브젝트 풀(Object Pool)**이다.

유니티에서도 기본 제공하는 오브젝트 풀이 있지만,
내가 원하는 기능들을 자유롭게 넣을 수 있도록 직접 제작해보고 싶었다.

마침 프로토타입을 진행하던 중, 시간이 조금 여유로워서
그 기회에 오브젝트 풀을 만들어 보기로 했다.

 

 

IPoolable

풀에서 관리할 오브젝트들에 연결할 인터페이스.

즉 오브젝트풀에 사용할 객체에 해당 인터페이스를 필수로 상속해야 사용이 가능하다.

 

using UnityEngine;

namespace dev.jun
{
    public interface IPoolable
    {
        //오브젝트풀 오브젝트가 준비된 상태인지 확인!
        bool isGetAble { get; }
        // 풀에서 대여될 때 호출한다.
        void OnSpawn();

        // 풀로 반납될 때 호출한다.
        void OnDespawn();

        // 내부 상태를 초기화할 때 호출한다.
        void ResetState();
    }

}

 

하단은 상속 예시

public class DamageText : MonoBehaviour,dev.jun.IPoolable

 

 

오브젝트 풀

 

// PoolManager.cs
using DevJun.DebugHelper;
using System. Collections;
using System. Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PoolManager : Singleton_generic<PoolManager>
{
    [Header("몬스터 풀")]
    [SerializeField] private AiMonster monsterPrefab;
    [SerializeField] private int monsterInitial = 16;
    [SerializeField] private int monsterMax = 50;
    [SerializeField] private bool monsterExpandable = true;

    private MonsterPool _monsterPool;
    private Transform _monsterRoot;

    [Header("데미지 텍스트 풀")]
    [SerializeField] private Transform DamageTextObj;
    [SerializeField] private Transform DamageTextRoot;

    [SerializeField] private int _textInitialPerId = 4;
    [SerializeField] private int _textMaxPerId = 64;
    [SerializeField] private bool _textExpandable = true;

    [Header("데미지 텍스트 프리팹")]
    [SerializeField] DamageText _txtPrefab;

    private DamageTextPool _txtPool;

    /// ============================================================
    /// 공격 오브젝트도 개별적인 풀을 가지고 있으나 해시테이블을 통해 관리를 진행한다.
    /// ============================================================

    //DB에서 데이터를 가지고온후 공격 오브젝트 번호 세팅 기본공격은 -1부터 시작 스킬은 1번부터 시작 
    [Header("공격오브젝트 풀")]
    [SerializeField] private Transform skillRoot;
    [SerializeField] private int skillInitialPerId = 3;
    [SerializeField] private int skillMaxPerId = 5;
    [SerializeField] private bool skillExpandable = true;

    [Header("공격용 프리팹)")]
    [SerializeField] private List<AtkEffectBase> skillPrefabs = new List<AtkEffectBase>();
    private Dictionary<int, AtkObjPool> _skillCorePool;






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

        if (_monsterRoot == null)
        {
            _monsterRoot = new GameObject("[POOL] Monsters").transform;
            _monsterRoot.SetParent(transform, false);
        }

        if (skillRoot == null)
        {
            skillRoot = new GameObject("[POOL] Skills").transform;
            skillRoot.SetParent(transform, false);
        }

        if (DamageTextObj == null)
        {
            DamageTextObj = new GameObject("[POOL] txt").transform;
            DamageTextObj.SetParent(transform, false);
        }

        var pool = _monsterRoot.GetComponent<MonsterPool>();
        if (pool == null)
        {
            pool = _monsterRoot.gameObject.AddComponent<MonsterPool>();
            LogC.LogColor("에드 컴포넌트 완료", Color.red);
        }

        _monsterPool = pool;
        // 컴포넌트 초기화(필드 세팅 or Init 호출)
        _monsterPool.Init(monsterPrefab, monsterInitial, monsterMax, monsterExpandable, _monsterRoot);



        var txt_pool = DamageTextObj.GetComponent<DamageTextPool>();
        if (txt_pool == null)
        {
            txt_pool = DamageTextObj.gameObject.AddComponent<DamageTextPool>();
            LogC.LogColor("데미지 텍스트 에드 컴포넌트 완료", Color.red);
        }

        _txtPool = txt_pool;
        // 컴포넌트 초기화(필드 세팅 or Init 호출)
        _txtPool.Init(_txtPrefab, _textInitialPerId, _textMaxPerId, _textExpandable, DamageTextRoot);

        //리스트에 프리팹 세팅후 옵젝에 스킬 등록하기
        _skillCorePool = new Dictionary<int, AtkObjPool>();
        RegisterSkills();


    }

    private void RegisterSkills()
    {
        if (skillPrefabs == null)
        {
            return;
        }

        for (int i = 0; i < skillPrefabs.Count; i++)
        {
            skillPrefabs[i].SkillNumbering(i + 1);
            AddPoolAtkObj(skillPrefabs[i]);
        }
    }

    private void AddPoolAtkObj(AtkEffectBase atkEffectBase)
    {

        if (atkEffectBase == null)
        {
            return;
        }

        IAtkObjectBase baseSkill = atkEffectBase as IAtkObjectBase;

        if (baseSkill == null)
        {
            Debug.LogWarning($"[PoolManager] {atkEffectBase.name}는 ISkillObjectBase를 구현하지 않습니다.");
            return;
        }

        int key = baseSkill.SkillId;
        if (!_skillCorePool.ContainsKey(key))
        {
            //포함되어 있지 않다면 새로운 오브젝트풀 할당한다.
            AtkObjPool atkObjPool = new AtkObjPool();
            atkObjPool.Init(atkEffectBase, skillInitialPerId, skillMaxPerId, skillExpandable, transform);
            LogC.LogColor("스킬의 키 : " + key+"스킬 오브젝트 이름:"+baseSkill, Color.red);
            _skillCorePool.Add(key, atkObjPool);
        }
        else
        {
#if UNITY_EDITOR
            LogC.LogColor("이미 할당된 공격 옵젝풀 key : " + key, Color.yellow);
#endif
        }

    }

    public AtkEffectBase GetAttackEffect(int skillId)
    {
        AtkObjPool atkObjPool;
        AtkEffectBase atkobj;
        _skillCorePool.TryGetValue(skillId,out atkObjPool);
        atkobj = atkObjPool.Rent();
        if(atkobj == null)
        {
            LogC.LogColor(skillId+"번호 풀에 없음",Color.yellow);
            //추후 데이터가 없으면 해당 옵젝풀에 객체 추가하는 로직 만들기.
            return null;
        }
        else
        {
            //오브젝트 풀 등록이 되어있으면

            return atkobj;
        }
    }



    // ===== Monster API =====

    public AiMonster RentMonster()
    {
        return _monsterPool.Rent();
    }



    public void ReturnMonster(AiMonster m)
    {
        _monsterPool.Return(m);
    }

    public void ReturnDamageText(DamageText t)
    {
        _txtPool.Return(t);
    }

}

 

 

예전에는 직접 만든 오브젝트 풀에서, 스폰된 오브젝트를 List에 Add하고
그중 비활성화된 오브젝트를 찾아서 꺼내 쓰는 방식으로 구현했다.

 

문제는 이 방식이 O(n) 시간 복잡도를 가지다 보니,
오브젝트 수가 많아질수록 성능 부담이 커진다는 점이었는데

 

그래서 로직을 전면 개선했다.


이제는 **O(1)**로 즉시 오브젝트를 가져올 수 있고,
제네릭을 적용해 다양한 타입의 오브젝트를 하나의 풀 시스템에서 관리할 수 있게 되었다.

 

 

// PoolManager.cs
using DevJun.DebugHelper;
using System. Collections;
using System. Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PoolManager : Singleton_generic<PoolManager>
{
    [Header("몬스터 풀")]
    [SerializeField] private AiMonster monsterPrefab;
    [SerializeField] private int monsterInitial = 16;
    [SerializeField] private int monsterMax = 50;
    [SerializeField] private bool monsterExpandable = true;

    private MonsterPool _monsterPool;
    private Transform _monsterRoot;

    [Header("데미지 텍스트 풀")]
    [SerializeField] private Transform DamageTextObj;
    [SerializeField] private Transform DamageTextRoot;

    [SerializeField] private int _textInitialPerId = 4;
    [SerializeField] private int _textMaxPerId = 64;
    [SerializeField] private bool _textExpandable = true;

    [Header("데미지 텍스트 프리팹")]
    [SerializeField] DamageText _txtPrefab;

    private DamageTextPool _txtPool;

    /// ============================================================
    /// 공격 오브젝트도 개별적인 풀을 가지고 있으나 해시테이블을 통해 관리를 진행한다.
    /// ============================================================

    //DB에서 데이터를 가지고온후 공격 오브젝트 번호 세팅 기본공격은 -1부터 시작 스킬은 1번부터 시작 
    [Header("공격오브젝트 풀")]
    [SerializeField] private Transform skillRoot;
    [SerializeField] private int skillInitialPerId = 3;
    [SerializeField] private int skillMaxPerId = 5;
    [SerializeField] private bool skillExpandable = true;

    [Header("공격용 프리팹)")]
    [SerializeField] private List<AtkEffectBase> skillPrefabs = new List<AtkEffectBase>();
    private Dictionary<int, AtkObjPool> _skillCorePool;






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

        if (_monsterRoot == null)
        {
            _monsterRoot = new GameObject("[POOL] Monsters").transform;
            _monsterRoot.SetParent(transform, false);
        }

        if (skillRoot == null)
        {
            skillRoot = new GameObject("[POOL] Skills").transform;
            skillRoot.SetParent(transform, false);
        }

        if (DamageTextObj == null)
        {
            DamageTextObj = new GameObject("[POOL] txt").transform;
            DamageTextObj.SetParent(transform, false);
        }

        var pool = _monsterRoot.GetComponent<MonsterPool>();
        if (pool == null)
        {
            pool = _monsterRoot.gameObject.AddComponent<MonsterPool>();
            LogC.LogColor("에드 컴포넌트 완료", Color.red);
        }

        _monsterPool = pool;
        // 컴포넌트 초기화(필드 세팅 or Init 호출)
        _monsterPool.Init(monsterPrefab, monsterInitial, monsterMax, monsterExpandable, _monsterRoot);



        var txt_pool = DamageTextObj.GetComponent<DamageTextPool>();
        if (txt_pool == null)
        {
            txt_pool = DamageTextObj.gameObject.AddComponent<DamageTextPool>();
            LogC.LogColor("데미지 텍스트 에드 컴포넌트 완료", Color.red);
        }

        _txtPool = txt_pool;
        // 컴포넌트 초기화(필드 세팅 or Init 호출)
        _txtPool.Init(_txtPrefab, _textInitialPerId, _textMaxPerId, _textExpandable, DamageTextRoot);

        //리스트에 프리팹 세팅후 옵젝에 스킬 등록하기

        RegisterSkills();


    }

 

 

메니저를 만들어서 쉽게 관리가 가능하다.

'최적화' 카테고리의 다른 글

튜닝<그래픽>  (1) 2025.06.17
튜닝 - 물리(Physics)  (1) 2025.06.15
에셋 관련 최적화  (1) 2025.06.01
튜닝 기본 상식(2)  (0) 2025.05.17
최적화<튜닝>의 기본상식1  (4) 2025.05.11