UE5 C++ 타이머 & 비동기 처리
개요 — UE5에서 시간 기반 로직을 처리하는 방법
Section titled “개요 — UE5에서 시간 기반 로직을 처리하는 방법”게임 로직에서 “3초 후 폭발”, “0.5초마다 재생”, “쿨다운 10초” 같은 시간 기반 동작은 매우 흔합니다. 언리얼 엔진은 이를 위한 여러 도구를 제공합니다.
| 도구 | 특징 | 사용 상황 |
|---|---|---|
FTimerHandle + FTimerManager | 엔진 통합 타이머, 정지·재개·취소 가능 | 쿨다운, 딜레이, 주기적 실행 |
Tick + 누적 시간 | 매 프레임 호출, 세밀한 제어 | 매 프레임 필요한 연속 처리 |
| Latent Action | Blueprint와 동일한 Delay 노드 C++ 구현 | C++ Utility 함수의 딜레이 |
AsyncTask | 게임 스레드 외부에서 무거운 작업 실행 | 파일 IO, 네트워크, 연산 집약 작업 |
1. FTimerHandle과 FTimerManager
Section titled “1. FTimerHandle과 FTimerManager”1.1 기본 구조
Section titled “1.1 기본 구조”FTimerHandle은 생성된 타이머를 식별하는 핸들입니다. 직접 타이머 로직을 담지 않으며, 이후 타이머 취소나 상태 조회 시 이 핸들을 인자로 전달합니다.
FTimerManager는 월드에 속한 타이머 관리자입니다. GetWorldTimerManager()로 접근합니다.
// 타이머 핸들 — 헤더에 멤버로 선언UPROPERTY() // UObject가 아니지만 직렬화/GC와 무관하므로 UPROPERTY 없어도 됨FTimerHandle CooldownTimerHandle;
// 타이머 관리자 접근FTimerManager& TimerManager = GetWorldTimerManager();
FTimerHandle은 UObject가 아닌 일반 구조체이므로UPROPERTY가 필수는 아닙니다. 단, 명확성을 위해 멤버로 선언하는 것이 일반적입니다.
1.2 SetTimer — 타이머 시작
Section titled “1.2 SetTimer — 타이머 시작”// 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);1.3 ClearTimer — 타이머 취소
Section titled “1.3 ClearTimer — 타이머 취소”// 특정 타이머 취소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);}1.5 타이머 상태 조회
Section titled “1.5 타이머 상태 조회”// 타이머 활성 여부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"));}2.2 반복 타이머 — 주기적 실행
Section titled “2.2 반복 타이머 — 주기적 실행”// 문: 화상 상태이상 — 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에서 구현할 수 있게 합니다.
#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 );};#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를 사용하면 작업을 백그라운드 스레드로 분리할 수 있습니다.
4.1 AsyncTask 기본 사용법
Section titled “4.1 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); }); });}4.2 ParallelFor — 병렬 반복문
Section titled “4.2 ParallelFor — 병렬 반복문”#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);}4.3 스레드 안전 주의사항
Section titled “4.3 스레드 안전 주의사항”// 스레드 안전한 작업 (백그라운드 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); } }); });}5. 실전 — 쿨다운 시스템 구현
Section titled “5. 실전 — 쿨다운 시스템 구현”5.1 UCooldownComponent 전체 구현
Section titled “5.1 UCooldownComponent 전체 구현”#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);};#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);}5.2 쿨다운 컴포넌트 사용 예시
Section titled “5.2 쿨다운 컴포넌트 사용 예시”// 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 갱신, 사운드 재생 등}| 도구 | 핵심 함수 | 사용 상황 |
|---|---|---|
FTimerHandle | SetTimer, ClearTimer, PauseTimer | 딜레이, 반복 실행, 쿨다운 |
| 상태 조회 | IsTimerActive, GetTimerRemaining | UI 진행률, 조건부 실행 |
| 람다 타이머 | SetTimer([this](){...}) | 간단한 일회성 콜백 |
| Latent Action | FPendingLatentAction 상속 | Blueprint Delay 노드 C++ 구현 |
AsyncTask | ENamedThreads::AnyBackgroundThreadNormalTask | 무거운 연산 백그라운드 처리 |
ParallelFor | ParallelFor(N, Lambda) | 대량 데이터 병렬 처리 |
EndPlay에서 모든FTimerHandle을ClearTimer로 정리합니다.- 반복 타이머가 불필요해지면 즉시
ClearTimer를 호출합니다. - 백그라운드 스레드에서 UObject에 직접 접근하지 않습니다.
- 백그라운드 결과를 UI/게임로직에 반영할 때는 반드시 게임 스레드로 되돌아와야 합니다.