(유니티)ObjectPool 오브젝트 풀링

티원·2025년 9월 7일

디자인패턴

목록 보기
7/16

오브젝트풀 설명 + 시연영상

오브젝트 풀링이란?

  • 게임에서 자주 생성하고 삭제되는 오브젝트를 미리 만들어 놓는 것
  • 어떤 Objcet가 생성과 삭제가 잦으면 메모리 단편화 발생할 수 있다 + GC스파이크
  • 메모리 단편화란? : 연속된 주소가 아닌 주소값 사이사이에 빈 공간이 생김

장점: 메모리 단편화 방지, 성능 향상(매번 생성, 삭제하는 비용 절감)
단점: 초기 메모리 점유, 풀 크기 조절 필요 (너무 크면 메모리낭비, 적으면 성능저하)

외부단편화 : 남아있는 메모리의 크기가 실행하려는 프로세스보다 크지만, 연속적이지 않은 공간에 존재
내부단편화 : 100MB만큼 저장하는 메모리공간에 80MB 프로세스 할당되면 20MB는 할당불가능상태가 된다
페이징,세그멘테이션~~ 개념도 이어짐

코드가 상당히 어려운데, 꾸준히 학습이 필요하고, 결국 중요한건 코드가 아니라 오브젝트풀 개념을 알고 내가 사용할 프로젝트에 적용할 수 있어야 함

오브젝트 풀 필수 항목
1. 미리 정해진 갯수만큼 만들고 저장
2. 필요할때 꺼내쓰고 다 썼으면 SetActive(false); 처리. Destroy 하지 않는다.
3. (선택) 꺼낼때 오브젝트 풀 공간을 모두 쓰고있다면 새로운 오브젝트 할당해서 풀 크기를 늘릴 수 있다.

코드 (정말 기본적인 오브젝트 풀)

  • Instantiate를 한번에 여러개 해두고, 이 배열에서 꺼져있는거 찾아서 키고, 포지션 바꾸고 사용하다가 사라져야할때 Destroy를 하지않고 Disable만 한다.
using System.Collections.Generic;
using UnityEngine;

public class PolingScript : MonoBehaviour
{
    //폴링을 위해서는 폴링을 만들 배열(리스트, 스택 큐 아무거나 가능) 필요
    [SerializeField] GameObject prefab;
    [SerializeField] int polingCount = 10;
    List<GameObject> polingObjcets;
    

    private void Awake()
    {
        //미리 polingCount 갯수만큼 SetActive false 상태로 만듬
        polingObjcets = new List<GameObject>();
        for(int i=0; i<polingCount; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.SetActive(false);
            obj.name = "bullet_" + i;
            polingObjcets.Add(obj);
        }
    }

    public GameObject GetPolingObject()
    {
        //가져와야할때 해당 함수 호출해서 1개씩 꺼낸다
        //개선점: 고정 위치가 아닌 Player 위치에서, 바라보는 방향으로 총알을 이동시켜야함.생성되면
        for(int i=0; i< polingCount; i++)
        {
            if (polingObjcets[i].activeInHierarchy == false)
            {
                polingObjcets[i].SetActive(true);
                return polingObjcets[i];
            }
        }
        GameObject obj = Instantiate(prefab);
        obj.SetActive(true);
        obj.name = "bullet_" + polingCount;
        polingObjcets.Add(obj);
        polingCount++;
        return obj;
    }
    public void ReturnObject(GameObject obj)
    {
        //오브젝트 다시 비활성화
        obj.SetActive(false);
    }
}

오브젝트풀 + 큐를 이용 코드

리스트에서 매번 검색해서 .isActiveHierachy해야되는데 비활성화체크가 비효율적임
큐로 변경하는데 대신 자기자신이 할당되었던 위치를 찾아서 다시 반환해주는 과정 필요

  1. 총알 발사 로직 소스
public class FireLogic : MonoBehaviour
{
    [Header("발사관련설정")]
    [SerializeField] GameObject bulletPrefab;
    [SerializeField] int initialPoolSize = 10;
    [SerializeField] float bulletSpeed = 20f;

