Tiny Star
본문 바로가기
Project/Unity Bootcamp 13th

[오버워치 디펜스 모바일] | #2 오브젝트 풀

by 별터 2025. 6. 30.

지난 번 기획서 작성이 어찌저찌 끝났다.

나중에 시간이 되면 작성한 기획서를 업로드하겠다.
지금은 '좀닉'이라는 몹을 생성해서 거점까지 이동시키는 것을 구현했는데,
이미 유니티에 오브젝트 풀 기능이 있어서 이렇게 어렵게 구현하지 않아도 된다는 피드백을 받았다.

 

그래서 이 글로 전후의 차이를 남겨보려고 한다.

 

[직접 구현한 오브젝트 풀]

구현했던 로직의 결과물은 밑의 동영상에서 확인해볼 수 있다.

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

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

[Serializable]
public struct enemyPrefabEntries
{
    public EnemyType type;
    public GameObject prefab;
}

public class EnemyPoolManager : MonoBehaviour
{
    [SerializeField] private List<enemyPrefabEntries> enemyPrefabEntries;
    [SerializeField] private int initialZomnicPoolSize = 20;

    private Dictionary<EnemyType, GameObject> enemyPrefabs;
    private Dictionary<EnemyType, Queue<GameObject>> enemyPools = new();


    private void Awake()
    {
        enemyPrefabs = enemyPrefabEntries.ToDictionary(e => e.type, e => e.prefab);
        InitialInstantiate();
    }

    public void InitialInstantiate()
    {
        foreach (KeyValuePair<EnemyType, GameObject> kvp in enemyPrefabs)
        {
            if (kvp.Key == EnemyType.Zomnic)
            {
                Queue<GameObject> queue = new Queue<GameObject>();

                for (int i = 0; i < initialZomnicPoolSize; i++)
                {
                    GameObject zomnic = Instantiate(kvp.Value, transform);

                    zomnic.SetActive(false);
                    queue.Enqueue(zomnic);
                }

                enemyPools[kvp.Key] = queue;
            }
        }
    }

    public GameObject GetEnemy(EnemyType enemyType)
    {
        if (enemyPools[enemyType].Count > 0)
        {
            return Activate(enemyPools[enemyType].Dequeue());
        }

        enemyPrefabEntries prefab = enemyPrefabEntries.Find(e => e.type == enemyType);
        return Activate(Instantiate(prefab.prefab, transform));
    }

    public void ReturnEnemy(GameObject enemy)
    {
        enemy.SetActive(false);
        Enemy e = enemy.GetComponent<Enemy>();
        e.ResetStatus();
        enemyPools[e.enemyType].Enqueue(enemy);
    }

    private GameObject Activate(GameObject obj)
    {
        obj.SetActive(true);
        return obj;
    }
}

 

작성했던 코드 블럭은 이렇다.

오브젝트 풀을 딕셔너리에 저장해 직접 넣어주는 것이다.

 

위의 코드는 추후 몬스터의 종류가 늘어날 것을 고려하여 작성한 코드인데,

이번 프로젝트에서는 일단 좀닉만 구현할 예정이다.

강사님께서 미래를 보지 말고 우선 필요한 것부터 만들라고 하셔서, 확장성 고려를 제외하고 코드를 수정해보려고 한다.

 

위의 코드에서 수정할 것

1. Enemy 에서 Zomnic 으로 범위 줄여서 만들기

2. 유니티의 Pool 기능 이용하여 작성하기

 

UnityEngine.Pool

UnityEngine.Pool 네임스페이스를 사용하면 직접 Dictionary 나 Queue 로 오브젝트 풀을 구현할 필요가 없다.

 

[주요 구성 요소]

IObjectPool<T> 풀의 기본 인터페이스 (Get(), Release(T), CountInactive 등)
ObjectPool<T> 내부적으로 스택을 이용한 풀
LinkedPool<T> 내부적으로 LinkedList 를 이용한 풀
GenericPool<T>
UnsafeGenericPool<T>
정적(singleton) 풀 구현

 

[ObjectPool<T> 생성자 주요 파라미터]

public ObjectPool<T>
(
    Func<T> createFunc,
    Action<T> actionOnGet = null,
    Action<T> actionOnRelease = null,
    Action<T> actionOnDestroy = null,
    bool collectionCheck = true,
    int defaultCapacity = 10,
    int maxSize = 10000
);
createFunc 풀에서 꺼낼 오브젝트가 부족할 때 새로 만들 함수
actionOnGet Get() 이후 호출되어 초기화나 활성화를 담당
actionOnRelease Release() 시 호출되어 비활성화나 리셋 처리
actionOnDestroy 풀에서 제거할 때 호출 (주로 Destroy() 사용)
collectionCheck 풀에 반환(Release)되는 오브젝트가 이미 풀에 들어있는 중복 반환인지,
풀 밖에서 관리되지 않는 오브젝트인지 확인해 주는 안전 검사 기능

