Adding Force to Pawn - Automation Testing

Hi, I’m having a problem with physics inside unit tests.

What I wanted to do is addForce to a Pawn via a method and testing it via unit tests. The pawn is set to simulate physics and it works when paying in editor, but it doesn’t change location when testing it via automation testing.

Here’s what I tried without success:

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

bool FAJetShouldMoveWhenAccelerationAddedTest::RunTest(const FString& Parameters)
{
	{
		UWorld* testWorld = FPhysicsTestHelpers::GetWorld();

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

		FVector forceToApply {10000, 0, 0};
		FVector currentLocation = testJet->GetActorLocation();
		
		testJet->addAcceleration(forceToApply);

		testJet->Tick(0.1f);

		FVector movedLocation = testJet->GetActorLocation();

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

	return true;
}

And the pawn constructor and method:

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);
}

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

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.

(if you want to see the code, it’s also here: Physics & Automation Testing - C++ Programming - Unreal Engine Forums )

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 this test, I don’t need the sound mixer or the AI subsystem for example.

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.

##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 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;
			}
			else
			{
				*tickCount = *tickCount + 1;

				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;
}

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_ONE_PARAMETER(FSpawningAJetMakeItAccelerateCommand, FAutomationTestBase*, test);

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;
			}
			else
			{
				*tickCount = *tickCount + 1;

				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, "ProjectR.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(this));
		int* tickCount = new int{0};
		int* tickLimit = new int{3};
		ADD_LATENT_AUTOMATION_COMMAND(FCheckAJetLocationCommand(tickCount, tickLimit, this));

		//ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand);
	}

	return true;
}

#endif //WITH_DEV_AUTOMATION_TESTS

Actually, there’s no need to use pointers with tickCount and tickLimit. When defining a Latent Command, you create a class and the parameters passed are used as private members of that class.

https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1814298-pass-by-reference-to-latent-command-odd-behaviour-automation-testing