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!