    //private List<GameObject> bulletPool; //리스트에서 매번 검색해서 .isActiveHierachy해야되는데 비활성화체크가 비효율적임 -> 큐로 변경
    private Queue<GameObject> bulletPool = new Queue<GameObject>(); //총알을 쏠때마다 Dequeue, 끝나면 다시 Enqueue , 대신 누가쏜건지 몰라서 알려줘야함

    private InputAction fireAction;

    //총알 새로 만들고 풀에 담음
    GameObject CreateNewBullet()
    {
        GameObject newBullet = Instantiate(bulletPrefab);
        newBullet.SetActive(false);
        bulletPool.Enqueue(newBullet);
        return newBullet;
    }
    void OnShoot(InputAction.CallbackContext ctx)
    {
        FireBullet();
    }
    private void Awake()
    {
        //오브젝트풀 선언 및 초기 크기 지정
        for(int i=0; i< initialPoolSize; i++)
        {
            CreateNewBullet();
        }
        fireAction = InputSystem.actions["Attack"];
    }

    //유니티6라서 Input을 InputAction으로 해야해서 performed 코드작성
    private void OnEnable()
    {
        fireAction.performed += OnShoot;
    }
    private void OnDisable()
    {
        fireAction.performed -= OnShoot;
    }
    //총알 발사
    void FireBullet()
    {
        //풀에서 총알 하나 가져온다
        GameObject bullet = GetInactiveBullet();
        ////풀에 남은공간이 없다면 하나 만들어줌
        if (bullet == null)
        {
            bullet = CreateNewBullet();
        }
        //총알 위치 및 Active상태 설정
        bullet.transform.position = transform.position + transform.forward * 2; //발사자 살짝앞에
        bullet.transform.rotation = transform.rotation; //발사자 방향
        bullet.SetActive(true);
        //의존성주입 (플레이어가 총알쏜건지 적이 총알쏜건지 기억하기위해)
        bullet.GetComponent<BulletCtrl>().SetOwner(this);
        //총알 발사 진행
        bullet.GetComponent<Rigidbody>().linearVelocity = transform.forward * bulletSpeed;
    }

    GameObject GetInactiveBullet()
    {
        if(bulletPool.Count > 0) //사용가능 총알이 있다면
        {
            return bulletPool.Dequeue();
        }

        return null;
    }
    public void ReturnBullet(GameObject bullet)
    {
        bullet.SetActive(false);
        bulletPool.Enqueue(bullet);
    }
}
  1. 총알 소스
    자신을 발사한 곳에 풀이 있기때문에 SetOwner 이용
public class BulletCtrl : MonoBehaviour
{
    public float lifeTime = 3f;
    Rigidbody rb;
    FireLogic fireLogic; 
    public void SetOwner(FireLogic fire)
    {
        fireLogic = fire;
    }

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }
    private void OnEnable()
    {
        Invoke(nameof(DisableBullet),lifeTime);
    }
    private void OnDisable()
    {
        rb.linearVelocity = Vector3.zero; //이전 속도가 남아있을수있어서
        rb.angularVelocity = Vector3.zero;
        CancelInvoke(); //모든 인보크 없애버림
    }

    private void OnCollisionEnter(Collision collision)
    {
        DisableBullet();
    }
    void DisableBullet()
    {
        fireLogic.ReturnBullet(this.gameObject);
    }
}

오브젝트 풀링 + 제네릭화 (사용)

오브젝트풀+제네릭 시연
인터페이스를 사용하여 구현하는 방법과, 사용하지 않고 하는 방법을 저장해두었다.
인터페이스 안쓰는 방법으로 프로젝트때는 썼었음

1. 인터페이스 안쓰고 오브젝트풀 + 제네릭 사용

마찬가지로 총알에서 풀을 반환할때 ReturnObject()을 호출해주면 된다

public class ObjPool<T> where T : MonoBehaviour
{
    //몬스터, 적, 총알, 파티클 등
    Queue<T> pool;
    private T prefab; 
    Transform poolParent;   //하위객체에 넣어서 관리할때 사용
    int aliveCount; //풀에 활성화되어있는 객체 숫자
    public ObjPool(T prefab, int initSize, Transform parentPos = null)
    {
        this.prefab = prefab;
        pool = new Queue<T>();
        this.poolParent = parentPos;
        aliveCount = 0;

        for (int i = 0; i < initSize; i++)
        {
            //ObjPool은 MonoBehavior를 상속받지 않기때문에 Object.을 붙여야함, Where은 제약조건이지 상속을받지는않았다
            T instance;
            if (poolParent == null)
            {
                instance = Object.Instantiate(prefab, poolParent);
            }
            else
            {
                instance = Object.Instantiate(prefab, poolParent);
            }
            instance.gameObject.SetActive(false);
            pool.Enqueue(instance);
        }
    }

