본문 바로가기
Unity 궁둥이 쪼물쪼물

006. 총알에 오브젝트 풀링 적용하기

by daco2020 2026. 4. 6.

Instantiate/Destroy 방식을 오브젝트 풀링으로 교체하여
총알이 재사용되도록 구현한 과정을 기록한다.

 

 

1. 왜 오브젝트 풀링이 필요한가?

Instantiate/Destroy의 문제

발사! → Instantiate(총알 생성) → 날아감 → Destroy(총알 파괴)
발사! → Instantiate(총알 생성) → 날아감 → Destroy(총알 파괴)
발사! → Instantiate(총알 생성) → 날아감 → Destroy(총알 파괴)
... 수백 번 반복

 

이 방식의 문제점은

  • 매번 메모리를 할당하고 해제 → CPU에 부하
  • 가비지 컬렉션(GC) 발생 → 게임이 순간적으로 멈칫(버벅임)
  • 빠르게 연사하면 프레임 드랍

 

비유하자면

Instantiate/Destroy = 일회용 종이컵
  매번 새 컵 만들기 → 쓰고 버리기 → 쓰레기 쌓임 → 청소 필요

Object Pooling = 머그컵
  미리 20개 준비 → 쓰고 씻기 → 다시 사용 → 쓰레기 없음!

 

 

 

2. 오브젝트 풀링의 동작 원리

전체 흐름은

게임 시작:
  총알 20개를 미리 만들어서 비활성화 → 풀(Pool)에 보관
  [🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵]  ← 20개 대기 중

발사할 때:
  풀에서 하나 꺼냄 → 활성화 → 위치/방향 설정 → 발사!
  [🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵]  ← 19개 남음
                                              🟡 → 날아가는 중

충돌 또는 3초 후:
  비활성화 → 풀에 다시 넣기
  [🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵]  ← 다시 20개

 

 

여기서 오브젝트 풀링을 구현할 때 두 가지 자료구조를 사용할 수 있는데 비교하자면 다음과 같다.

구분 Queue (FIFO) - 선입선출  Stack (LIFO) - 후입선출
동작 먼저 들어온 것 먼저 사용 나중에 들어온 것 먼저 사용
성능 보통 더 빠름
사용 패턴 골고루 재사용 일부만 집중 재사용
캐시 효율 낮음 높음
추천 상황 균등 사용 필요할 때 성능 중요할 때
실무 사용 가끔 대부분

 

나의 경우 처음에는 Queue 로 구현하였으나 Stack 이 더 성능이 좋다는 걸 알게되어 수정하였다.

 

 

3. 코드 스크립트 추가/변경

ObjectPool.cs 추가

// 게임 시작 시 미리 생성
private void Awake()
{
    for (int i = 0; i < poolSize; i++)
    {
        GameObject obj = Instantiate(prefab, transform);
        obj.SetActive(false);    // 비활성화
        _pool.Push(obj);      // 풀에 넣기
    }
}

// 꺼내기
public GameObject GetFromPool(Vector3 position, Quaternion rotation)
{
    GameObject obj = _pool.Pop();   // 풀에서 꺼냄
    obj.transform.position = position;
    obj.SetActive(true);                // 활성화
    return obj;
}

// 반환하기
public void ReturnToPool(GameObject obj)
{
    obj.SetActive(false);     // 비활성화
    _pool.Push(obj);       // 줄 뒤에 세움
}

PlayerShooting.cs 변경

// 변경 전
GameObject bullet = Instantiate(bulletPrefab, spawnPosition, rotation);

// 변경 후
GameObject bullet = bulletPool.GetFromPool(spawnPosition, rotation);
bullet.GetComponent<Bullet>().SetPool(bulletPool);  // 풀 정보 전달

Bullet.cs 변경점

// 변경 전
private void Deactivate()
{
    Destroy(gameObject);  // 파괴 → 메모리에서 삭제
}

// 변경 후
private void Deactivate()
{
    rb.linearVelocity = Vector3.zero;    // 속도 초기화 (중요!)
    _pool.ReturnToPool(gameObject);      // 풀에 반환 → 재사용
}

 

속도 초기화가 중요한 이유

총알 재사용 시 이전 속도가 남아있으면,, 풀에서 꺼냄 → 새 방향으로 발사하려는데 → 이전 속도가 섞임 → 이상한 방향으로 날아감!

 

그러므로 총알을 반환할 때 속도를 0으로 리셋해야 한다.

 

 

4. Unity 설정

BulletPool 오브젝트 만들기

  1. Hierarchy → Create Empty → 이름: BulletPool
  2. Add Component → ObjectPool
  3. Prefab: Assets/Prefabs/Bullet 연결
  4. Pool Size: 20

 

 

Play 시 결과

Play를 누르면 BulletPool 아래에 20개의 Bullet(Clone)이 생성된다. 전부 비활성화(회색 글씨) 상태로 대기 중!

 

발사하면 하나가 활성화되고, 충돌/시간 경과 후 다시 비활성화된다.
총 개수는 항상 20개를 유지하며 만약 사용자가 3초 안에 20개 이상의 총알을 발사하면 하나씩 추가되도록 하였다. 

 

 

5. Pool Size는 어떻게 정할까?

문득 이런 의문이 들었다. 찾아보니 정답은 없고, 동시에 존재할 수 있는 최대 개수를 추측하여 정한다.

총알 수명 = 3초
발사 속도 = 초당 2~3발 (사람 클릭 속도)
동시 존재 = 3초 × 3발 = 약 9개
넉넉하게 = 20개

 

권장 Pool Size는 다음과 같다고 한다.

상황권장 Pool Size

상황 권장 Pool Size
단발 총 (이 게임) 10~20
머신건 (초당 10발) 50~100
파티클/이펙트 30~50

 

개수가 적으면 → 풀이 비었을 때 새로 Instantiate 를 만들어 하나씩 추가한다.
반대로 많으면 → 메모리를 미리 차지 하므로 낭비가 될 수 있다.

 

 

6. 이전 방식 vs 풀링 방식 비교

구분 Instantiate/Destroy Object Pooling
생성 매번 Instantiate() 처음 한 번만, 이후 GetFromPool()
파괴 Destroy() ReturnToPool() (비활성화)
메모리 할당/해제 반복 고정 (미리 확보)
GC 부하 높음 거의 없음
성능 연사 시 프레임 드랍 안정적
Hierarchy 오브젝트가 생겼다 사라짐 개수 고정, 활성/비활성 전환

 

 

 

7. 이번 작업에서 배운 것 요약

1. 오브젝트 풀링 = 미리 만들어두고 재사용 (머그컵 패턴)
2. Queue = 줄 서기 (먼저 넣은 걸 먼저 꺼냄)
3. SetActive(true/false) = 오브젝트 활성화/비활성화
4. 재사용 시 이전 상태(속도 등)를 반드시 초기화!
5. Pool Size = 동시에 존재할 최대 개수 기준으로 결정
6. OnEnable/OnDisable이 활성화/비활성화 때마다 호출됨 → 풀링과 궁합 좋음