Physics & Automation Testing

Hi, I’m having a problem with Automation Testing.

I want to test accelerating a pawn (AJet), utilizing AddForce to the pawn Root Component (a StaticMesh Component).

I enabled physics on my pawn’s component, enabled gravity and assigned a mesh to it.
For the test I created a UWorld so that the pawn spawns there. But strangely, the world doesn’t initiate the Physics Engine, because the test fails.

I digged a little and it seems that a UWorld sets it’s bShouldSimulatePhysics as false when calling CreateWorld, but a Physics Scene is created anyway.

So I thought that maybe, changing the bShouldSimulatePhysics to true, calling SetupPhysicsTickFunctions and StartPhysicsSim would trigger the Physics Engine, but it doesn’t.

The funny thing is that when playing in editor, everything works, but it doesn’t in the tests.
What’s worse is that not even gravity works in the test, because even the actor Z axis should be modified after ticking a little.

Does anyone know how could I start the physics engine this way? What am I missing?

Maybe I’m approaching the test in the wrong way and there’s a simpler way to test adding a force to a pawn. If you know, please let me know.

This is the test:



IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAJetShouldMoveWhenAccelerationAddedTest, "Project.Unit.JetTests.ShouldMoveWhenAccelerationAdded", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FAJetShouldMoveWhenAccelerationAddedTest::RunTest(const FString& Parameters)
{
{
UWorld* testWorld = UWorld::CreateWorld(EWorldType::Game, true);

testWorld->bShouldSimulatePhysics = true;

testWorld->SetupPhysicsTickFunctions(0.01);

testWorld->StartPhysicsSim();

AJet* testJet = testWorld->SpawnActor<AJet>(AJet::StaticClass());

FVector forceToApply {10000, 0, 0};

FVector currentLocation = testJet->GetActorLocation();


testJet->addAcceleration(forceToApply);

testJet->Tick(1.0f);

FVector movedLocation = testJet->GetActorLocation();



TestFalse(TEXT("The Jet location should change after an acceleration is added (after ticking)."), movedLocation.Equals(currentLocation));
}

return true;
}


The Jet.h file:



/// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Jet.generated.h"

UCLASS()
class PROJECTR_API AJet : public APawn
{
GENERATED_BODY()

public:

// Sets default values for this pawn's properties
AJet();

protected:

// Called when the game starts or when spawned
virtual void BeginPlay() override;

UPROPERTY(VisibleAnywhere, Category = "Components")
UStaticMeshComponent* meshComponent;

public:

// Called every frame
virtual void Tick(float DeltaTime) override;

// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

void addAcceleration(FVector forceToApply);
};



The Jet.cpp file:



// Fill out your copyright notice in the Description page of Project Settings.


#include "Jet.h"

#include "Components/StaticMeshComponent.h"

// Sets default values
AJet::AJet()
{
// Set this pawn to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;

meshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh Component"));
RootComponent = meshComponent;

meshComponent->SetSimulatePhysics(true);
meshComponent->SetEnableGravity(true);
meshComponent->SetCanEverAffectNavigation(false);

UStaticMesh* Mesh = Cast<UStaticMesh>(StaticLoadObject(UStaticMesh::StaticClass(), NULL, TEXT("/Engine/EditorMeshes/EditorCube")));
meshComponent->SetStaticMesh(Mesh);

}

// Called when the game starts or when spawned
void AJet::BeginPlay()
{
Super::BeginPlay();

}

// Called every frame
void AJet::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void AJet::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);

}

void AJet::addAcceleration(FVector forceToApply)
{
meshComponent->AddForce(forceToApply,NAME_None, true);
}




UPDATE:

I had a fundamental understanding issue with how Unreal works.
I was creating a map, spawning an object inside it, but I wasn’t simulating anything!

I modified the test to load a previously created map into the editor. Then, it starts a PIE session and adds the pawn (AJet) into the current PIE world level.

Here’s the updated test:



#include "Tests/AutomationEditorCommon.h"
#include "Editor.h"


IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAJetShouldMoveWhenAccelerationAddedTest, "Project.Unit.JetTests.ShouldMoveWhenAccelerationAdded", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FAJetShouldMoveWhenAccelerationAddedTest::RunTest(const FString& Parameters)
{
    {
        FString testWorldName = FString("/Game/Tests/TestMaps/VoidWorld");
        
        ADD_LATENT_AUTOMATION_COMMAND(FEditorLoadMap(testWorldName))
        
        ADD_LATENT_AUTOMATION_COMMAND(FStartPIECommand(true));
        ADD_LATENT_AUTOMATION_COMMAND(FSpawningAJetMakeItAccelerateAndTestCommand(this));
        //ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand);
    }
    return true;
}





DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FSpawningAJetMakeItAccelerateAndTestCommand, FAutomationTestBase*, Test);

