본문 바로가기
프로젝트/Keeping Night

오브젝트 풀링 시스템

by argentdarae 2025. 1. 3.
반응형

개요

현재 개발중인 Keeping Night에서 사용하는 오브젝트 풀링 기법을 어떻게 구현하였는지 소개하고자 한다.

 

 

내용

배경

인게임에서 플레이어블 캐릭터와, 대다수의 적 유닛들이 총을 사용한다.

계속해서 인스턴싱 되는 총알 오브젝트들

 

이 때문에, 많은 총알 객체가 반복 생성 및 제거되어 성능 저하와 메모리 단편화 우려가 있었고 이를 해결하기 위해 풀링 시스템을 도입하였다.

 

 

문제

유니티에서 객체가 반복적으로 생성 및 제거되면 다음과 같은 문제가 있다.

성능 저하

Unity에서 객체를 반복적으로 생성하고 제거하면 성능 저하가 발생할 수 있다.

 

객체를 생성할 때는 메모리 할당과 초기화 등의 연산이 필요하다. 따라서 짧은 시간에 많은 객체가 생성될 경우 프레임 드랍과 같은 성능 문제가 발생할 수 있다.

 

객체를 제거할 때는 가비지 컬렉션이 일어난다. GC가 메모리를 과정에서 추가적인 CPU 자원을 소모하여 성능 저하를 초래할 수 있다.

메모리 단편화

메모리 단편화(memory fragmentation)는 메모리 할당과 해제가 반복되면서 사용 가능한 메모리가 여러 작은 조각으로 나뉘어, 연속된 큰 메모리 블록을 할당하지 못하게 되는 현상이다. 이 문제는 연속적인 메모리 공간이 필요한 경우에 특히 두드러지며, 메모리 부족 에러나 성능 저하를 유발할 수 있다.

 

Unity GC(Boehm GC)는 .NET C#의 GC(CoreCLR GC)와는 다르게 메모리 압축을 실행하지 않는다. 따라서 메모리 단편화 문제가 발생할 수 있으며, 잦은 객체의 생성 및 제거는 이 문제를 야기할 수 있다.

 

 

이를 해결하기 위하여, Unity의 오브젝트 풀을 이용하여 풀링 시스템을 구축하였다.

 

 

해결

ComponentPoolBase 클래스 구현

public abstract class ComponentPoolBase<T> : MonoBehaviour where T : Component
{
    [SerializeField] private T prefab;

    protected ObjectPool<T> objectPool;

    [SerializeField] private int defaultCapacity = 10;
    [SerializeField] private int maxSize = 20;

    protected virtual void Awake()
    {
        objectPool = new ObjectPool<T>(
            CreatePooledItem, 
            OnTakeFromPool, 
            OnReturnedToPool, 
            OnDestroyPoolObject, 
            true, 
            defaultCapacity, 
            maxSize);
    }

    protected virtual T CreatePooledItem()
    {
        return Instantiate(prefab);
    }

    protected virtual void OnTakeFromPool(T item)
    {
        item.gameObject.SetActive(true);
    }

    protected virtual void OnReturnedToPool(T item)
    {
        item.gameObject.SetActive(false);
    }

    protected virtual void OnDestroyPoolObject(T item)
    {
        Destroy(item.gameObject);
    }

    public T GetObject()
    {
        return objectPool.Get();
    }

    public void ReturnObject(T item)
    {
        objectPool.Release(item);
    }
}
public class BulletPool : ComponentPoolBase<Bullet>
{
    protected override Bullet CreatePooledItem()
    {
        Bullet bullet = base.CreatePooledItem();
        bullet.SetPool(this);  // Bullet에게 자신의 풀을 설정
        return bullet;
    }
}

 

먼저 위와 같이 UnityEngine.Pool.ObjectPool을 사용하여 풀을 구현하였다. 그리고 이를 상속받는 BulletPool을 구현하였다. 

 

구현 후에는 다음과 같이 사용하였다.

// Gun 클래스 내부

protected virtual void ProcessBulletFire(Vector2 direction)
{
    Bullet bullet = bulletPool.GetObject();

    // ...
}
// Bullet 클래스 내부

private async UniTask ReturnBulletToPool()
{
    // ...
    
    _bulletPool.ReturnObject(this);
}

 

불렛 오브젝트 풀 동작

의도한 대로 동작함을 볼 수 있었다.

 

응용

디버프 시스템을 설계하면서, 특정 총알에 피격된 상대에게 디버프를 적용해야 하는 요구 사항이 생겼다. 이 과정에서 디버프 객체 역시 반복적으로 생성 및 제거될 경우 성능 저하와 메모리 단편화 문제가 발생할 수 있어 재사용이 필요했다.

 

디버프 객체는 Monobehaviour의 제어를 받을 필요가 없어 일반 클래스로 구현하였으며, 따라서 위에서 구현한 제약조건이 달린 ComponentPoolBase는 사용할 수 없었다.

 

따라서 다음과 같이 InstancePoolBase와 DebuffPool을 구현하였다.

public abstract class InstancePoolBase<T> where T : class
{
    private readonly ObjectPool<T> _objectPool;

    protected InstancePoolBase(int defaultCapacity = 10, int maxSize = 100)
    {
        _objectPool = new ObjectPool<T>(
            CreatePooledItem, 
            OnTakeFromPool, 
            OnReturnedToPool, 
            OnDestroyPoolObject, 
            true, 
            defaultCapacity, 
            maxSize);
    }

    protected abstract T CreatePooledItem();
    protected virtual void OnTakeFromPool(T deBuff) { }
    protected virtual void OnReturnedToPool(T deBuff) { }
    protected virtual void OnDestroyPoolObject(T deBuff) { }

    public T GetObject()
    {
        return _objectPool.Get();
    }

    public void ReturnObject(T item)
    {
        _objectPool.Release(item);
    }
}
public class DeBuffPool : InstancePoolBase<DeBuffData>
{
    private readonly EWeaponDeBuffType _weaponDeBuffType;

    public DeBuffPool(EWeaponDeBuffType weaponDeBuffType, int defaultCapacity = 100, int maxSize = 100)
        : base(defaultCapacity, maxSize)
    {
        _weaponDeBuffType = weaponDeBuffType;
    }

    protected override DeBuffData CreatePooledItem()
    {
        var data = WeaponDeBuffCreateModule.CreateDeBuffData(_weaponDeBuffType);

        return data;
    }
}

 

디버프 시스템의 요구사항에 맞춰 객체 풀링을 확장하고, 일반 객체를 관리할 수 있는 구조로 성공적으로 응용하였다.

 

 

마무리

반복적인 객체 생성과 제거로 인한 성능 저하와 메모리 단편화 문제를 효과적으로 해결하였고, 추상화된 풀을 통해 총알과 디버프와 같은 다양한 객체를 관리할 수 있는 확장 가능한 구조로 구현되었다.

 

앞으로도 비슷한 문제를 해결해야할 때, 더 빠르게 문제를 해결할 수 있을 것이다.

'프로젝트 > Keeping Night' 카테고리의 다른 글

KEEPING NIGHT 프로젝트 소개  (0) 2025.04.08
조준 방향 기반 탄착군 구현  (0) 2025.04.06
로딩 시스템 구현  (2) 2025.01.03
씬 구조 설계  (0) 2025.01.03