Skip to content

UE5 C++ 타이머 & 비동기 처리

개요 — UE5에서 시간 기반 로직을 처리하는 방법

Section titled “개요 — UE5에서 시간 기반 로직을 처리하는 방법”

게임 로직에서 “3초 후 폭발”, “0.5초마다 재생”, “쿨다운 10초” 같은 시간 기반 동작은 매우 흔합니다. 언리얼 엔진은 이를 위한 여러 도구를 제공합니다.

도구특징사용 상황
FTimerHandle + FTimerManager엔진 통합 타이머, 정지·재개·취소 가능쿨다운, 딜레이, 주기적 실행
Tick + 누적 시간매 프레임 호출, 세밀한 제어매 프레임 필요한 연속 처리
Latent ActionBlueprint와 동일한 Delay 노드 C++ 구현C++ Utility 함수의 딜레이
AsyncTask게임 스레드 외부에서 무거운 작업 실행파일 IO, 네트워크, 연산 집약 작업

FTimerHandle은 생성된 타이머를 식별하는 핸들입니다. 직접 타이머 로직을 담지 않으며, 이후 타이머 취소나 상태 조회 시 이 핸들을 인자로 전달합니다.

FTimerManager는 월드에 속한 타이머 관리자입니다. GetWorldTimerManager()로 접근합니다.

// 타이머 핸들 — 헤더에 멤버로 선언
UPROPERTY() // UObject가 아니지만 직렬화/GC와 무관하므로 UPROPERTY 없어도 됨
FTimerHandle CooldownTimerHandle;
// 타이머 관리자 접근
FTimerManager& TimerManager = GetWorldTimerManager();

FTimerHandle은 UObject가 아닌 일반 구조체이므로 UPROPERTY가 필수는 아닙니다. 단, 명확성을 위해 멤버로 선언하는 것이 일반적입니다.

// GetWorldTimerManager().SetTimer(핸들, 객체, 함수, 딜레이, 반복여부, 첫번째실행딜레이)
// 예시 1: 3초 후 1회 실행
GetWorldTimerManager().SetTimer(
CooldownTimerHandle, // 핸들 (out)
this, // 대상 객체 (UObject)
&AMyActor::OnCooldownEnd, // 콜백 함수
3.f, // 딜레이 (초)
false // 반복 여부 (false = 1회)
);
// 예시 2: 0.5초 간격 무한 반복
GetWorldTimerManager().SetTimer(
TickEffectTimerHandle,
this,
&AMyActor::TickBurnEffect,
0.5f,
true // 반복
);
// 예시 3: 2초 후 시작, 1초 간격 반복
GetWorldTimerManager().SetTimer(
RegenerateTimerHandle,
this,
&AMyCharacter::RegenerateHealth,
1.f, // 반복 간격
true, // 반복
2.f // 첫 실행까지 딜레이 (생략 시 InRate와 동일)
);
// 예시 4: 람다로 콜백 지정
GetWorldTimerManager().SetTimer(
SpawnTimerHandle,
[this]()
{
SpawnEnemy();
},
5.f,
true
);
// 특정 타이머 취소
GetWorldTimerManager().ClearTimer(CooldownTimerHandle);
// 취소 후 핸들이 유효하지 않음 — IsTimerActive로 확인 가능
if (GetWorldTimerManager().IsTimerActive(CooldownTimerHandle))
{
UE_LOG(LogTemp, Warning, TEXT("Timer is still active (unexpected)"));
}
// 타이머가 활성화된 경우에만 취소 (조건부 취소 패턴)
void AMyActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// EndPlay에서 타이머 반드시 정리
GetWorldTimerManager().ClearTimer(CooldownTimerHandle);
GetWorldTimerManager().ClearTimer(RegenerateTimerHandle);
Super::EndPlay(EndPlayReason);
}

1.4 PauseTimer / UnPauseTimer — 타이머 일시정지

