PART 10 - 강의 2/6

Latent Command

비동기 테스트, 맵 로딩 테스트, 조건 대기

01

Latent Command란?

프레임 단위로 실행되는 비동기 테스트 명령

필요성

일반 테스트는 동기적으로 한 프레임 내에 완료되어야 합니다. 하지만 다음과 같은 작업은 여러 프레임에 걸쳐 진행됩니다:

맵 로딩

레벨 스트리밍, 에셋 로드

네트워크

서버 연결, 데이터 송수신

애니메이션

몽타주 재생, 상태 전이

AI

행동 트리 실행, 경로 탐색

Latent Command 작동 원리

Execution Flow // Latent Command는 매 프레임 Update()가 호출됨 // false 반환 = 다음 프레임에 다시 호출 // true 반환 = 완료, 다음 Command 실행 Frame 1: Command1.Update() -> false // 진행 중 Frame 2: Command1.Update() -> false // 진행 중 Frame 3: Command1.Update() -> true // 완료! Frame 4: Command2.Update() -> true // 즉시 완료 Frame 5: Command3.Update() -> ...
02

기본 제공 Latent Commands

언리얼 엔진이 제공하는 내장 Latent Commands

자주 사용하는 Latent Commands

Built-in Latent Commands #include "Misc/AutomationTest.h" bool FMyAsyncTest::RunTest(const FString& Parameters) { // 1. 대기 명령 - 지정된 시간 동안 대기 ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(2.0f)); // 2초 대기 // 2. 맵 로드 명령 ADD_LATENT_AUTOMATION_COMMAND(FLoadGameMapCommand(TEXT("/Game/Maps/TestMap"))); // 3. 프레임 대기 - 지정된 프레임 수만큼 대기 ADD_LATENT_AUTOMATION_COMMAND(FWaitForFramesCommand(60)); // 60프레임 대기 // 4. 조건 대기 - 조건이 참이 될 때까지 대기 ADD_LATENT_AUTOMATION_COMMAND(FWaitUntilCommand([this]() { // 조건 검사 return bSomeConditionMet; }, 10.0f)); // 타임아웃 10초 // 5. 콘솔 명령 실행 ADD_LATENT_AUTOMATION_COMMAND(FExecStringLatentCommand(TEXT("stat fps"))); return true; }

맵 로딩 테스트 예제

MapLoadingTest.cpp #if WITH_DEV_AUTOMATION_TESTS IMPLEMENT_SIMPLE_AUTOMATION_TEST( FMapLoadingTest, "MyGame.Maps.TestMapLoading", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter ) bool FMapLoadingTest::RunTest(const FString& Parameters) { // 1. 테스트 맵 로드 ADD_LATENT_AUTOMATION_COMMAND(FLoadGameMapCommand(TEXT("/Game/Maps/TestLevel"))); // 2. 맵 로딩 완료 대기 ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(1.0f)); // 3. 월드 검증 ADD_LATENT_AUTOMATION_COMMAND(FFunctionLatentCommand([this]() { UWorld* World = GEngine->GetWorldContexts()[0].World(); TestNotNull(TEXT("World should exist"), World); if (World) { // 특정 액터 검색 AActor* SpawnPoint = UGameplayStatics::GetActorOfClass( World, APlayerStart::StaticClass()); TestNotNull(TEXT("PlayerStart should exist"), SpawnPoint); } return true; })); // 4. 정리 - 기본 맵으로 복귀 ADD_LATENT_AUTOMATION_COMMAND(FLoadGameMapCommand(TEXT("/Game/Maps/EmptyLevel"))); return true; } #endif
03

커스텀 Latent Command 작성

프로젝트에 맞는 커스텀 비동기 테스트 명령

기본 구조

