[TroubleShooting]GetWorldTimeManager() - UnrealEngine
바로 전날 GetWorldTimeManager 에 대해 알아보았다. 정확히는 내 언리얼 프로젝트에서 이와 관련된 오류가 발견되어 더 깊게 알아보고자 정리한 것이었다. 오늘은 내 프로젝트에 어떤 오류가 있었고 어떻게 해결했는지 적어보겠다.
문제 상황
MineItem 이라는 클래스에서 ActivateItem 함수에 GerWorldTImeManger 을 이용해 1.7 초 뒤 이 엑터가 Destroy 되며 데미지를 주는 로직을 짰고 이는 잘 작동했다. 그러나 내 프로젝트에서는 웨이브가 끝나면 다음 레벨로 바로 넘어가지기에 웨이브가 끝나기 직전 MineItem 이 Activate 되었으면 갑자기 바뀐 World 에 의해 MineItem 이 존재하지 않게 되는 거였다!
그러나 MineItem 은 GetWorldTimerManger 을 이용해 1.7초 뒤 데미지를 주면서 파티클을 만들어야했다. 데미지를 주는 부분은 상관없으나 파티클을 생성하는 과정에서 없는 MineItem 과 바뀐 WorldTimeManger 에 의해 오류가 난 것 이었다.
void ABaseItem::ActivateItem(AActor* Activator)
{
UParticleSystemComponent* Particle = nullptr;
if (PickupParticle)
{
Particle = UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
PickupParticle,
GetActorLocation(),
GetActorRotation(),
true
);
}
if (PickupSound)
{
UGameplayStatics::PlaySoundAtLocation(
GetWorld(),
PickupSound,
GetActorLocation()
);
}
if (Particle)
{
FTimerHandle DestroyParticleTimerHandle;
GetWorld()->GetTimerManager().SetTimer(
DestroyParticleTimerHandle,
[Particle]()
{
Particle->DestroyComponent(); // 오류가 난 부분
},
0.5f,
false
);
}
}
즉 정리하자면
1. 레벨이 넘어가기 직전 MineItem 이 Activate 되는 상황이 나왔다.
2. 레벨이 넘어가자 MineItem 안의 함수에 GetWordlTimeManger 가 HandeTime 을 불러오지 못했다. 또한 MineItem Actor 도 찾을 수 없어 이에 대한 함수를 실행시킬 수 없게 되었다.
3. 그렇기에 파티클을 보여주고 DestroyComponent 해주는 과정에서 컴파일 에러가 났다..
문제 해결
처음에는 레벨이 넘어갈 때 ClearTime 을 통해 TimerHandle 을 해제하려고 하였다. 그러나 EndLevel 에 대한 로직은 GamseState 에 존재하고 이를 확인하려면 GameState 에서 EndLevel 함수를 사용할 때 MineItem 이 이를 참조하여 TimerHandle 을 해제하는 명령어를 실행해야 하는 복잡한 상황이다. 그렇기에 이것은 기각이되었다.
두 번째로는 라운드가 끝나고 레벨이 넘어갈때 3초 간의 유예시간을 주어 이펙트들이 모두 실행 된 뒤 라운드가 넘어가게 만들기로 계획하였다. 약간의 편법같지만 EndLevel 함수를 호출해야할 상황에 GetWorldTimeHandle 을 이용해 3.0f 초 뒤에 EndLevel 함수가 실행되도록 만들었다. 실제로 이 방법은 유효했다! 그러나 이 3초 동안 플레이어가 MineItem 에 오버랩 되면 다시 오류가 나기 시작했다...
결국은 두 번째 방법을 보완하기로 결정했다. 원래 게임에서 다음 레벨로 넘어가는 조건은 2개였다.
1. 맵에 스폰된 코인을 모두 획득하기
2. 시간초 동안 생존하기
1 번과 2 번의 조건이 다르기에 서로 다른 코드로 EndLevel 을 호출하게 만들었는데 둘다 오류가 발생하기에 한 함수로 묶어 EndLevel 을 호출하게 만들었다. 아래가 그 코드의 원본이다.
void ASpartaGameState::OnLevelTimeUp()
{
EndLevel();
}
원래는 2번 조건을 만족할 때 EndLevel 을 호출하게 만들었으나 이제는 3초 뒤에 EndLevel을 호출하게 만들었다. 그리고 1번 조건에도 이 OnLevelTimeUp() 을 호출하게 만들어 주었다. 아래는 수정한 코드이다.
void ASpartaGameState::OnLevelTimeUp()
{
FTimerHandle EndLevelTimerHandle;
GetWorldTimerManager().SetTimer(EndLevelTimerHandle, this, &ASpartaGameState::EndLevel, 3.0f, false);
}
FtimerHandle을 선언해주고 3.0f 초 뒤에 한번 EndLevel 을 호출하게 만들어 주었다. 그러나 아직 문제가 남았다. 이 3초 동안 플레이어가 마인 아이템을 건드리면 다시 오류가 발생한다! 그렇다면 플레이어가 아이템을 먹을 수 없게 만들면 되지 않을까? 그렇기에 PlayerController 에서 Pawn 을 불러와 그 폰의 콜리전을 DisableCollision() 을 통해 오버랩이 불가능 하게 만들었다. 아래가 최종 코드이다.
void ASpartaGameState::OnLevelTimeUp()
{
if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
{
if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(PlayerController->GetPawn()))
{
PlayerCharacter->DisableCollision();
}
FTimerHandle EndLevelTimerHandle;
GetWorldTimerManager().SetTimer(EndLevelTimerHandle, this, &ASpartaGameState::EndLevel, 3.0f, false);
}
문제 해결이 끝나고
이번에 문제 해결을 하며 뿌듯하였는데 처음 오류가 생기자마자 어떤 곳이 문제인지 바로 알았기 떄문이었다. 그러나 이 문제를 해결하는데는 좀 오래 걸렸었다. 처음에는 깔끔하게 ClearTimer 을 사용하면 될 줄 알았는데 먹히지 않아서 결국은 편법으로 문제를 해결하였다. 이게 최선의 수였을까? 라고 물으면 난 단호하게 아니라고 말할 수 있다. 그러나 이런 방식으로 문제를 해결했기에 게임의 완성도는 조금 높아진 느낌이다.
완성도가 높아졌다 느낀 이유는 라운드가 끝나고 3초 동안의 시간이 주어지니 약간의 숨을 돌릴 시간이 되기도 하고 이 시간에 UI 를 추가하여 다음 라운드로 넘어감을 예고 할 수 있게 되었다. 처음에는 라운드가 끝나자마자 바로 다음 라운드로 넘어갔는데 다시 돌이켜보면 게임으로서는 조금 어색했던 것 같다.
오늘 경험으로 느낀바는 트러블 슈팅은 문제를 최적으로 해결하는 것 보다는 이를 통해 전체적으로 더 완성도 있게 만드는 방법인 것 같다. 앞으로 많은 오류가 생길 것이고 많은 해결방법을 찾아내야 할 것이다. 그 중 어려웠거나 흥미로웠던 부분들은 트러블 슈팅을 작성해 보도록 하겠다.