Section titled “1.4 PauseTimer / UnPauseTimer — 타이머 일시정지”
// 타이머 일시정지 (게임 일시정지 시 유용)
void AMyActor::OnGamePaused()
{
GetWorldTimerManager().PauseTimer(CooldownTimerHandle);
}
// 타이머 재개
void AMyActor::OnGameResumed()
{
GetWorldTimerManager().UnPauseTimer(CooldownTimerHandle);
}
// 타이머 활성 여부
bool bIsActive = GetWorldTimerManager().IsTimerActive(CooldownTimerHandle);
// 타이머 일시정지 여부
bool bIsPaused = GetWorldTimerManager().IsTimerPaused(CooldownTimerHandle);
// 남은 시간 조회
float Remaining = GetWorldTimerManager().GetTimerRemaining(CooldownTimerHandle);
// 경과 시간 조회
float Elapsed = GetWorldTimerManager().GetTimerElapsed(CooldownTimerHandle);
// UI에 남은 쿨다운 표시 예시
float UCooldownUI::GetCooldownPercent() const
{
float Remaining = GetWorldTimerManager().GetTimerRemaining(SkillTimerHandle);
return FMath::Clamp(Remaining / CooldownDuration, 0.f, 1.f);
}

2. 반복 타이머 vs 일회성 타이머

Section titled “2. 반복 타이머 vs 일회성 타이머”

2.1 일회성 타이머 — 딜레이 후 1회 실행

Section titled “2.1 일회성 타이머 — 딜레이 후 1회 실행”
// 문: 피격 후 3초 뒤 무적 해제
void AMyCharacter::StartInvincibility()
{
bIsInvincible = true;
GetWorldTimerManager().SetTimer(
InvincibilityTimerHandle,
this,
&AMyCharacter::EndInvincibility,
3.f,
false // 1회만
);
}
void AMyCharacter::EndInvincibility()
{
bIsInvincible = false;
UE_LOG(LogTemp, Log, TEXT("Invincibility ended"));
}
// 문: 화상 상태이상 — 0.5초마다 데미지
void AMyCharacter::ApplyBurnEffect(float DamagePerTick, float Duration)
{
BurnDamagePerTick = DamagePerTick;
// 반복 타이머
GetWorldTimerManager().SetTimer(
BurnTimerHandle,
this,
&AMyCharacter::TickBurnDamage,
0.5f,
true
);
// Duration 후 화상 해제 (일회성 타이머 중첩)
GetWorldTimerManager().SetTimer(
BurnEndTimerHandle,
this,
&AMyCharacter::RemoveBurnEffect,
Duration,
false
);
}
void AMyCharacter::TickBurnDamage()
{
if (StatComponent)
{
StatComponent->ApplyDamage(BurnDamagePerTick);
}
}
void AMyCharacter::RemoveBurnEffect()
{
GetWorldTimerManager().ClearTimer(BurnTimerHandle);
UE_LOG(LogTemp, Log, TEXT("Burn effect removed"));
}

3. Latent Action — C++에서 Blueprint Delay 노드 구현

Section titled “3. Latent Action — C++에서 Blueprint Delay 노드 구현”

Latent Action은 Blueprint의 Delay 노드처럼 코루틴 방식의 딜레이를 C++ UFUNCTION에서 구현할 수 있게 합니다.

MyBlueprintFunctionLibrary.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"
UCLASS()
class MYGAME_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// Latent 함수 선언 — Blueprint에서 딜레이처럼 사용 가능
UFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = "LatentInfo",
WorldContext = "WorldContextObject", Duration = "1.0"),
Category = "Utilities")
static void DelaySeconds(
UObject* WorldContextObject,
float Duration,
FLatentActionInfo LatentInfo
);
};
MyBlueprintFunctionLibrary.cpp
#include "MyBlueprintFunctionLibrary.h"
#include "Engine/LatentActionManager.h"
#include "LatentActions.h"
void UMyBlueprintFunctionLibrary::DelaySeconds(
UObject* WorldContextObject,
float Duration,
FLatentActionInfo LatentInfo)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject,
EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentManager = World->GetLatentActionManager();
// 동일 UUID의 Latent Action이 없을 때만 추가 (중복 방지)
if (!LatentManager.FindExistingAction<FDelayAction>(LatentInfo.CallbackTarget, LatentInfo.UUID))
{
LatentManager.AddNewAction(
LatentInfo.CallbackTarget,
LatentInfo.UUID,
new FDelayAction(Duration, LatentInfo)
);
}
}
}

참고: FDelayAction은 엔진 내부 클래스(LatentActions.h)입니다. 실제 Latent 로직을 커스텀하려면 FPendingLatentAction을 상속해 UpdateOperation()을 구현합니다.


4. AsyncTask — 백그라운드 스레드 작업

Section titled “4. AsyncTask — 백그라운드 스레드 작업”

게임 스레드(Game Thread)에서 무거운 연산을 실행하면 프레임 드롭이 발생합니다. AsyncTask를 사용하면 작업을 백그라운드 스레드로 분리할 수 있습니다.

