본문 바로가기
프로젝트/Lucky Defense (InGame Clone)

에너미와 유닛 재활용을 위한 풀링 시스템

by argentdarae 2025. 3. 14.
반응형

개요

프로젝트에서 구현한 풀링 시스템을 소개하는 글이다

 

 

내용

ObjectPoolBase

UnityEngine.Pool.ObjectPool을 기반으로 한 오브젝트 풀링 관리 추상 클래스다

제약 조건을 걸어 Component 타입을 내부에서 사용할 수 있게 하였다

 

일관된 패턴을 사용함으로서 개발자의 러닝 커브를 줄이고 필요에 따라 메서드를 오버라이드하여 특정 풀링 로직을 커스텀할 수 있게 하였다

public abstract class ObjectPoolBase<T> : MonoBehaviourBase where T : Component
{     
    private ObjectPool<T> objectPool;
    [SerializeField] private T prefab;        
    [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 ReleaseObject(T item)
    {
        objectPool.Release(item);
    }
}
// 예시: UnitBasePool
public class UnitBasePool : ObjectPoolBase<UnitRoot>
{
    private UnitDependencyContainer _dependencyContainer;

    public void Init(RootManager rootManager)
    {
        _dependencyContainer = new UnitDependencyContainer(rootManager);
    }

    protected override UnitRoot CreatePooledItem()
    {
        var enemy = base.CreatePooledItem();
        enemy.CreatePooledItemInit(_dependencyContainer);

        return enemy;
    }
}

 

 

PooledEntityRootBase

풀에서 가져온 엔티티의 기본 동작을 정의하는 추상 클래스를 정의하였다

public abstract class PooledEntityRootBase : MonoBehaviourBase
{
    public abstract void CreatePooledItemInit(DependencyContainerBase containerBase);
    public abstract void OnTakeFromPoolInit(EPlayerSide side);
}
public class UnitRoot : PooledEntityRootBase
{
    // ...

    public override void CreatePooledItemInit(DependencyContainerBase containerBase)
    {
        UnitDependencyContainer container = containerBase as UnitDependencyContainer;
        AssertHelper.NotNull(typeof(UnitRoot), container);

        dependencyContainer = container;
        _btController = new UnitBTController(this);
        spriteController.CreatePooledItemInit(this);
        attackController.CreatePooledItemInit(this);
        _moveController.CreatePooledItemInit(this);
        skillController.CreatePooledItemInit(this);
    }

    public void SetupUnitClassification(EUnitGrade unitGrade, EUnitType unitType)
    {
        AssertHelper.NotEqualsEnum(typeof(EnemySpawnHandler),unitGrade, EUnitGrade.None);
        AssertHelper.NotEqualsEnum(typeof(EnemySpawnHandler),unitType, EUnitType.None);

        grade = unitGrade;
        type = unitType;
    }

    public override void OnTakeFromPoolInit(EPlayerSide side)
    {
        AssertHelper.NotEqualsEnum(typeof(EnemySpawnHandler),side, EPlayerSide.None);
        AssertHelper.NotEqualsEnum(typeof(EnemySpawnHandler),grade, EUnitGrade.None);
        AssertHelper.NotEqualsEnum(typeof(EnemySpawnHandler),type, EUnitType.None);

        spriteController.ChangeVisible();
        _btController.StartBtTick();
        attackController.ChangeAttackData(this);
        _moveController.ChangeEffectScale();
        skillController.SetCurrentSkillGrade(this);
    }

	// ...
}
// UnitSpawnHandler 내부
private void SpawnUnit(SUnitSpawnRequestData data)
{
    UnitRoot unit = _unitBasePool.GetObject();
    AssertHelper.NotNull(typeof(UnitSpawnHandler), unit);
    unit.SetupUnitClassification(data.UnitGrade, data.UnitType);
    unit.OnTakeFromPoolInit(data.SpawnSide);
	
    // ...
}

 

풀에서 생성될 때 필요한 참조를 셋업하고, 풀에서 나올 때(Get) 등급과 타입에 따라 데이터를 세팅하도록 구현하였다

 

예를 들어 UnitSpriteController에서는

public void ChangeVisible()
{
    UnitMetaData metaData = _mdl.GetResource(_unitRoot.grade, _unitRoot.type);
    AssertHelper.NotNull(typeof(UnitAttackController), metaData);

    Material material = _mdl.GetUnitGradeMaterial(_unitRoot.grade);
    AssertHelper.NotNull(typeof(UnitAttackController), material);

    _spriteRenderer.sprite = metaData.Sprite;
    _spriteRenderer.material = material;
    _spriteRenderer.transform.localScale = Vector3.one * metaData.ScaleSize;
    _animator.runtimeAnimatorController = metaData.AnimatorController;
}

 

이런식으로 적절한 리소스가 세팅된다

 

 

마무리

이번 풀링 시스템 구현에서는 객체의 재사용성을 극대화하면서, 필요한 설정을 자동으로 적용할 수 있도록 설계했다

  • ObjectPoolBase<T>를 통해 일관된 패턴을 유지하면서도, 필요한 경우 오버라이드하여 커스텀할 수 있도록 구현했다
  • PooledEntityRootBase를 도입하여, 풀에서 꺼낼 때 필요한 초기화 로직을 엔티티 별로 정의할 수 있도록 했다
  • UnitSpriteController와 같은 개별 컨트롤러에서 풀링된 객체의 상태를 설정하여, 객체가 풀링될 때마다 적절한 데이터를 적용하도록 했다

이러한 구조를 통해 객체 생성/삭제 비용을 줄이고, 유지보수를 용이하게 만들었다
이 풀링 시스템을 기반으로, 향후 더 많은 유닛과 에너미를 관리할 때도 일관된 방식으로 확장할 수 있는 기반을 마련했다