폭탄 오브젝트 풀링
언리얼에서 Object Pooling 사용해보기
개요
폭탄을 던질 때마다 새로운 폭탄을 생성하는 기존의 방식에서, 미리 생성해둔 폭탄을 사용하는 오브젝트 풀링 방식으로 변경한다. 이를 통해 생성 및 소멸에서 발생하는 코스트를 낮추고자 한다.
1) 구조 설계
동작 관계
- 폭탄 매니저
- 캐릭터 베이스에 장착된 ActorComponent이다.
- 폭탄 오브젝트 풀(TArray)을 관리한다.
- 캐릭터 베이스로부터 어떠한 폭탄을 만들 것인지 알리는 BP_Bomb을 받아 폭탄을 생성 및 보관한다.
- 폭탄을 제공하는 함수가 있다. 비활성화(보관중)된 폭탄이 있다면 이를 활성화 시킨 이후 캐릭터에 제공한다.
- 새로운 폭탄을 얻었을 때 해당 폭탄으로 교체하는 기능이나 수량 표시 UI용 체크 기능 등을 추가할 예정이다.
- 폭탄
- 폭탄 내부의 설정을 조절하는 활성화 / 비활성화 함수가 있다.
- 폭발 호출이 들어오면 n초 이후 폭발 이펙트를 재생한다. 그리고 자동으로 비활성화되어 폭탄 매니저에 의해 관리된다.
- 캐릭터 베이스
- BP_Bomb을 보유한다.
- 폭탄 매니저를 서브 오브젝트로 가진다. BeginPlay에서 폭탄 매니저에 폭탄을 채워 넣는 함수를 호출한다.
- 캐릭터
- 실질적으로 BP_Bomb을 지정하고, 폭탄 요청 및 던지기를 담당한다.
- 받아온 폭탄의 위치와 회전을 손에 들고있는 폭탄에 맞춰 조정한 이후 던지기를 진행한다.
단일 환경에서의 오브젝트 풀링 테스트
미리 생성해둔 폭탄이 레벨에 존재하고, 이들을 꺼내서 사용하는 모습이다. 폭탄이 터지더라도 Destroy되지 않기에 수량이 변하지 않는다.
리플리케이션 고려하기
멀티플레이 환경에서 오브젝트 풀링을 사용할 것이다. 폭탄의 생성 및 관리는 서버의 몫이라는 것을 잊지 말자.
- Authority가 확인된 경우(서버)에만 진행되는 요소들
- 폭탄 매니저에 폭탄 생성 요청하기. 매니저는 모두가 각각 소유하지만 서버의 폭탄 매니저만 폭탄을 보유한다.
- 던지기 동작에서 폭탄 지급 요청 및 힘 가하기.
- 폭탄 활성화 / 비활성화 → 물리 요소는 StaticMeshComponent에 있다. 이것도 리플리케이션
- 클라이언트에서도 진행되는 요소들
- 폭탄이 터졌을 때 이펙트 재생
- 동작 몽타주 재생
- 손에 들고있는 폭탄 on/off
2) 코드
캐릭터 베이스
// EPCharacterBase.cpp
// Bomb Manager 설정
BombManager = CreateDefaultSubobject<UEPBombManager>(TEXT("BombManager"));
void AEPCharacterBase::BeginPlay()
{
Super::BeginPlay();
...
if (HasAuthority())
{
BombManager->MakeBombObjectPool(BP_Bomb);
}
}
캐릭터
// EPCharacterPlayer.cpp
if (HasAuthority())
{
...
BombToThrow = BombManager->TakeBomb();
if (BombToThrow)
{
BombToThrow->SetActorLocationAndRotation(BombInHandLocation, BombInHandRotation);
ThrowingPower = ThrowingDirection * ThrowingVelocityMultiplier * ThrowingVelocity * BombMass;
BombToThrow->GetBombMeshComponent()->AddImpulse(ThrowingPower);
}
}
폭탄
// EPBombBase.cpp
void AEPBombBase::ActiveBomb()
{
SetActorHiddenInGame(false);
SetActorTickEnabled(true);
BombMeshComponent->SetSimulatePhysics(true);
BombMeshComponent->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
bIsBombActive = true;
}
void AEPBombBase::DeactiveBomb()
{
SetActorHiddenInGame(true);
SetActorTickEnabled(false);
BombMeshComponent->SetSimulatePhysics(false);
BombMeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
bIsBombActive = false;
}
void AEPBombBase::ActiveBombTimeTrigger()
{
if (HasAuthority())
{
FTimerHandle DestroyTimerHandle;
GetWorld()->GetTimerManager().SetTimer(DestroyTimerHandle, this, &AEPBombBase::MulticastRPCExplode, BombDelayTime, false);
}
}
void AEPBombBase::MulticastRPCExplode_Implementation()
{
BombGamePlayStatics->SpawnEmitterAtLocation(GetWorld(), BombParticleSystem, GetActorLocation());
if (HasAuthority())
{
DeactiveBomb();
}
}
폭탄 매니저
//EPBombManager.cpp
void UEPBombManager::MakeBombObjectPool(TSubclassOf<AEPBombBase> BP_Bomb)
{
UWorld* World = GetWorld();
if (World)
{
for (int i = 0; i < MaxBombCount; ++i)
{
AEPBombBase* PoolableBomb = World->SpawnActor<AEPBombBase>(BP_Bomb, FVector().ZeroVector, FRotator().ZeroRotator);
PoolableBomb->SetOwner(GetOwner());
BombList.Add(PoolableBomb);
}
}
}
TObjectPtr<AEPBombBase> UEPBombManager::TakeBomb()
{
for (TObjectPtr<AEPBombBase> Bomb : BombList)
{
if (!Bomb->GetIsBombActive())
{
Bomb->ActiveBomb();
Bomb->ActiveBombTimeTrigger();
return Bomb;
}
}
return nullptr;
}
3) 최종 결과
잘 안보이지만 클라이언트는 폭탄 5~7을, 서버는 폭탄0~2를 사용하는 모습이다.
4) 오류 기록
- Physics & Collision은 StaticMeshComponent에서 설정한다. 해당 컴포넌트를 리플리케이션 하지 않을 시, 클라이언트에서만 폭탄의 움직임이 한번씩 이상해지는 문제가 발생한다. 네트워크 우선순위나 다른 옵션을 변경해도 문제가 되풀이되었는데… 리플리케이션 문제였다.
- SetSimulatePhysics는 SetCollisionEnabled보다 앞서 진행되어야한다. 또한 QueryAndPhysics or PhysicsOnly로 지정했을 때 사용 가능하다.
- 충격량 사용이 불가능하다는 메세지가 자주 발생한다. 옵션을 설정/해제 해도 상관 없이 발생. SetActorEnableCollision 사용을 주저하는 이유 중 하나이다.
- 클라이언트에서 바라볼 때 서버의 프록시가 갑자기 하늘로 치솟는 문제가 발생, 움직임도 어마어마하게 흔들리며 이동한다. 폭탄 초기화 단계에서 콜리전을 제거해버리고 이후 필요할 때 활성화 하는 것으로 해결.
- SetActorEnableCollision을 사용한다면 서버와 클라이언트의 동작이 동일해지지만, 갑작스럽게 땅에 던져졌다가 튕겨 오르는 문제가 발생, 일단 컴포넌트 리플리케이션을 사용하는 것으로 결정.
참고자료
- https://github.com/Othereum/ActorPool/blob/master/Source/ActorPool/Private/PoolActor.cpp
- https://www.slideshare.net/slideshow/ss-236587716/236587716