공부 기록/유니티 Unity

[Unity/TIL] 제네릭 활용(제네릭 싱글톤, 제네릭 오브젝트 풀 예제)

톰마토 2025. 3. 8. 18:48

제네릭 활용

스탠다드반 강의를 도강하면서 유용한 정보들을 쏙쏙 뽑아왔다. (대충 꺼억콩)

제네릭 싱글톤

다양한 Manager 클래스를 만들 때 싱글톤으로 만드는 코드가 반복되는데, 제네릭을 사용해서 싱글톤 객체를 효율적으로 만들 수 있다. 

  • public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
    • Singleton : MonoBehaviour  Singleton 클래스가 MonoBehaviour를 상속받는다. (그래야 FindObjectOfType 가능)
    • where T : MonoBehaviour T에 들어올 클래스가  MonoBehaviour를 상속받고 있어야 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 제네릭 싱글톤 
/// </summary>
/// <typeparam name="T">싱글톤 인스턴스로 만들 클래스 타입</typeparam>
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<T>();
                // 찾은 후에도 없으면 생성
                if (_instance == null)
                {
                    GameObject singletonObj = new GameObject(typeof(T).Name);
                    _instance = singletonObj.AddComponent<T>();
                    DontDestroyOnLoad(singletonObj);
                }
            }
            return _instance;
        }
    }

    protected virtual void Awake()
    {
        if (_instance == null)
        {
            _instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

제네릭 싱글톤을 사용하는 예제

위의 Singleton<T> 클래스를 상속받으면 바로 싱글톤 기능을 사용할 수 있다. 

using UnityEngine;

public class GameManager : Singleton<GameManager>
{
    public int playerScore = 0;

    public void AddScore(int amount)
    {
        playerScore += amount;
        Debug.Log("현재 점수: " + playerScore);
    }
}

 

제네릭 오브젝트 풀 

오브젝트 풀링에 제네릭을 사용하면 어떤 타입의 오브젝트든 재사용할 수 있는 오브젝트 풀을 만들 수 있다. 

제네릭 오브젝트 풀 클래스

public class ObjectPool<T> where T : MonoBehaviour
{
    private Queue<T> pool = new Queue<T>();
    private T prefab;
    private Transform parent;
    
    // 생성자에서 initialSize만큼 만들어서 pool에 넣어놓기
    public ObjectPool(T prefab, int initialSize, Transform parent = null)
    {
        this.prefab = prefab;
        this.parent = parent;

        for (int i = 0; i < initialSize; i++)
        {
            T obj = Object.Instantiate(prefab, parent);
            obj.gameObject.SetActive(false);
            pool.Enqueue(obj);
        }
    }
    
    // pool에 남아있다면 빼서 주고, 없다면 새로 만들어서 줌
    public T Get()
    {
        if (pool.Count > 0)
        {
            T obj = pool.Dequeue();
            obj.gameObject.SetActive(true);
            return obj;
        }
        else
        {
            T newObj = Object.Instantiate(prefab, parent);
            return newObj;
        }
    }

    // 리턴된 오브젝트는 비활성화 시킨 뒤 pool에 넣어놓는다.
    public void Return(T obj)
    {
        obj.gameObject.SetActive(false);
        pool.Enqueue(obj);
    }
}

이 제네릭 오브젝트 풀을 이용해서 Bullet pool을 만든다고 치면 아래와 같은 코드가 되겠다. 

public class BulletPool : MonoBehaviour
{
    public static BulletPool Instance;
    private ObjectPool<Bullet> pool; // 제네릭 오브젝트 풀 사용

    public Bullet bulletPrefab;
    public int poolSize = 10;

    private void Awake()
    {
        Instance = this;
        pool = new ObjectPool<Bullet>(bulletPrefab, poolSize, transform);
    }

    public Bullet Get()
    {
        return pool.Get();
    }

    public void Return(Bullet bullet)
    {
        pool.Return(bullet);
    }
}

그리고 이 BulletPool에서 나온 Bullet은 충돌 시 싱글톤으로 만들어진 BulletPool에 리턴하면 되겠다.

public class Bullet : MonoBehaviour
{
    public float speed = 10f;

    void Update()
    {
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }

    void OnTriggerEnter(Collider other)
    {
        // 충돌 시 Bullet Pool에 반환
        BulletPool.Instance.Return(this);
    }
}

 


 

지난 '레전드 닌자' 프로젝트에서 원거리 몬스터의 Projectile을 구현할 때 오브젝트 풀링을 사용해보고 싶어서 잠시 공부했었는데, 이해하는 데에 시간이 걸려서 적용하지 못했다. 아쉬움이 남았었는데 제네릭을 활용하면서 제대로 배우게 된 것 같아서 아주 만족스럽다. 이번 개인 프로젝트에서라도 꼭 적용해보고 싶다.