    public T GetObject()
    {
        aliveCount++;
        if (pool.Count > 0)
        {
            T obj = pool.Dequeue();
            obj.gameObject.SetActive(true);
            return obj;
        }
        //풀이 모두 사용중이라면 풀을 늘림
        T newObj;
        if (poolParent == null)
        {
            newObj = Object.Instantiate(prefab);
        }
        else
        {
            newObj = Object.Instantiate(prefab, poolParent);
        }
        return newObj;
    }

    public void ReturnObject(T obj)
    {
        aliveCount--;
        pool.Enqueue(obj);
        obj.gameObject.SetActive(false);
    }
    public int AlivePoolCount()
    {
        return aliveCount;
    }
}

사용예제

public class ExpPointSpawner : Singleton<ExpPointSpawner>
{
    //오브젝트풀 프리팹,사이즈 , 선언
    [SerializeField] ExpPointScript _expPointPrefab;
    [SerializeField] int poolSize = 10;
    ObjPool<ExpPointScript> _expPointPool;
    public event Action<int> _getExpPoint;

    protected override void Awake()
    {
        isDestroyOnLoad = false;
        base.Awake();
        _expPointPool = new ObjPool<ExpPointScript>(_expPointPrefab, poolSize, gameObject.transform);
    }
    //생성 코드
    public ExpPointScript GetPrefabByPool(Transform trf)
    {
        ExpPointScript expBuf =  _expPointPool.GetObject();
        expBuf.transform.position = trf.position;
        expBuf._destroyExpPoint += ReturnExpPointToPool;
		return expBuf;
    }
    //오브젝트 반환 (반환이벤트없으면 public으로 써도됨)
    private void ReturnPrefabToPool(ExpPointScript expPoint)
    {
        expPoint._destroyExpPoint -= ReturnExpPointToPool;
        _expPointPool.ReturnObject(expPoint);
        //경험치를 주웠을때 
        _getExpPoint?.Invoke(10);
    }
}

//오브젝트풀로 만들 경험치구슬 클래스
public class ExpPointScript : MonoBehaviour
{
    //오브젝트풀에 반환하기위한 이벤트
    public event Action<ExpPointScript> _destroyExpPoint;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("발동");
        if (collision.CompareTag("Player"))
        {
            Debug.Log("이벤트까지발동");
            _destroyExpPoint?.Invoke(this);
        }
    }
}

사용은 이런식으로 받아오면 된다

Monster monsterBuf = ExpPointSpawner.Instance.GetMonsterByPool(transform);

1-2 추가 (몬스터마다 다른 스케일을 지정하기 위해서 부모 오브젝트가 필요했음)

  • 상황 설명 : 몬스터마다 다른 스케일을 지정하기 위해서 오브젝트풀할때 Monster가 담겨있는 부모오브젝트가 필요했음(부모에서 localScale설정하기위햬)

제네릭오브젝트풀 코드는 안건드리고,
Prefab을 MonsterParent-Monster구조로 저장되도록 하나만들고, ObjectPool 소스가 있는 'MonsterSpawner'클래스에 MonsterParent가 가지는 스크립트를 하나만들어서 오브젝트풀을 만들고, Get과 Return을 반환할때 Monster를 반환하도록 설정했음
MonsterPreab의 부모에 해당 스크립트도 넣고 끝(MonsterRoot, MonsterSpawner.cs파일 참조)

public class MonsterRoot : MonoBehaviour
{
    //몬스터의 부모에 스크립트를 넣어서 스케일을 관리하기 위해서 추가
    private Monster _monster;
    public Monster Monster => _monster;

