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를 사용하여 서버-클라이언트 간 동기화를 테스트하세요. 서버에서 아이템 생성 후 클라이언트에서 수신되는 것을 비동기적으로 검증하세요.