bool FSpawningAJetMakeItAccelerateAndTestCommand::Update()
{
    if(!GEditor->IsPlayingSessionInEditor())//if not, everything would be made while the map is loading and the PIE is in progress of starting.
    {
        return false;
    }
    
    UWorld* testWorld = GEditor->GetPIEWorldContext()->World();

    //UE_LOG(LogTemp, Log, TEXT("Finished loading and starting testWorld."));
    //GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, TEXT("Finished loading and starting testWorld."));

    AJet* testJet = Cast<AJet, AActor>(GEditor->AddActor(testWorld->GetCurrentLevel(), AJet::StaticClass(),FTransform(FQuat(0,0,0,0))));

    //UE_LOG(LogTemp, Log, TEXT("testJet spawning transform: %s"), *testJet->GetActorTransform().ToString());
    //GEngine->AddOnScreenDebugMessage(-1, 50.f, FColor::Red, FString::Printf(TEXT("testJet spawning transform: %s"),*testJet->GetActorTransform().ToString()) );
    // if(testJet)
    // {
    //  UE_LOG(LogTemp, Log, TEXT("testJet Spawned into world."));
    //  GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, TEXT("testJet Spawned into world."));
    // }

    testWorld->InitializeActorsForPlay(testWorld->URL);
    testWorld->BeginPlay();

    // if(testJet->IsActorInitialized() && testJet->IsActorBeginningPlay())
    // {
    //  UE_LOG(LogTemp, Log, TEXT("testJet is initialized."));
    //  GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::White, TEXT("testJet is initialized."));
    // }
    
    
    testWorld->bDebugPauseExecution = true;
    testJet->SetActorLocation(FVector(0,0,0));
    testJet->SetActorRotation(testJet->GetGravityDirection().Rotation());
    FVector currentLocation = testJet->GetActorLocation();

    // UE_LOG(LogTemp, Log, TEXT("testJet current location: %s"),*currentLocation.ToString());
    // GEngine->AddOnScreenDebugMessage(-1, 50.f, FColor::Red, FString::Printf(TEXT("testJet current location: %s"),*currentLocation.ToString()) );
    // UE_LOG(LogTemp, Log, TEXT("Current World gravity: %f"),testWorld->GetGravityZ());
    // GEngine->AddOnScreenDebugMessage(-1, 50.f, FColor::Red, FString::Printf(TEXT("Current World gravity: %f"), testWorld->GetGravityZ()) );

    testWorld->bDebugPauseExecution = false;
    FVector forceToApply = FVector(10000);
    testJet->addAcceleration(forceToApply);

    testWorld->bDebugPauseExecution = true;
    
    FVector movedLocation = testJet->GetActorLocation();
    
    
    // UE_LOG(LogTemp, Log, TEXT("testJet moved location: %s"),*movedLocation.ToString() );
    // GEngine->AddOnScreenDebugMessage(-1, 50.f, FColor::Red, FString::Printf(TEXT("testJet moved location: %s"),*movedLocation.ToString()) );
    
    check(Test);
    //this should be changed to account for gravity or disable it inside the jet
    Test->TestFalse(TEXT("The Jet location should change after an acceleration is added (after ticking)."), movedLocation.Equals(currentLocation));
    return true;
}


Now, the problem I have is related with time (more related with frames than time):

My current problem is that everything works except that when I call “FVector movedLocation = testJet->GetActorLocation();” it registers the actor location, but before *“testJet->addAcceleration(forceToApply);” * finishes.
From what I read, AddForce is applied the next frame. So, I have a problem with time/frames.

I thought of two possible solutions:

-Add a timer (I think it could work, but I could be wasting frames or even worse, in a slower system it could behave different).

-Maybe declare a Tick method and add it to the world, after the Physics’s Tick (I think that this is better, but it’s possible that it should tick more than one frame, so maybe it should be combined with a latent command or something).

Are these approaches correct? Am I missing something?

PD:
Another thing is that loading a map and starting a PIE session takes like 10 seconds, which is a lot.

Is it possible to start the engine with fewer features? For example, I don’t need the sound mixer or the AI subsystem for this test.

To save even more time, I think it would be nice if I started a PIE session once and used it with various tests. This last thing I think I could achieve it with a simple test that starts the PIE session and executes complex tests that use the PIE session.

Thanks!

I solved it!!

I could finally do something to test this, it’s a caveman solution but it works and I know it could be refactored into something better.

So, the problem was that addAcceleration required AddForce to move the actor (which is applied on the next tick) and I was trying to test it in the same frame that it was called (which always made the test fail). I tried to use timers but I couldn’t make them work. I thought of using tick functions but I didn’t understand how to make my own very well.

