언리얼엔진(UE)

[UE] 타이머 사용 시 유의 사항과 해결방법

2h1824 2025. 2. 12. 23:10

1. 문제 개요

오늘은 이전에 강의를 들으면서 만들고 있던 간단한 코인 줍기 게임에서 지뢰 아이템과 관련한 로직을 만지는 도중에 문제를 마주했다.

게임 시작 시 메뉴와 게임 오버 시 메뉴를 구분해서 구현한 뒤에 각각 시작, 재시작에 해당하는 버튼을 만들고 해당 버튼을 눌렀을 때, 정상적으로 게임이 초기화되어 시작이 되는지를 확인하는 중이었는데, 어떤 방식이든 상관없이 다시 시작을 하는 과정에서 곧장 게임이 종료될뿐만 아니라 에디터까지 함께 종료되는 크래시 현상을 보였다. 어딘가 잘못되었다는 것은 확실했지만, 이전까지 다른 기능들을 구현하고 확인하는 과정에서는 별다른 이상현상을 보이지 않았기에 도대체 뭐가 문제인지 생각해보았다.

크래시 문구와 함께 에러와 관련한 문구를 살펴보다 보니 내가 이전에 구현했던 지뢰 아이템과 관련한 곳에서 에러가 발생하길래 디버그 모드로도 한번 재시작을 했더니 아니나 다를까 지뢰 아이템 쪽 코드에서 에러가 발생하는 것을 확인했다. 이후 곰곰히 생각해 보니 에러가 발생하는 재시작 조건은 내가 일부러 재시작 버튼을 시험해보기 위해 지뢰를 여러개 밟아 죽은 직후에 재시작을 눌렀을 때였다. 이쯤되니 짚이는 점이 있었는데 지뢰 폭발 시 지뢰 아이템을 Destroy 하고 파티클도 타이머를 이용해 일정 시간 이후 삭제가 되게끔 하였는데, 아무래도 지뢰가 터지고 파티클이 사라지기 전에 내가 재시작 버튼을 누름으로써 레벨이 삭제, 재생성 되는 과정에서 파티클 삭제 관련 타이머가 호출되기 전에 액터가 강제로 삭제가 되면서 발생한 에러였다.

2. 해결 과정

처음에는 타이머를 아이템을 삭제하기 전에 설정하면 혹여라도 해결이 될까 싶어 한번 시도해 보았다.

void AMineItem::Explosion()
{
	UParticleSystemComponent* Particle = nullptr;

	if (ExplosionParticle)
	{
		Particle = UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld()->PersistentLevel,
			ExplosionParticle,
			GetActorLocation(),
			GetActorRotation(),
			false
		);
	}

	if (ExplosionSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			GetWorld(),
			ExplosionSound,
			GetActorLocation()
		);
	}

	TArray<AActor*> OverlappingActors;
	ExplosionCollision->GetOverlappingActors(OverlappingActors);

	for (AActor* Actor : OverlappingActors)
	{
		if (Actor && Actor->ActorHasTag("Player"))
		{
			//데미지 발생시켜 Actor->TakeDamage() 호출하도록 만듬
			UGameplayStatics::ApplyDamage(
				Actor,
				ExplosionDamage,
				nullptr,
				this,
				UDamageType::StaticClass()
			);
		}
	}

	if (Particle)
	{
		GetWorld()->GetTimerManager().SetTimer(
			DestroyParticleTimerHandle,
			[Particle]()
			{
				// GameThread에서 DestroyComponent() 실행
				AsyncTask(ENamedThreads::GameThread, [Particle]()
					{
						if (Particle)
						{
							Particle->DestroyComponent();
						}
					});
			},
			2.0f,
			false
		);
	}

	DestroyItem();

}

더불어 GameThread에서 파티클의 DestroyComponent를 실행하면 에러가 발생하지 않을 수 있다고 하는 말도 있었고, 파티클을 PersistentLevel에 생성하면 괜찮다는 말도 있어 관련한 방법들을 하나씩 적용하다 보니 위와 같은 코드가 되었지만 여전히 에러는 발생했다.

//EndPlay() 에서 모든 타이머 해제
void AMineItem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	// ExplosionTimerHandle 해제
	if (ExplosionTimerHandle.IsValid())
	{
		GetWorld()->GetTimerManager().ClearTimer(ExplosionTimerHandle);
	}

	// DestroyParticleTimerHandle 해제
	if (DestroyParticleTimerHandle.IsValid())
	{
		GetWorld()->GetTimerManager().ClearTimer(DestroyParticleTimerHandle);
	}
}

결국 문제를 해결하기 위해 떠올린 방법은 위 코드였다. 레벨이 삭제, 재생성 되는 과정에서 타이머가 해제되지 않아 발생하는 문제라면 레벨이 삭제되는 과정에서 액터들을 모두 삭제하기 위해 지뢰 아이템이 삭제될 때 호출되는 EndPlay에서 파티클 삭제 관련 타이머와 폭발 관련 타이머들을 혹시 모르니 모두 해제시켜 주는 방법이었다. 기대 반 걱정 반으로 위 코드를 작성하고 실행을 하며 여러번의 테스트를 거친 결과는 성공이었다.

3. 정리

오늘 문제를 해결하는 과정에서 느낀 것은 타이머를 다룸에 있어 여러 가지 예외 사항들을 상정하고 대비하는 편이 좋겠다는 것이다. 다른 것들도 마찬가지겠지만 폭발 아이템과 같은 것을 구현할 때는 아이템이 사라지고 이펙트가 생기는 경우가 종종 있는데 보통 상황에서야 별다른 문제가 없겠지만, 나의 경우에는 폭발 시점에서 캐릭터가 죽어버리면 다시 재시작을 하는 과정에서 해제되지 않은 타이머가 남아 버리면서 마치 댕글링 포인터와 같이 타이머가 붕 떠버려 메모리 참조 관련 에러가 발생했다. 앞으로 타이머를 사용할 때는 특정 상황에서 타이머를 해제해줘야 하지는 않는지 고려하는 습관을 들일 것 같다.


참고자료

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/blueprint-debugging-example-in-unreal-engine