본문 바로가기

게임 개발 공부/언리얼엔진

UE5 | C++ - FTimerHandle

언리얼에서 시간조건 함수호출 방법엔 몇가지가 존재한다.

가장 대표적인 방법이 Tick함수를 사용하는 것. 그러나 시스템에 부담을 줄 수 있어 대규모 프로젝트에선 사용하기가 조심스럽고 꺼려진다. Tick함수는 일단 켜놓으면 좋으나 싫으나 프레임당 신호를 보내기때문에, 덮어놓고 쓰다보면 부하가 생길 수 밖에 없다.

그의 훌륭한 대안 중 하나가 바로 FTimerHadle이다.

FTimerHandle은 언리얼엔진의 타이머 시스템에서 특정 타이머를 관리하고 추척하는데 사용되는 핸들 객체이다.

언리얼의 FTimerManager에서 생성된 타이머를 관리할 때 사용되며, 특정 타이머의 상태를 확인하거나 조작할 때 유용하다.

 

 

FTimerHadle은 다음과 같이 선언한다.

FTimerHadle MyTimerHandle;

 

 

타이머를 설정하는 방법은 다음과 같다.

Getworld()->GetTimerManager().SetTimer(MyTimerHandle, this, &MyActor::MyFunction, 2.0f, false);

 

각 매개변수는 다음과 같다

1. MyTimerHandle           : 사용할 타이머핸들

2. this                               : 실행할 객체 (this는 현재 객체)

3. &MyActor::MyFunction : 실행할 함수

4. 2.0f                               : 타이머 주기(현재는 2초마다 실행)

5. false                              : 반복 여부(현재는 한번만 실행)

 

 

현재 객체가 AActor 또는 UWorld기반일 경우엔 다음과 같이 GetWorld()를 생략하고 사용할 수 있다.

GetWorldTimerManager().SetTimer(MyTimerHandle, this, &AMyActor::MyFunction, 2.0f, false);

AActor와 UWorld기반 객체는 자체적으로 GetWorld()를 호출한다.

 

 

매개변수 작성에는 람다함수를 사용할수도 있다.

GetWorldTimerManager().SetTimer(MyTimerHandle, [this](){MyFunction()}, 2.0f, false);

최근부터는 변수 캡쳐에는 항상  'this'를 명시해줘야한다. 

 

 

람다함수를 이용해 작성하면 인자를 함께 넣어줄수도 있다.

GetWorldTimerManager().SetTimer(MyTimerHandle, [this, MyVariable](){MyFunction(MyVariable)}, 2.0f, false);

이 경우에도 'this'는 명시해줘야한다.

 

타이머가 현재 작동중 조건을 건다면 다음과 같이 쓸 수 있다.

if(GetWorldTimerManager().IsTimerActive(MyTimerHandle)) {}

작동 여부에 따라 코드를 진행시킬 수 있다.

 

 

타이머를 취하려면 다음과 같이 쓸 수 있다.

GetWorldTimerManager().ClearTimer(MyTimerHandle);

 

 

타이머 종료까지 남은시간은 다음과 같이 구할 수 있다.

GetWorldTimerManager().GetTimerRemaining(MyTimerHandle);

 

 

타이머 실행 후 경과된 시간은 다음과 같이 구할 수 있다.

GetWorldTimerManager().GetTimerElapsed(MyTimerHandle);

 

위와 같은 함수들을 이용하면 타이머를 이용해 다양한 기능을 구현할 수 있다.

 

 

 

얼마전, FTimerHandle을 이용해 도트 데미지를 입히고 또 FTimerHandle을 이용해서 중단하는 로직을 생각하고 구현했다.

void AWEPoisonItem::ActivateItem(AActor* Activator)
{
	if (AWorkEightCharacter* PlayerCharacter = Cast<AWorkEightCharacter>(Activator))
	{
		if (!PlayerCharacter->GetPoisonState())
		{
			Super::ActivateItem(Activator);

			if (Activator && Activator->ActorHasTag("Player"))
			{
				PlayerCharacter->PoisonAddiction(true);

				GetWorldTimerManager().ClearTimer(StopPoisonDamageTimerHandle);
				GetWorldTimerManager().SetTimer(
					StopPoisonDamageTimerHandle,
					this,
					&AWEPoisonItem::EndDamage,
					TotalDamageTime,
					false
				);

				GetWorldTimerManager().ClearTimer(PoisonDamageTimerHandle);
				GetWorldTimerManager().SetTimer(
					PoisonDamageTimerHandle,
					[this, Activator]()
					{GiveDamage(Activator); },
					DamageInterval,
					true
				);
			}
			StaticMesh->SetVisibility(false);
		}
	}
}

void AWEPoisonItem::GiveDamage(AActor* Activator)
{
	if (AWorkEightCharacter* PlayerCharacter = Cast<AWorkEightCharacter>(Activator))
	{
		UGameplayStatics::ApplyDamage(
			PlayerCharacter, 
			PoisonDamage, 
			nullptr, 
			this, 
			UDamageType::StaticClass()
		);

		if (PoisonDamageSound)
		{			
			UGameplayStatics::PlaySoundAtLocation(
				GetWorld(), 
				PoisonDamageSound, 
				PlayerCharacter->GetActorLocation()
			);
		}
	}
}

void AWEPoisonItem::EndDamage()
{
	UE_LOG(LogTemp, Warning, TEXT("ENdDamage"))
	GetWorldTimerManager().ClearTimer(PoisonDamageTimerHandle);
	GetWorldTimerManager().ClearTimer(StopPoisonDamageTimerHandle);
	//DestroyItem();

	if (ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0))
	{
		if (AWorkEightCharacter* WEPlayerCharecter = Cast<AWorkEightCharacter>(PlayerCharacter))
		{
			WEPlayerCharecter->PoisonAddiction(false);
		}			
	}	
}

 근데 왜인지는 모르겠지만, 첫번째 아이템이 발동됐을땐 제대로 데미지도 들어가고 제 시간에 멈췄는데 두번째 아이템부턴 데미지가 제 시간에 멈추지 않고 영원히 반복되었다.

로그를 이용해 확인해봤을때 타이머는 잘 작동하는데 함수를 호출하지 못하는것 같았다.

아직 이유는 모르겠지만 타이머의 공유문제인것같기도 하다.

더 연구를 해서 이유를 밝혀내면 다시 업데이트해야겠다.