481 lines
18 KiB
C++
481 lines
18 KiB
C++
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
|
|
|
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
|
|
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
|
|
#include "Tests/DirectiveUtilTestObject.h"
|
|
#include "Engine/World.h"
|
|
#include "Engine/Engine.h"
|
|
#include "Engine/EngineBaseTypes.h"
|
|
#include "Engine/EngineTypes.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Engine/StaticMeshActor.h"
|
|
#include "Components/StaticMeshComponent.h"
|
|
#include "GameFramework/DefaultPawn.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "TimerManager.h"
|
|
#include "Misc/AutomationTest.h"
|
|
|
|
namespace DirectiveUtilAsyncTraceTestHelpers
|
|
{
|
|
UWorld* CreateTraceWorld()
|
|
{
|
|
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
|
if (!World)
|
|
{
|
|
return nullptr;
|
|
}
|
|
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
|
WorldContext.SetCurrentWorld(World);
|
|
World->InitializeActorsForPlay(FURL());
|
|
World->BeginPlay();
|
|
return World;
|
|
}
|
|
|
|
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
|
|
{
|
|
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
|
|
UStaticMeshComponent* Component = Cube->GetStaticMeshComponent();
|
|
Component->SetMobility(EComponentMobility::Movable);
|
|
Component->SetStaticMesh(CubeMesh);
|
|
Component->SetCollisionProfileName(TEXT("BlockAll"));
|
|
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
return Cube;
|
|
}
|
|
}
|
|
|
|
bool UDirectiveUtilTestMoveToLocationTask::HasRegisteredTimers() const
|
|
{
|
|
const UWorld* World = TimerWorld.Get();
|
|
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
|
|
}
|
|
|
|
void UDirectiveUtilTestMoveToLocationTask::RegisterTimersForTest(UWorld* World)
|
|
{
|
|
TimerWorld = World;
|
|
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
|
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
|
}
|
|
|
|
bool UDirectiveUtilTestMoveToActorTask::HasRegisteredTimers() const
|
|
{
|
|
const UWorld* World = TimerWorld.Get();
|
|
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
|
|
}
|
|
|
|
void UDirectiveUtilTestMoveToActorTask::RegisterTimersForTest(UWorld* World)
|
|
{
|
|
TimerWorld = World;
|
|
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
|
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
|
}
|
|
|
|
class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand
|
|
{
|
|
public:
|
|
FDirectiveUtilTickTraceWorld(FAutomationTestBase* InTest, UWorld* InWorld, const TArray<UDirectiveUtilDelegateListener*>& InListeners, int32 InFrames)
|
|
: Test(InTest)
|
|
, World(InWorld)
|
|
, Listeners(InListeners)
|
|
, FramesRemaining(InFrames)
|
|
{
|
|
}
|
|
|
|
virtual bool Update() override
|
|
{
|
|
if (UWorld* TickWorld = World.Get())
|
|
{
|
|
TickWorld->Tick(LEVELTICK_All, 0.05f);
|
|
}
|
|
|
|
bool bAllComplete = true;
|
|
for (const UDirectiveUtilDelegateListener* Listener : Listeners)
|
|
{
|
|
if (Listener && !Listener->bCompleted)
|
|
{
|
|
bAllComplete = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bAllComplete || --FramesRemaining <= 0)
|
|
{
|
|
for (UDirectiveUtilDelegateListener* Listener : Listeners)
|
|
{
|
|
if (!Listener)
|
|
{
|
|
continue;
|
|
}
|
|
Test->TestTrue(TEXT("Async trace broadcasts Completed"), Listener->bCompleted);
|
|
Test->TestTrue(TEXT("Async trace through a blocking cube reports a hit"), Listener->HitCount > 0);
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
if (UWorld* TearDownWorld = World.Get())
|
|
{
|
|
GEngine->DestroyWorldContext(TearDownWorld);
|
|
TearDownWorld->DestroyWorld(false);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
FAutomationTestBase* Test;
|
|
TWeakObjectPtr<UWorld> World;
|
|
TArray<UDirectiveUtilDelegateListener*> Listeners;
|
|
int32 FramesRemaining;
|
|
};
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
|
|
|
bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
|
{
|
|
AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
|
|
{
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_AsyncTrace* Task = UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(nullptr, FVector::ZeroVector, FVector(0, 0, 100), ETraceTypeQuery::TraceTypeQuery1, false);
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("Null world trace still broadcasts Completed", Listener->bCompleted);
|
|
TestEqual("Null world trace reports no hits", Listener->HitCount, 0);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
|
if (!CubeMesh)
|
|
{
|
|
AddError(TEXT("Engine cube mesh unavailable for async trace hit scenarios."));
|
|
return false;
|
|
}
|
|
|
|
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
|
if (!World)
|
|
{
|
|
AddError(TEXT("Failed to create a transient game world for the async trace test."));
|
|
return false;
|
|
}
|
|
DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh);
|
|
|
|
const FVector Start(0.0f, 0.0f, 500.0f);
|
|
const FVector End(0.0f, 0.0f, -500.0f);
|
|
const ETraceTypeQuery VisibilityChannel = ETraceTypeQuery::TraceTypeQuery1;
|
|
|
|
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
|
|
{
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted);
|
|
Task->Activate();
|
|
return Listener;
|
|
};
|
|
|
|
TArray<UDirectiveUtilDelegateListener*> Listeners;
|
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, VisibilityChannel, false)));
|
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, VisibilityChannel, false)));
|
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, VisibilityChannel, false)));
|
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, VisibilityChannel, false)));
|
|
|
|
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
|
|
|
|
return true;
|
|
}
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
|
|
|
bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
|
{
|
|
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
|
|
{
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(nullptr, nullptr, FVector(100.0f, 0.0f, 0.0f));
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("Null controller broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("Null controller reports failure", Listener->bLastSuccess);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
|
if (!World)
|
|
{
|
|
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
|
|
return false;
|
|
}
|
|
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
|
WorldContext.SetCurrentWorld(World);
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("Controller without a pawn broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("Controller without a pawn reports failure", Listener->bLastSuccess);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->EndTask();
|
|
|
|
TestTrue("EndTask broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("EndTask reports failure", Listener->bLastSuccess);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->EndTask();
|
|
Task->EndTask();
|
|
|
|
TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
|
|
if (Controller && Pawn)
|
|
{
|
|
Controller->SetPawn(Pawn);
|
|
UDirectiveUtilTestMoveToLocationTask* Task = NewObject<UDirectiveUtilTestMoveToLocationTask>();
|
|
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
|
|
Task->RegisterTimersForTest(World);
|
|
TestTrue("Move to location registers both lifecycle timers", Task->HasRegisteredTimers());
|
|
Task->ClearController();
|
|
Task->Complete();
|
|
TestFalse("Move to location clears timers without a controller", Task->HasRegisteredTimers());
|
|
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
|
|
Task->Activate();
|
|
TestFalse("A completed move to location should not restart", Task->HasRegisteredTimers());
|
|
}
|
|
}
|
|
|
|
GEngine->DestroyWorldContext(World);
|
|
World->DestroyWorld(false);
|
|
|
|
{
|
|
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
|
if (!MoveWorld)
|
|
{
|
|
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
|
return false;
|
|
}
|
|
|
|
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
|
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
|
if (!Controller || !Pawn)
|
|
{
|
|
AddError(TEXT("Failed to spawn a controller or pawn for the no-navigation move scenario."));
|
|
GEngine->DestroyWorldContext(MoveWorld);
|
|
MoveWorld->DestroyWorld(false);
|
|
return false;
|
|
}
|
|
Controller->SetPawn(Pawn);
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(MoveWorld, Controller, FVector(10000.0f, 0.0f, 0.0f), 100.0f, false);
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("A location move without navigation broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("A location move without navigation reports failure", Listener->bLastSuccess);
|
|
TestEqual("A location move without navigation completes exactly once", Listener->CompletedCount, 1);
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
GEngine->DestroyWorldContext(MoveWorld);
|
|
MoveWorld->DestroyWorld(false);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
|
|
|
bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
|
{
|
|
AddExpectedMessagePlain(TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
|
|
{
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(nullptr, nullptr, nullptr);
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("Null controller broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("Null controller reports failure", Listener->bLastSuccess);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
|
if (!World)
|
|
{
|
|
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
|
|
return false;
|
|
}
|
|
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
|
WorldContext.SetCurrentWorld(World);
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
|
if (Controller && Pawn)
|
|
{
|
|
Controller->SetPawn(Pawn);
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr);
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("Null goal broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("Null goal reports failure", Listener->bLastSuccess);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
else
|
|
{
|
|
AddError(TEXT("Failed to spawn a controller or pawn for the null-goal scenario."));
|
|
}
|
|
}
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr);
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->EndTask();
|
|
Task->EndTask();
|
|
|
|
TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1);
|
|
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
}
|
|
|
|
{
|
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
|
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
|
|
AStaticMeshActor* Goal = World->SpawnActor<AStaticMeshActor>();
|
|
if (Controller && Pawn && Goal)
|
|
{
|
|
Controller->SetPawn(Pawn);
|
|
UDirectiveUtilTestMoveToActorTask* Task = NewObject<UDirectiveUtilTestMoveToActorTask>();
|
|
Task->Configure(Controller, Goal, true);
|
|
Task->RegisterTimersForTest(World);
|
|
TestTrue("Move to actor registers both lifecycle timers", Task->HasRegisteredTimers());
|
|
Task->ClearController();
|
|
Task->Complete();
|
|
TestFalse("Move to actor clears timers without a controller", Task->HasRegisteredTimers());
|
|
Task->Configure(Controller, Goal, true);
|
|
Task->Activate();
|
|
TestFalse("A completed move to actor should not restart", Task->HasRegisteredTimers());
|
|
}
|
|
}
|
|
|
|
GEngine->DestroyWorldContext(World);
|
|
World->DestroyWorld(false);
|
|
|
|
{
|
|
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
|
if (!MoveWorld)
|
|
{
|
|
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
|
return false;
|
|
}
|
|
|
|
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
|
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
|
AStaticMeshActor* GoalActor = MoveWorld->SpawnActor<AStaticMeshActor>(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator);
|
|
if (!Controller || !Pawn || !GoalActor)
|
|
{
|
|
AddError(TEXT("Failed to spawn a controller, pawn, or goal for the no-navigation move scenario."));
|
|
GEngine->DestroyWorldContext(MoveWorld);
|
|
MoveWorld->DestroyWorld(false);
|
|
return false;
|
|
}
|
|
Controller->SetPawn(Pawn);
|
|
|
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
|
Listener->AddToRoot();
|
|
|
|
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(MoveWorld, Controller, GoalActor, 100.0f, false);
|
|
Listener->Keepalive = Task;
|
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
|
Task->Activate();
|
|
|
|
TestTrue("An actor move without navigation broadcasts Completed", Listener->bCompleted);
|
|
TestFalse("An actor move without navigation reports failure", Listener->bLastSuccess);
|
|
TestEqual("An actor move without navigation completes exactly once", Listener->CompletedCount, 1);
|
|
Listener->Keepalive = nullptr;
|
|
Listener->RemoveFromRoot();
|
|
GEngine->DestroyWorldContext(MoveWorld);
|
|
MoveWorld->DestroyWorld(false);
|
|
}
|
|
|
|
return true;
|
|
}
|