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 오브젝트 만들기
- Hierarchy → Create Empty → 이름: BulletPool
- Add Component → ObjectPool
- Prefab: Assets/Prefabs/Bullet 연결
- 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이 활성화/비활성화 때마다 호출됨 → 풀링과 궁합 좋음
'Unity 궁둥이 쪼물쪼물' 카테고리의 다른 글
| 008. 넉백 코드 리팩토링 (0) | 2026.04.13 |
|---|---|
| 007. 넉백(Knockback) 구현 (0) | 2026.04.06 |
| 005. 발사체(Projectile) 구현 (0) | 2026.04.06 |
| 004. Enemy(적) 애니메이션 구현 (0) | 2026.04.06 |
| 003. 적 AI 구현 & 맵 꾸미기 (0) | 2026.04.06 |