false 로 설정하면 검사하지 않으므로, 검사에 따른 오버헤드가 제거
defaultCapacity 초기 풀 크기 설정
maxSize 최대 허용 크기 설정

 

[간단 사용 예제]

using UnityEngine;
using UnityEngine.Pool;

public class EnemyPool : MonoBehaviour
{
    [SerializeField] GameObject enemyPrefab;
    [SerializeField] int initialSize = 20, maxSize = 50;

    IObjectPool<GameObject> pool;

    void Awake()
    {
        pool = new ObjectPool<GameObject>(
            createFunc:        () => Instantiate(enemyPrefab, transform),
            actionOnGet:       obj => obj.SetActive(true),
            actionOnRelease:   obj => obj.SetActive(false),
            actionOnDestroy:   obj => Destroy(obj),
            collectionCheck:   true,
            defaultCapacity:   initialSize,
            maxSize:           maxSize
        );

        // 미리 풀 채우기
        for (int i = 0; i < initialSize; i++)
            pool.Release(Instantiate(enemyPrefab, transform));
    }

    public GameObject GetEnemy() => pool.Get();
    public void ReturnEnemy(GameObject enemy) => pool.Release(enemy);
}

ObjectPool 사용 후의 코드

using UnityEngine;
using UnityEngine.Pool;

public class ZomnicPoolManager : MonoBehaviour
{
    [SerializeField] private GameObject zomnicPrefab;
    [SerializeField] private int initialSize = 20;

    IObjectPool<GameObject> zomnicPool;

    void Awake()
    {
        zomnicPool = new ObjectPool<GameObject>(
            createFunc: () => {
                var go = Instantiate(zomnicPrefab, transform);
                go.SetActive(false);
                return go;
            },
            actionOnRelease: obj => obj.SetActive(false),
            collectionCheck: true,
            defaultCapacity: initialSize
        );

        for (int i = 0; i < initialSize; i++)
            zomnicPool.Release(Instantiate(zomnicPrefab, transform));
    }

    public GameObject GetZomnic(Vector3 spawnPoint)
    {
        GameObject zomnic = zomnicPool.Get();
        zomnic.transform.position = spawnPoint;
        zomnic.SetActive(true);
        return zomnic;
    }
    public void ReturnZomnic(GameObject zomnicPrefab) => zomnicPool.Release(zomnicPrefab);
}

코드가 굉장히 간결해졌다.

나는 zomnic 이 enable 할 때, 필요한 로직들이 있어서 createFunc 에서 비활성화해주는 작업이 필요했다.

using System.Collections;
using UnityEngine;

public class WaveManager : MonoBehaviour
{
    [SerializeField] private GameObject[] spawnPoint;
    [SerializeField] private ZomnicPoolManager zomnicPoolManager;

    [Tooltip("마지막 몬스터가 생성되고 나서 다음 웨이브 시작하기까지의 시간")]
    [SerializeField] private float waveTimer = 15f;

    private int _waveCount = 1;

    private void Start()
    {
        StartCoroutine(WaveCo());
    }

    IEnumerator WaveCo()
    {
        while (true)
        {
            int idx = Random.Range(0, spawnPoint.Length - 1);
            yield return StartCoroutine(FiveZomnicSpawnCo(spawnPoint[idx].transform.position));

            yield return new WaitForSeconds(waveTimer);
            _waveCount++;
        }
    }

    IEnumerator FiveZomnicSpawnCo(Vector3 spawnPoint)
    {
        int spawnCount = 0;
        float spawnInterval = 1f;

        for (int i = 0; i < 5; i++)
        {
            yield return new WaitForSeconds(spawnInterval);

            GameObject zomnic = zomnicPoolManager.GetZomnic(spawnPoint);

            spawnCount++;
        }
    }
}
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class Zomnic : MonoBehaviour
{
    [SerializeField] private NavMeshAgent agent;
    [SerializeField] private Vector3 basePoint;

    public NavMeshAgent Agent { get { return agent; } }

    private void OnEnable()
    {
        // To Do : HP reset
        MoveToBasePoint();
    }

    public void MoveToBasePoint()
    {
        agent.Warp(transform.position);

        if (agent.isOnNavMesh == false)
            return;
        agent.SetDestination(basePoint);
    }
}

내가 이번 좀닉 이동 때 사용한 모든 코드이다. 이 세 개의 코드 스크립트로 좀닉을 스폰지점에서 생성해서 거점지점까지 옮겨줄 수 있다.

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

보이는 것은 이전과 큰 차이가 없지만, 내부 코드가 전체적으로 개편이 되었다. !!