#include "Async/Async.h"
// 백그라운드 스레드에서 무거운 작업 실행 후 게임 스레드로 결과 반환
void UMyGameSubsystem::LoadSaveDataAsync()
{
// 게임 스레드가 아닌 스레드 풀에서 실행
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]()
{
// 백그라운드에서 실행 — UI 업데이트, UObject 접근 금지
TArray<uint8> RawData = LoadFileFromDisk(SaveFilePath);
FSaveData ParsedData = ParseSaveData(RawData);
// 결과를 게임 스레드로 돌려보냄
AsyncTask(ENamedThreads::GameThread, [this, ParsedData]()
{
// 게임 스레드에서 실행 — UObject 접근 안전
OnSaveDataLoaded(ParsedData);
});
});
}
#include "Async/ParallelFor.h"
// 대량 데이터를 병렬로 처리
void UPathfindingComponent::BatchCalculatePaths(const TArray<FVector>& Targets)
{
const int32 Count = Targets.Num();
TArray<FNavPathSharedPtr> Results;
Results.SetNum(Count);
// 각 목표지점에 대한 경로를 병렬로 계산
ParallelFor(Count, [&](int32 Index)
{
// 각 스레드가 독립적으로 실행
Results[Index] = CalculatePath(GetOwner()->GetActorLocation(), Targets[Index]);
});
// 결과 처리는 게임 스레드에서
OnPathsCalculated(Results);
}
// 스레드 안전한 작업 (백그라운드 OK)
// - 순수 계산 (수학 연산, 데이터 파싱)
// - 파일 IO
// - 네트워크 요청
// 게임 스레드에서만 해야 하는 작업
// - UObject 생성/소멸 (NewObject, SpawnActor, Destroy)
// - UPROPERTY 접근/수정
// - 렌더링 관련 작업
// - World/Level 접근
void UMyComponent::SafeAsyncWork()
{
// 백그라운드 스레드에서 UObject에 접근하는 잘못된 예
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]()
{
// WRONG: UObject인 this가 백그라운드 스레드에서 소멸될 수 있음
// if (SomeUObjectMember) { ... } // 크래시 위험
// CORRECT: 필요한 데이터를 값으로 캡처
FString LocalData = DataToProcess; // 값 복사 후 사용
ProcessData(LocalData);
AsyncTask(ENamedThreads::GameThread, [this, LocalData]()
{
// 게임 스레드에서 UObject 안전하게 접근
if (IsValid(this))
{
HandleResult(LocalData);
}
});
});
}

CooldownComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CooldownComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnCooldownStarted, FName, AbilityName, float, Duration);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCooldownEnded, FName, AbilityName);
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class MYGAME_API UCooldownComponent : public UActorComponent
{
GENERATED_BODY()
public:
UCooldownComponent();
// 쿨다운 시작
UFUNCTION(BlueprintCallable, Category = "Cooldown")
bool StartCooldown(FName AbilityName, float Duration);
// 쿨다운 강제 종료
UFUNCTION(BlueprintCallable, Category = "Cooldown")
void CancelCooldown(FName AbilityName);
// 쿨다운 여부 조회
UFUNCTION(BlueprintPure, Category = "Cooldown")
bool IsOnCooldown(FName AbilityName) const;
// 남은 시간 조회
UFUNCTION(BlueprintPure, Category = "Cooldown")
float GetRemainingCooldown(FName AbilityName) const;
// 진행률 조회 (0.0 ~ 1.0, 0 = 쿨다운 끝, 1 = 막 시작)
UFUNCTION(BlueprintPure, Category = "Cooldown")
float GetCooldownProgress(FName AbilityName) const;
// 이벤트
UPROPERTY(BlueprintAssignable, Category = "Cooldown|Events")
FOnCooldownStarted OnCooldownStarted;
UPROPERTY(BlueprintAssignable, Category = "Cooldown|Events")
FOnCooldownEnded OnCooldownEnded;
protected:
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
struct FCooldownEntry
{
FTimerHandle TimerHandle;
float Duration = 0.f;
float StartTime = 0.f;
};
// 능력 이름 → 쿨다운 데이터 맵
TMap<FName, FCooldownEntry> ActiveCooldowns;
void HandleCooldownExpired(FName AbilityName);
};
CooldownComponent.cpp
#include "CooldownComponent.h"
UCooldownComponent::UCooldownComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
bool UCooldownComponent::StartCooldown(FName AbilityName, float Duration)
{
if (AbilityName.IsNone() || Duration <= 0.f)
{
return false;
}
// 이미 쿨다운 중이면 무시
if (IsOnCooldown(AbilityName))
{
return false;
}
FCooldownEntry Entry;
Entry.Duration = Duration;
Entry.StartTime = GetWorld()->GetTimeSeconds();
// 타이머 설정 — 람다로 능력 이름 캡처
GetWorldTimerManager().SetTimer(
Entry.TimerHandle,
[this, AbilityName]()
{
HandleCooldownExpired(AbilityName);
},
Duration,
false
);
ActiveCooldowns.Add(AbilityName, Entry);
OnCooldownStarted.Broadcast(AbilityName, Duration);
return true;
}
void UCooldownComponent::CancelCooldown(FName AbilityName)
{
if (FCooldownEntry* Entry = ActiveCooldowns.Find(AbilityName))
{
GetWorldTimerManager().ClearTimer(Entry->TimerHandle);
ActiveCooldowns.Remove(AbilityName);
OnCooldownEnded.Broadcast(AbilityName);
}
}
bool UCooldownComponent::IsOnCooldown(FName AbilityName) const
{
return ActiveCooldowns.Contains(AbilityName);
}
float UCooldownComponent::GetRemainingCooldown(FName AbilityName) const
{
if (const FCooldownEntry* Entry = ActiveCooldowns.Find(AbilityName))
{
return GetWorldTimerManager().GetTimerRemaining(Entry->TimerHandle);
}
return 0.f;
}
float UCooldownComponent::GetCooldownProgress(FName AbilityName) const
{
if (const FCooldownEntry* Entry = ActiveCooldowns.Find(AbilityName))
{
float Remaining = GetWorldTimerManager().GetTimerRemaining(Entry->TimerHandle);
return FMath::Clamp(Remaining / Entry->Duration, 0.f, 1.f);
}
return 0.f;
}
void UCooldownComponent::HandleCooldownExpired(FName AbilityName)
{
ActiveCooldowns.Remove(AbilityName);
OnCooldownEnded.Broadcast(AbilityName);
}
void UCooldownComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// 모든 활성 타이머 정리
for (auto& Pair : ActiveCooldowns)
{
GetWorldTimerManager().ClearTimer(Pair.Value.TimerHandle);
}
ActiveCooldowns.Empty();
Super::EndPlay(EndPlayReason);
}
// MyCharacter.cpp — 쿨다운 컴포넌트 사용
void AMyCharacter::UseSkill(FName SkillName, float CooldownDuration)
{
if (!CooldownComponent)
{
return;
}
// 쿨다운 중이면 사용 불가
if (CooldownComponent->IsOnCooldown(SkillName))
{
float Remaining = CooldownComponent->GetRemainingCooldown(SkillName);
UE_LOG(LogTemp, Log, TEXT("%s is on cooldown. Remaining: %.1f sec"), *SkillName.ToString(), Remaining);
return;
}
// 스킬 실행
ExecuteSkill(SkillName);
// 쿨다운 시작
CooldownComponent->StartCooldown(SkillName, CooldownDuration);
}
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (CooldownComponent)
{
// 쿨다운 종료 이벤트 구독
CooldownComponent->OnCooldownEnded.AddDynamic(this, &AMyCharacter::OnSkillCooldownEnded);
}
}
UFUNCTION()
void AMyCharacter::OnSkillCooldownEnded(FName AbilityName)
{
UE_LOG(LogTemp, Log, TEXT("Skill ready: %s"), *AbilityName.ToString());
// UI 갱신, 사운드 재생 등
}

도구핵심 함수사용 상황
FTimerHandleSetTimer, ClearTimer, PauseTimer딜레이, 반복 실행, 쿨다운
상태 조회IsTimerActive, GetTimerRemainingUI 진행률, 조건부 실행
람다 타이머SetTimer([this](){...})간단한 일회성 콜백
Latent ActionFPendingLatentAction 상속Blueprint Delay 노드 C++ 구현
AsyncTaskENamedThreads::AnyBackgroundThreadNormalTask무거운 연산 백그라운드 처리
ParallelForParallelFor(N, Lambda)대량 데이터 병렬 처리
  • EndPlay에서 모든 FTimerHandleClearTimer로 정리합니다.
  • 반복 타이머가 불필요해지면 즉시 ClearTimer를 호출합니다.
  • 백그라운드 스레드에서 UObject에 직접 접근하지 않습니다.
  • 백그라운드 결과를 UI/게임로직에 반영할 때는 반드시 게임 스레드로 되돌아와야 합니다.