Then I had an idea: latent commands are called every frame until they return true, so, what if I use one of them as a tick counter and check if there’s a change as a condition to then run the test.

Now I thought that yes, that could work if I know that sometime in the future I would be receiving a change. What if that change never came? Well, this is the caveman workaround: I count every time the latent command is called and I use a limit to stop calling it if the actor location didn’t change after a few frames. If that happens, the test fails and an error is displayed, telling that the tick limit was reached.

The above refers to this piece of code:




DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckAJetLocationCommand, int, tickCount, int, tickLimit, FAutomationTestBase*, test);
bool FCheckAJetLocationCommand::Update()
{
    if (GEditor->IsPlayingSessionInEditor())
    {
        UWorld* testWorld = GEditor->GetPIEWorldContext()->World();
        AJet* testJet = Cast<AJet, AActor>(UGameplayStatics::GetActorOfClass(testWorld, AJet::StaticClass()));
        if (testJet)
        {
            FVector currentLocation = testJet->GetActorLocation();

            if (currentLocation.X > 0)//it would be better to align the jet first and then check against it's forward vector. Be have to be careful of gravity in this test.
            {
                check(test);
                test->TestTrue(TEXT("The Jet X location should increase after an acceleration is added (after ticking)."), currentLocation.X > 0);
                return true;
            }
            else
            {
                ++tickCount;
                if ( tickCount > tickLimit)
                {
                    test->TestFalse(TEXT("Tick limit reached for this test. The Jet Location never changed from (0,0,0)."), tickCount > tickLimit);
                    return true;
                }
            }
        }
    }
    return false;
}



And it’s called from the main body of the test like this:



int tickCount = 0;
int tickLimit = 3;
ADD_LATENT_AUTOMATION_COMMAND(FCheckAJetLocationCommand(tickCount, tickLimit, this));


The full test is as follows:



#include "JetTest.h"

#include "Jet.h"

#include "Misc/AutomationTest.h"
#include "Tests/AutomationEditorCommon.h"
#include "Editor.h"
#include "Kismet/GameplayStatics.h"


#if WITH_DEV_AUTOMATION_TESTS

DEFINE_LATENT_AUTOMATION_COMMAND(FSpawningAJetMakeItAccelerateCommand);

bool FSpawningAJetMakeItAccelerateCommand::Update()
{
    if (!GEditor->IsPlayingSessionInEditor())//if not, everything would be made while the map is loading and the PIE is in progress.
    {
        return false;
    }
    UWorld* testWorld = GEditor->GetPIEWorldContext()->World();


    AJet* testJet = testWorld->SpawnActor<AJet>(AJet::StaticClass());

    FVector forceToApply = FVector(10000);
    testJet->addAcceleration(forceToApply);
    return true;
}

DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FCheckAJetLocationCommand, int, tickCount, int, tickLimit, FAutomationTestBase*, test);

bool FCheckAJetLocationCommand::Update()
{
    if (GEditor->IsPlayingSessionInEditor())
    {
        UWorld* testWorld = GEditor->GetPIEWorldContext()->World();
        AJet* testJet = Cast<AJet, AActor>(UGameplayStatics::GetActorOfClass(testWorld, AJet::StaticClass()));
        if (testJet)
        {
            FVector currentLocation = testJet->GetActorLocation();

            if (currentLocation.X > 0)//it would be better to align the ship first and then check against it's forward vector. Be have to be careful of gravity in this test.
            {
                check(test);
                test->TestTrue(TEXT("The Jet X location should increase after an acceleration is added (after ticking)."), currentLocation.X > 0);
                return true;
            }
 
             ++tickCount;
             if ( tickCount > tickLimit)
             {
                 test->TestFalse(TEXT("Tick limit reached for this test. The Jet Location never changed from (0,0,0)."), tickCount > tickLimit);
                 return true;
             }
        }
    }
    return false;
}



IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAJetShouldMoveWhenAccelerationAddedTest, "Project.Unit.JetTests.ShouldMoveWhenAccelerationAdded", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FAJetShouldMoveWhenAccelerationAddedTest::RunTest(const FString& Parameters)
{
    {
        FString testWorldName = FString("/Game/Tests/TestMaps/VoidWorld");
        ADD_LATENT_AUTOMATION_COMMAND(FEditorLoadMap(testWorldName))
        ADD_LATENT_AUTOMATION_COMMAND(FStartPIECommand(true));
        ADD_LATENT_AUTOMATION_COMMAND(FSpawningAJetMakeItAccelerateCommand);
        int tickCount = 0;
        int tickLimit =3;
        ADD_LATENT_AUTOMATION_COMMAND(FCheckAJetLocationCommand(tickCount, tickLimit, this));
        //ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand);
    }
    return true;
}

#endif //WITH_DEV_AUTOMATION_TESTS