    private void Awake()
    {
        _monster = GetComponentInChildren<Monster>();
    }
    public void SetScale(float value)
    {
        //스케일 조정  (값에 따라)
        transform.localScale = new Vector3(value, value, value);
    }
}
public class MonsterSpawner : Singleton<MonsterSpawner>
{
    //오브젝트풀 프리팹,사이즈 , 선언
    [SerializeField] MonsterRoot _monsterPrefab;
    [SerializeField] int poolSize = 10;
    ObjPool<MonsterRoot> _monsterRootPool; //기존 오브젝트풀과 다르게 부모를 하나 추가하기위해 MonsterRoot를 넣어서 사용

    protected override void Awake()
    {
        isDestroyOnLoad = false;
        base.Awake();
        _monsterRootPool = new ObjPool<MonsterRoot>(_monsterPrefab, poolSize, gameObject.transform);
    }
    //생성 코드
    public Monster GetMonsterByPool(Transform trf)
    {
        MonsterRoot monParent = _monsterRootPool.GetObject();
        monParent.transform.position = trf.position;
        Monster monsterBuf = monParent.Monster;
        //monsterBuf.transform.position = trf.position;
        monsterBuf.OnMonsterDead += ReturnMonsterToPool;
        return monsterBuf;
    }
    //오브젝트 반환
    private void ReturnMonsterToPool(Monster monster)
    {
        monster.OnMonsterDead -= ReturnMonsterToPool;
        //부모를 가져온다
        MonsterRoot monParent = monster.GetComponentInParent<MonsterRoot>();
        _monsterRootPool.ReturnObject(monParent);
    }
}

2. 오브젝트풀 + 제네릭 (인터페이스 사용)

2-1.인터페이스

public interface IPoolable
{
    public void OnCreate(); //생성 시
    public void OnSpawn(); //풀에서 꺼내질때
    public void OnDeSapwn(); //풀로 돌아갈때
}

2-2. 오브젝트풀 (인터페이스 사용)

public class ObjPool<T> where T : Component, IPoolable
{
    private T _prefab;
    private Stack<T> _pool;
    public bool IsExpandable { get; set; } //모자라면 확장 가능하게 할건지
    public bool IsEmpty => _pool.Count == 0;

    public ObjPool(T prefab, int capacity = 10, bool isExpandable = false)
        => Init(prefab, capacity, isExpandable);
    //초기 생성자
    private void Init(T prefab,int capacity, bool isExpandable)
    {
        _prefab = prefab;
        IsExpandable = isExpandable;
        _pool = new Stack<T>(capacity);
        for(int i=0; i< capacity; i++)
        {
            T pooledObject = CreatePooledObject();
            Despawn(pooledObject);
        }
    }
    //생성 처리 함수 (하나씩 반환해준다)
    public T Spawn()
    {
        T pooledObject;
        if (IsExpandable && IsEmpty) pooledObject = CreatePooledObject();
        else pooledObject = _pool.Pop();
        pooledObject.OnSpawn();
        return pooledObject;
    }
    //다쓰고 생성 해제 함수 (Pool로 반환)
    public void Despawn(T poolableObject)
    {
        poolableObject.OnDeSapwn();
        _pool.Push(poolableObject);
    }
    //객체 하나 생성할때 사용
    private T CreatePooledObject()
    {
        T pooledObject = MonoBehaviour.Instantiate(_prefab);
        pooledObject.OnCreate();
        return pooledObject;
    }
}

2-3 총알소스 (인터페이스 사용)

public class BulletController : MonoBehaviour, IPoolable
{
    Rigidbody _rigidbody;
    private float _timeCount;
    public event Action<BulletController> OnLifetimeEnd;
    //오브젝트풀 사용법 1. 인터페이스를 적용후 구현
    public void OnCreate() //생성
    {
        Init();
    }
    public void OnSpawn() // 풀에서 꺼냈을때
    {
        gameObject.SetActive(true);
    }
    public void OnDeSapwn() //풀로 돌아갈때
    {
        gameObject.SetActive(false);
    }
    public void OnDispose() //파괴될때
    {
        Destroy(gameObject);
    }
    private void Init()
    {
        _rigidbody = GetComponent<Rigidbody>();
    }
    //총알 위치 지정
    public void SetUp(Vector3 position, Quaternion rotation, Vector3 force, float lifetime)
    {
        transform.SetPositionAndRotation(position, rotation);
        _rigidbody.linearVelocity = Vector3.zero;
        _rigidbody.AddForce(force, ForceMode.Impulse);
        _timeCount = lifetime;
    }
    private void Update() => HandleTimeout();
    //총알 수명체크 (코루틴써도됨)
    private void HandleTimeout()
    {
        _timeCount -= Time.deltaTime;
        if(_timeCount <= 0)
        {
            OnLifetimeEnd?.Invoke(this);
        }
    }
}

