Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(106일차/Final Project) - 오브젝트풀 매니저로 통합하기

by 독기품은토끼 2025. 10. 29.
✅ 오늘의 백로그
- 다른 팀 프로젝트 중간 발표 보기
- Object Pool Manager 통합

1. Object Pool

이번에 천우인을 구현하면서 오브젝트풀을 만들었었는데

다른분도 오브젝트풀을 사용하고 계시다해서 매니저를 통합하기로 하였다.

// Pool 스크립트
    /// <summary>
    /// poolSize만큼 미리 생성하여 큐에 넣기
    /// </summary>
    void CreateQueue()
    {
        for (int i = 0; i < poolSize; i++)
        {
            var go = Instantiate(cheonwooin, transform);
            go.SetActive(false);

            // 생성한 인스턴스에 Cheonwooin 스크립트가 컴포넌트로 붙어있어야함 (물리 적용한 스크립트)
            var pooled = go.GetComponent<Cheonwooin>();
            if (!pooled) pooled = go.AddComponent<Cheonwooin>();
            pooled.SetOwnerPool(this);

            _pool.Enqueue(go);
        }
    }

// 천우인 스크립트 일부 
public void SetOwnerPool(CheonwooinPool pool) => _owner = pool;

    /// <summary>
    /// 풀 되돌리기
    /// </summary>
    public void ReturnPool()
    {
        if (_owner != null)
            _owner.Return(gameObject);
        else
            gameObject.SetActive(false); // 풀이 없을 때 예외처리
    }
}

 

맨 처음 오브젝트 풀 관련 스크립트를 만들었을 때

이런식으로 구현을 했었어서

지금 내 스크립트에서 다른 프리팹과 그 프리팹의 동작이 구현된 스크립트를 연결해주려면 Cheonwooin 을 연결해준 부분을 싹 바꿔야 된다 생각했었다.

 

public interface IPoolable
{
    void SetOwnerPool(ObjectPoolManager pool); // 오브젝트풀 반납 경로 주입
}

 

그래서 서로 다른 풀 공간에 오브젝트를 반납할 수 있도록 인터페이스를 만들어주었는데

 

🚨 여기서 생길 수 있는 문제점

IPoolable 인터페이스가 ObjectPoolManager를 받도록 설계했기 때문에

풀을 모르면 구현 자체가 불가능해지고

 

 

ObjectPoolManager도 풀에 넣을 때 마다 이렇게 IPoolable을 찾고 SetOwnerPool(this)를 주입하도록 바꿔야 했다.

 

결국은 IPoolable 인터페이스로 추상화처리를 해주었음에도 불구하고

메서드 시그니처가 구체화된 타입을 요구하기 때문에 커플링이 발생하는 문제가 있었다..

 

 

천우인이 스스로 회수할 때에도 Poolable에 접근해서 ObjectPoolManager로 들어가 Return을 호출하는 형식으로 구현하였기 때문에

불과 하루전에 작성한 코드인데 왜 이따구로 적었을까 싶은 하드 코딩이다.

 

 

그리고 이렇게 구현하게 되면

나중에 씬이 전환될 때 ObjectPoolManager 스크립트는 파괴되고

일부 천우인들은 타이밍 이슈로 그대로 살아남을 수 있는데 이때 여전히 파괴된 Manager을 참조하기 때문에 NRE이 나타날 수 있다..

 


 

그래서 이번에는 모든 변수들을 다 고려해서 오브젝트 풀 매니저를 만들어주려고 한다!

 

1. 게임에서 이상현상은 난이도에 따라 0개 ~ N개 생성될 수 있다

→ 미리 큐를 만들어두면 GC 발생

오브젝트풀을 소환하는 스폰 매니저 필요

 

2. 스폰 매니저

→ 난이도를 조절해주는 부분이 필요

→ 어둑시니같은 오브젝트풀을 사용하지 않는 이상현상도 다뤄야함

→ 부적 상호작용 성공 시 해당하는 이상현상만 제거

→ 풀에서 꺼낼 때 이전 상태가 남아있지 않도록

 

우선 당장은 이정도만..? 고려하면 되지 않을까 생각든다..

 

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 단일 매니저에서 여러 프리팹의 오브젝트 풀을 관리하는 스크립트
/// 
/// 규칙:
/// - 등록(Register) : 맵에 생성될 이상현상만 등록
/// - 꺼낼 때(Get):  프리팹 키로만 요청한다. (반환값은 "비활성" 상태 / 스포너가 초기화 후 활성화)
/// - 반납(Return):  인스턴스만 넘긴다. (프리팹 키 불필요, 내부 매핑으로 복귀)
/// </summary>
public class ObjectPoolManager : MonoBehaviour
{
    // 프리팹 대기열
    readonly Dictionary<GameObject, Queue<GameObject>> _pools = new();

    // 프리팹 역매핑 (반납시 어느 큐에 넣을지)
    readonly Dictionary<GameObject, GameObject> _instanceKey = new();

    /// <summary>
    /// 맵에 생성할 prefab과 수량을 등록하는 함수
    /// 등록 시점에 size 만큼 즉시 생성하여 비활성 상태로 큐에 적재
    /// </summary>
    public void Register(GameObject prefab, int size)
    {
        if (prefab == null)
        {
            Debug.Log("Register 실패 : 프리팹이 없습니다.");
            return;
        }

        if (_pools.ContainsKey(prefab))
        {
            Debug.Log("Register 실패 : 이미 등록한 프리팹입니다.");
        }

        var q = new Queue<GameObject>(size);
        _pools.Add(prefab, q );

        for (int i = 0; i < size; i++)
        {
            var inst = Instantiate(prefab, transform); // 매니저 하위에 생성 (불편하시면 각자 프리팹 이름으로 생성되게 바꾸겠습니다!)
            inst.SetActive(false);

            q.Enqueue(inst);
            _instanceKey[inst] = prefab; // 인스턴스(inst)와 원본 프리팹(prefab)을 1:1로 역매핑 (반납 시 소속 풀을 찾기 위함)
        }
    }

    /// <summary>
    /// 지정된 프리팹의 비활성 인스턴스를 꺼내는 함수
    /// 반환값은 항상 비활성 상태다(초기화/활성은 호출자가 수행)
    /// </summary>
    public GameObject Get(GameObject prefab)
    {
        if (prefab == null)
        {
            Debug.Log("Get 실패 : 프리팹이 없습니다.");
            return null;
        }

        if (!_pools.TryGetValue(prefab, out var q))
        {
            Debug.Log($"Get 실패 : Register 먼저 호출하세요. ({prefab.name})");
        }

        // 정해진 수량을 넘어서는 사용 막기
        if (q.Count == 0)
        {
            Debug.LogWarning($"풀 소진: {prefab.name} 재고 0개");
            return null;
        }

        var inst = q.Dequeue();
        if (inst.activeSelf) inst.SetActive(false);
        return inst;
    }

    /// <summary>
    /// 사용이 끝난 인스턴스를 풀로 반환
    /// </summary>
    public void Return(GameObject instance)
    {
        if (instance == null) return;

        if (!_instanceKey.TryGetValue(instance, out var prefab))
        {
            Debug.Log("Return 실패 : 풀에서 관리하는 인스턴스가 아님");
            return;
        }

        // 상태 초기화
        instance.SetActive(false);
        _pools[prefab].Enqueue(instance);
    }
}

 

얼추 이렇게 오브젝트풀매니저는 통합했고..

천우인, 흑기 모두 이 오브젝트풀에 맞게 안에 스크립트를 좀 뜯어고쳐주어야 한다..

이 작업은 내일..!