CustomLatentCommand.h // 파라미터 없는 Latent Command DEFINE_LATENT_AUTOMATION_COMMAND(FMySimpleLatentCommand); // 1개 파라미터 DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER( FMyLatentCommandWithParam, FString, TestParameter ); // 2개 파라미터 DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER( FMyLatentCommandTwoParams, FString, Param1, int32, Param2 ); // 테스트 참조 포함 (검증 함수 사용 위해) DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER( FVerifyActorLatentCommand, FAutomationTestBase*, Test );

커스텀 Latent Command 구현

WaitForActorSpawn.cpp /** * 특정 액터가 스폰될 때까지 대기하는 Latent Command */ DEFINE_LATENT_AUTOMATION_COMMAND_TWO_PARAMETER( FWaitForActorSpawnCommand, UClass*, ActorClass, float, TimeoutSeconds ); bool FWaitForActorSpawnCommand::Update() { // 첫 호출 시 시작 시간 기록 static float StartTime = FPlatformTime::Seconds(); static bool bInitialized = false; if (!bInitialized) { StartTime = FPlatformTime::Seconds(); bInitialized = true; } // 타임아웃 검사 if (FPlatformTime::Seconds() - StartTime > TimeoutSeconds) { UE_LOG(LogTemp, Warning, TEXT("WaitForActorSpawn timed out")); bInitialized = false; return true; // 타임아웃으로 종료 } // 월드에서 액터 검색 if (GEngine && GEngine->GetWorldContexts().Num() > 0) { UWorld* World = GEngine->GetWorldContexts()[0].World(); if (World) { AActor* FoundActor = UGameplayStatics::GetActorOfClass(World, ActorClass); if (FoundActor) { UE_LOG(LogTemp, Log, TEXT("Actor found: %s"), *FoundActor->GetName()); bInitialized = false; return true; // 성공으로 완료 } } } return false; // 아직 찾지 못함, 다음 프레임에 다시 시도 }

복잡한 시퀀스 테스트

CombatSequenceTest.cpp // 전투 시퀀스를 테스트하는 Latent Command DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER( FCombatSequenceTestCommand, FAutomationTestBase*, Test ); bool FCombatSequenceTestCommand::Update() { static int32 TestPhase = 0; static float PhaseStartTime = 0; if (TestPhase == 0) { PhaseStartTime = FPlatformTime::Seconds(); TestPhase = 1; } float ElapsedTime = FPlatformTime::Seconds() - PhaseStartTime; UWorld* World = GEngine->GetWorldContexts()[0].World(); if (!World) return true; switch (TestPhase) { case 1: // 플레이어 찾기 { ACharacter* Player = UGameplayStatics::GetPlayerCharacter(World, 0); if (Player) { Test->TestNotNull(TEXT("Player should exist"), Player); TestPhase = 2; PhaseStartTime = FPlatformTime::Seconds(); } else if (ElapsedTime > 5.0f) { Test->AddError(TEXT("Player not found within timeout")); TestPhase = 0; return true; } break; } case 2: // 적 스폰 대기 { TArray<AActor*> Enemies; UGameplayStatics::GetAllActorsOfClass(World, AEnemyCharacter::StaticClass(), Enemies); if (Enemies.Num() > 0) { Test->TestTrue(TEXT("Enemy should exist"), Enemies.Num() > 0); TestPhase = 3; PhaseStartTime = FPlatformTime::Seconds(); } else if (ElapsedTime > 10.0f) { Test->AddError(TEXT("Enemy not spawned within timeout")); TestPhase = 0; return true; } break; } case 3: // 전투 완료 대기 { // 전투 완료 조건 검사 if (ElapsedTime > 3.0f) { Test->TestTrue(TEXT("Combat sequence completed"), true); TestPhase = 0; // 리셋 return true; // 테스트 완료 } break; } } return false; // 계속 진행 }
04

Spec 테스트에서 비동기 처리

LatentIt을 사용한 Spec 기반 비동기 테스트

LatentIt 사용

AsyncSpec.cpp BEGIN_DEFINE_SPEC( FAsyncOperationSpec, "MyGame.Async", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask ) TSharedPtr<FMyAsyncLoader> Loader; bool bLoadComplete; END_DEFINE_SPEC(FAsyncOperationSpec) void FAsyncOperationSpec::Define() { Describe("Async asset loading", [this]() { BeforeEach([this]() { Loader = MakeShared<FMyAsyncLoader>(); bLoadComplete = false; }); // LatentIt - 비동기 테스트 LatentIt("should load asset asynchronously", EAsyncExecution::ThreadPool, [this](const FDoneDelegate& Done) { // 비동기 로드 시작 Loader->LoadAssetAsync( TEXT("/Game/Items/Sword"), FOnAssetLoaded::CreateLambda([this, Done](UObject* Asset) { TestNotNull(TEXT("Asset should be loaded"), Asset); bLoadComplete = true; Done.Execute(); // 테스트 완료 신호 }) ); }); LatentIt("should timeout if asset not found", FTimespan::FromSeconds(5), // 타임아웃 [this](const FDoneDelegate& Done) { Loader->LoadAssetAsync( TEXT("/Game/NonExistent"), FOnAssetLoaded::CreateLambda([this, Done](UObject* Asset) { TestNull(TEXT("Asset should be null"), Asset); Done.Execute(); }) ); }); }); Describe("Network operations", [this]() { LatentIt("should connect to server", FTimespan::FromSeconds(10), [this](const FDoneDelegate& Done) { // 서버 연결 시뮬레이션 FMyNetworkManager::Get().ConnectAsync( TEXT("127.0.0.1:7777"), FOnConnected::CreateLambda([this, Done](bool bSuccess) { TestTrue(TEXT("Connection should succeed"), bSuccess); Done.Execute(); }) ); }); }); }

LatentBeforeEach / LatentAfterEach

LatentSetup.cpp void FMapTestSpec::Define() { Describe("Level testing", [this]() { // 비동기 설정 LatentBeforeEach(FTimespan::FromSeconds(30), [this](const FDoneDelegate& Done) { // 맵 로드 FLatentActionInfo LatentInfo; LatentInfo.CallbackTarget = this; LatentInfo.Linkage = 0; UGameplayStatics::OpenLevel( GEngine->GetWorldContexts()[0].World(), TEXT("/Game/Maps/TestLevel")); // 맵 로드 완료 대기 FTSTicker::GetCoreTicker().AddTicker( FTickerDelegate::CreateLambda([Done](float DeltaTime) { if (GEngine->GetWorldContexts()[0].World()->HasBegunPlay()) { Done.Execute(); return false; // 티커 제거 } return true; // 계속 체크 })); }); // 비동기 정리 LatentAfterEach(FTimespan::FromSeconds(10), [this](const FDoneDelegate& Done) { UGameplayStatics::OpenLevel( GEngine->GetWorldContexts()[0].World(), TEXT("/Game/Maps/EmptyLevel")); // 정리 완료 대기 FPlatformProcess::Sleep(0.5f); Done.Execute(); }); It("should have player start", [this]() { UWorld* World = GEngine->GetWorldContexts()[0].World(); AActor* PlayerStart = UGameplayStatics::GetActorOfClass( World, APlayerStart::StaticClass()); TestNotNull(TEXT("PlayerStart"), PlayerStart); }); }); }
05

실전 테스트 예제

게임플레이 시나리오 전체 테스트

게임플레이 시퀀스 테스트

GameplaySequenceTest.cpp #if WITH_DEV_AUTOMATION_TESTS IMPLEMENT_SIMPLE_AUTOMATION_TEST( FGameplaySequenceTest, "MyGame.Gameplay.FullSequence", EAutomationTestFlags::ClientContext | EAutomationTestFlags::ProductFilter ) bool FGameplaySequenceTest::RunTest(const FString& Parameters) { // 1. 테스트 맵 로드 ADD_LATENT_AUTOMATION_COMMAND(FLoadGameMapCommand( TEXT("/Game/Maps/TestArena"))); ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(2.0f)); // 2. 플레이어 스폰 확인 ADD_LATENT_AUTOMATION_COMMAND(FWaitForActorSpawnCommand( AMyPlayerCharacter::StaticClass(), 5.0f)); // 3. 인벤토리에 아이템 추가 ADD_LATENT_AUTOMATION_COMMAND(FFunctionLatentCommand([this]() { UWorld* World = GEngine->GetWorldContexts()[0].World(); if (AMyPlayerCharacter* Player = Cast<AMyPlayerCharacter>( UGameplayStatics::GetPlayerCharacter(World, 0))) { if (UInventoryComponent* Inventory = Player->GetInventory()) { FItemData TestSword; TestSword.ItemId = FName(TEXT("TestSword")); Inventory->AddItem(TestSword); TestEqual(TEXT("Item added"), Inventory->GetItemCount(), 1); } } return true; })); // 4. 장비 장착 ADD_LATENT_AUTOMATION_COMMAND(FFunctionLatentCommand([this]() { UWorld* World = GEngine->GetWorldContexts()[0].World(); if (AMyPlayerCharacter* Player = Cast<AMyPlayerCharacter>( UGameplayStatics::GetPlayerCharacter(World, 0))) { Player->EquipItem(FName(TEXT("TestSword"))); } return true; })); ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(0.5f)); // 5. 장비 확인 ADD_LATENT_AUTOMATION_COMMAND(FFunctionLatentCommand([this]() { UWorld* World = GEngine->GetWorldContexts()[0].World(); if (AMyPlayerCharacter* Player = Cast<AMyPlayerCharacter>( UGameplayStatics::GetPlayerCharacter(World, 0))) { bool bEquipped = Player->IsItemEquipped(FName(TEXT("TestSword"))); TestTrue(TEXT("Sword should be equipped"), bEquipped); } return true; })); // 6. 정리 ADD_LATENT_AUTOMATION_COMMAND(FLoadGameMapCommand( TEXT("/Game/Maps/EmptyLevel"))); return true; } #endif
Latent Command 팁
  • 항상 타임아웃을 설정하여 무한 대기 방지
  • static 변수 사용 시 테스트 간 초기화 주의
  • 복잡한 시퀀스는 상태 머신 패턴으로 구현
  • FFunctionLatentCommand로 간단한 람다 테스트 가능
SUMMARY

핵심 요약

  • Latent Command는 매 프레임 Update()가 호출되며, true 반환 시 완료
  • FWaitLatentCommand, FLoadGameMapCommand 등 기본 제공 명령 활용
  • DEFINE_LATENT_AUTOMATION_COMMAND로 커스텀 비동기 테스트 명령 작성
  • Spec 테스트에서는 LatentIt, LatentBeforeEach 사용
  • 타임아웃과 상태 초기화에 주의하여 안정적인 테스트 작성
다음 강의 예고

다음 강의에서는 커스텀 로그 카테고리와 UE_LOG 레벨별 사용법을 학습합니다.

PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: Latent Automation Command 작성

FLatentAutomationCommand를 상속하여 시간이 걸리는 RPG 테스트(세이브/로드 후 데이터 검증, 비동기 에셋 로드 완료 대기)를 작성하세요. Update()에서 완료 조건을 체크하세요.

실습 2: 게임플레이 시뮬레이션 테스트

Latent Command로 RPG 전투 시퀀스를 자동 테스트하세요. 캐릭터 스폰 -> 적 접근 -> 공격 -> 데미지 검증 -> 사망 확인의 전체 플로우를 자동화하세요.

심화 과제: 네트워크 Latent 테스트

멀티플레이어 환경에서 Latent Command를 사용하여 서버-클라이언트 간 동기화를 테스트하세요. 서버에서 아이템 생성 후 클라이언트에서 수신되는 것을 비동기적으로 검증하세요.