2-4.플레이어 무기(총) 코드

public class GunController : MonoBehaviour
{
    [SerializeField] private Transform _muzzleTransform;
    [SerializeField] private float _force;
    [SerializeField] private float _timeToBulletLive;
    [SerializeField] private BulletController _bulletPrefab;

    private ObjPool<BulletController> _bulletPool;

    private void Awake() => Init();
    private void Update() => HandleGun();

    private void Init()
    {
        _bulletPool = new ObjPool<BulletController>(_bulletPrefab, 10, true);
    }
    private void HandleGun()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1)) Fire();
    }
    private void Fire()
    {
        BulletController bullet = _bulletPool.Spawn();
        bullet.OnLifetimeEnd += ReturnToPool;

        Vector3 force = _muzzleTransform.forward * _force;
        bullet.SetUp(_muzzleTransform.position, _muzzleTransform.rotation, force, _timeToBulletLive);
    }
    private void ReturnToPool(BulletController bullet)
    {
        bullet.OnLifetimeEnd -= ReturnToPool;
        _bulletPool.Despawn(bullet);
    }
}

유니티 내장 오브젝트 풀 코드

public class RealFinalPool : MonoBehaviour
{
    //유니티 내장풀 사용해봄

    ObjectPool<GameObject> myPool; //유니티 내장 풀을 만듬
    public GameObject toInstantiate;
    private void Start()
    {
        //생성할프리팹
        //Get수행메서드
        //Return수행메서드
        //파괴시메서드
        /*
        myPool = new ObjectPool<GameObject>
            (
                CreateNewObject,
                ActivateObject,
   DeactivateObject,
   DestroyObject

            );
        */
        //한줄이니까 람다식으로 쓸수있다
        //아래 toInstantiate 델리게이트 캡쳐 조심하기
        myPool = new ObjectPool<GameObject>
            (
               ()=> Instantiate(toInstantiate),
                obj => obj.SetActive(true),
              obj => obj.SetActive(false),
             obj => Destroy(obj.gameObject)

            );
    }
    //객체 생성 메서드
    GameObject CreateNewObject()
    {
        return Instantiate(toInstantiate);
    }
    //객체 활성화 메서드 및 풀에서 꺼낼때 수행할 메서드
    void ActivateObject(GameObject obj)
    {
        obj.SetActive(true);
        //+ 위치를 발사자 위치로 변경 
    }
    void DeactivateObject(GameObject obj)
    {
        obj.SetActive(false); 
        //+ 총알의경우 리니어벨로시티 0으로 만드는 코드 작성
    }
    void DestroyObject(GameObject obj)
    {
        Destroy(obj.gameObject);
    }

    //플레이어는 아래처럼 플레이어 로직만 들고있게 하고싶음
    //아래코드는 그냥 예시를 위해 막 쓴 코드임
    private void Update()
    {
        if(Input.GetKeyUp(KeyCode.Escape))
        {
            var obj = myPool.Get();
            obj.GetComponent<Rigidbody>().AddForce(transform.position);
            //myPool.Release(반환시키고자하는 총알);
        }
    }

}

  • 이해 단계 차근차근히
    1.오브젝트풀 개념
    2.배열/List로 SetActive true, false (+Queue로 구현)
    3.심화: 제네릭 버전으로 만든다
    4.모든걸 다 액션 기반으로 등록할 수 있는 시스템구축 (수업때는안함)

  • 유니티 내장 오브젝트풀이랑 제네릭화 커스텀 둘중 무엇을 쓸지 고민

  • 맵 정보를 오브젝트풀로 저장했다가 플레이어가 더이상 안가는곳은 없애고, 다음에갈곳은 생성해놔서 응용가능

0개의 댓글