개인프로젝트/IronBird

[IronBird #3] Object Pool 구현 - Spawn/Destroy 비용 제거

Client Side 2026. 5. 28. 10:34

IronBird 개발일지 #3

Object Pool 구현 - Spawn/Destroy 비용 제거

Object Pool · ABulletPool · Game 스레드 25% 감소 · FPS +27%

🎯 목표

#2에서 의도적으로 남겨둔 SpawnActor/Destroy 반복 구조를 Object Pool로 교체한다. Before 수치를 미리 기록해뒀기 때문에 교체 후 정확한 Before/After 비교가 가능하다.

Before 수치 기록(#2) → Object Pool 구현 → After 수치 측정 → 차이 분석

📖 Object Pool이란?

게임에서 총알, 이펙트, 적 같은 오브젝트는 생성과 삭제가 빈번하게 반복된다. 매번 SpawnActor/Destroy를 호출하면 메모리 할당/해제 비용이 매 프레임 발생한다.

Object Pool은 오브젝트를 미리 일정 수량 만들어두고, 필요할 때 꺼내 쓰고 다 쓰면 삭제하지 않고 비활성화해서 다시 넣어두는 방식이다.

방식 동작 비용
SpawnActor/Destroy 매번 생성 → 삭제 메모리 할당/해제 반복
Object Pool 미리 생성 → 활성화/비활성화 초기 1회 생성 비용만
// Before: 매번 생성/삭제
ABullet* Bullet = GetWorld()->SpawnActor<ABullet>(...);
Bullet->Destroy();

// After: Pool에서 꺼내 쓰고 반납
ABullet* Bullet = BulletPool->GetBullet();  // 비활성 총알 꺼냄
BulletPool->ReturnBullet(Bullet);         // 비활성화 후 반납

🔧 구현 내용

파일 변경 내용
BulletPool.h/.cpp (신규) 총알 30개 사전 생성, GetBullet() / ReturnBullet() 구현
Bullet.h/.cpp Destroy() → ReturnBullet(), Activate() 재활성화 로직 추가
IronBirdPawn.cpp SpawnActor → BulletPool->GetBullet() 교체

ABulletPool 동작 흐름

// 1. BeginPlay: 총알 30개 미리 숨김 상태로 생성
for (int32 i = 0; i < PoolSize; i++)
{
    ABullet* Bullet = GetWorld()->SpawnActor<ABullet>(...);
    Bullet->SetActorHiddenInGame(true);
    Bullet->SetActorEnableCollision(false);
    Pool.Add(Bullet);
}

// 2. GetBullet(): 비활성 총알 반환. 없으면 새로 생성
for (ABullet* Bullet : Pool)
    if (Bullet->IsHidden()) return Bullet;

// 3. ReturnBullet(): 비활성화 후 Pool 대기 상태로
Bullet->SetActorHiddenInGame(true);
Bullet->SetActorEnableCollision(false);
Bullet->SetActorTickEnabled(false);

모바일 관점: GetBullet()에서 TArray를 선형 탐색(O(n))하고 있다. 지금은 총알 30개라 문제없지만 오브젝트가 많아지면 TQueue나 인덱스 관리로 O(1) 접근으로 개선할 수 있다.

📊 Before / After 수치 비교

측정 환경: PIE, 총알 50개+ 동시 존재, 발사 간격 0.1초

항목 Before After 변화
FPS 59.69 76.20 ▲ +27%
Game 스레드 17.35ms 13.05ms ▼ -25%
Draw 스레드 11.76ms 5.43ms ▼ -54%
Draws 212 197 ▼ -7%
Mesh draw calls 11.83 avg 7.40 avg ▼ -37%

핵심 결과: Game 스레드 25% 감소가 Object Pool의 직접적인 효과다. SpawnActor/Destroy 반복 시 발생하던 메모리 할당/해제 비용이 제거됐다. Draw 스레드 54% 감소는 오브젝트 재사용으로 렌더링 상태 변경 비용도 줄어든 추가 효과다.

💡배운 것

• Object Pool은 개념은 단순하지만 효과가 명확하다. 총알처럼 빈번하게 생성/삭제되는 오브젝트에 필수적인 패턴이다.

• 비활성화는 SetActorHiddenInGame + SetActorEnableCollision + SetActorTickEnabled 세 가지를 함께 처리해야 완전히 꺼진다.

• 현재 GetBullet()은 TArray 선형 탐색 O(n)이다. 오브젝트가 많아지면 TQueue로 교체해 O(1) 접근이 필요하다.

탄막 슈팅처럼 수백 발이 필요한 경우엔 AActor 풀링 자체를 버리고 Niagara 파티클이나 UInstancedStaticMeshComponent 기반으로 전환하는 게 맞다.

최적화는 반드시 수치로 증명해야 한다. 감으로 "빠를 것 같다"가 아니라 Before/After 측정이 필요하다.

📌 다음 작업

[IronBird #4] 적 스폰 시스템

위에서 아래로 내려오는 적 기체 스폰 매니저 구현. DataTable로 스테이지별 스폰 패턴 관리.