Issues with a racing game prototype

Hello guys!

Last week I started a racing game prototype. But I’m having some issues to keep the AI agent over the track. Here is the video: Game Prototype - YouTube

And now the code:

NPCCarPawn.h



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

#pragma once

#include "CoreMinimal.h"
#include "PRJ_ProgrammingTestPawn.h"
#include "NPCCarPawn.generated.h"

/**
*
*/
class ASplineActor;
class USphereComponent;
class USpringArmComponent;

UCLASS()
class PRJ_PROGRAMMINGTEST_API ANPCCarPawn : public APRJ_ProgrammingTestPawn
{ GENERATED_BODY()

UPROPERTY()
float AngleBetween;

UPROPERTY()
bool bCanMove;

UPROPERTY(EditInstanceOnly, Category = "Vehicle Properties", meta = (AllowPrivateAccess = "true"))
bool bIsDirectionReversed = false;

UPROPERTY()
ASplineActor* CurrentTrack = nullptr;

UPROPERTY()
FTimerHandle DebugLineHandle;

UPROPERTY(EditInstanceOnly, Category = "Vehicle Properties", meta = (AllowPrivateAccess = "true"))
float DistancePerLiter;

UPROPERTY()
float DistanceTravelled;

UPROPERTY(EditInstanceOnly, Category = "Vehicle Properties", meta = (AllowPrivateAccess = "true"))
float GasCapacity;

UPROPERTY()
USpringArmComponent* TrackCircuitProbeArm;

UPROPERTY(EditInstanceOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* TrackCircuitProbe;

UPROPERTY(EditInstanceOnly, Category = "Vehicle Properties", meta = (AllowPrivateAccess = "true"))
float QuantityGasConsumed;

UPROPERTY(EditInstanceOnly, Category = "Spline", meta = (AllowPrivateAccess = "true"))
TArray<ASplineActor*> TrackCircuitArr;

void ConsumeGas();

UFUNCTION()
void DrawLine();
 
  protected:
  void BeginPlay() override;

void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
 
  public:
  ANPCCarPawn(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

void AccumulateDistanceTravelled(float Distance);

float CalculateAngleBetween();

bool CanMove() const { return bCanMove; };

ASplineActor* GetTrackCircuit() const { return CurrentTrack; };

bool IsTravellingReverseMode() const { return bIsDirectionReversed; };

void SetTrackCircuit(uint8 TrackCircuitIndex) { CurrentTrack = TrackCircuitArr[TrackCircuitIndex]; };

float GetGasCapacity() const { return GasCapacity; };
 
};



NPCCarPawn.cpp



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


#include "NPCCarPawn.h"
#include "WheeledVehicleMovementComponent.h"
#include "Components/SplineComponent.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Kismet/KismetMathLibrary.h"
#include "SplineActor.h"
#include "DrawDebugHelpers.h"

ANPCCarPawn::ANPCCarPawn(const FObjectInitializer& ObjectInitializer)
{ TrackCircuitProbe = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TrackCircuitProbe"));
TrackCircuitProbe->SetRelativeLocation(FVector(200.f, .0f, 0.0f));
TrackCircuitProbe->SetWorldRotation(GetActorForwardVector().ToOrientationQuat());
TrackCircuitProbe->SetWorldScale3D(FVector(.2f, .2f, .2f));
TrackCircuitProbe->SetCollisionEnabled(ECollisionEnabled::NoCollision);
TrackCircuitProbe->SetupAttachment(RootComponent);

UWheeledVehicleMovementComponent* WheeledVehicleMovementComp = GetVehicleMovementComponent();
WheeledVehicleMovementComp->MaxEngineRPM = 5000;
bCanMove = true;
 
}

void ANPCCarPawn::AccumulateDistanceTravelled(float Distance)
{ DistanceTravelled += Distance;
if (DistanceTravelled == DistancePerLiter)
{
ConsumeGas();
 }

}

void ANPCCarPawn::BeginPlay()
{ Super::BeginPlay();

if (!GetWorldTimerManager().IsTimerActive(DebugLineHandle))
{
GetWorldTimerManager().SetTimer(DebugLineHandle, this, &ANPCCarPawn:::DrawLine, 0.5f, true);
}
if(!CurrentTrack && TrackCircuitArr.Num() != 0)
{
CurrentTrack = TrackCircuitArr[0];
 }

}

float ANPCCarPawn::CalculateAngleBetween()
{ USplineComponent* SplineComp = CurrentTrack->GetSplineComponent();
FVector Temp = TrackCircuitProbe->GetForwardVector();
FVector2D CarDirection = FVector2D(Temp.X, Temp.Y);
Temp = SplineComp->FindDirectionClosestToWorldLocation(TrackCircuitProbe->GetComponentLocation(), ESplineCoordinateSpace::World);
FVector2D SplineDirection = FVector2D(Temp.X, Temp.Y);
AngleBetween = FMath::RadiansToDegrees(UKismetMathLibrary::Asin((FVector2D::CrossProduct(CarDirection, SplineDirection)) / (CarDirection.Size() * SplineDirection.Size())));

return AngleBetween;
 }

void ANPCCarPawn::ConsumeGas()
{ GasCapacity -= QuantityGasConsumed;
if (GasCapacity == 0)
{
bCanMove = false;
}
  }
 
void ANPCCarPawn:::DrawLine()
{ FVector PawnLocation = GetActorLocation();
DrawDebugLine(GetWorld(), PawnLocation, PawnLocation + (GetActorForwardVector() * 500), FColor::Cyan, false, 180.f, 3.f);
DrawDebugPoint(GetWorld(), PawnLocation, 25.f, FColor::Red, false, 180.f);
DrawDebugString(GetWorld(), PawnLocation + FVector(0.f,0.f,300.f), *FString::Printf(TEXT("Angle = %f"), CalculateAngleBetween()), this, FColor::Orange, 180.f, false, 500.f);
 }

void ANPCCarPawn::EndPlay(const EEndPlayReason::Type EndPlayReason)
{ FTimerManager& TimerManager = GetWorldTimerManager();

if (TimerManager.IsTimerActive(DebugLineHandle))
{
TimerManager.ClearTimer(DebugLineHandle);
}
  }
 

CarAIController.h



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

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "CarAIController.generated.h"

/**
*
*/
class UBehaviorTree;
class UBehaviorTreeComponent;
class UBlackboardComponent;

UCLASS()
class PRJ_PROGRAMMINGTEST_API ACarAIController : public AAIController
{ GENERATED_BODY()
 
protected: UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "AI")
UBehaviorTree* BehaviorTree;

UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "AI")
UBehaviorTreeComponent* BehaviorTreeComp;

UPROPERTY(VisibleInstanceOnly, Category = "AI")
UBlackboardComponent* BlackboardComp;
 
public: ACarAIController(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

void BeginPlay() override;

void OnPossess(APawn* const OutPawn ) override;

UBlackboardComponent* GetBlackboard() const { return BlackboardComp; };
 };


CarAIController.cpp



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


#include "CarAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"

ACarAIController::ACarAIController(const FObjectInitializer& ObjectInitializer)
{ static ConstructorHelpers::FObjectFinder<UBehaviorTree> BehaviorTreeObj(TEXT("BehaviorTree'/Game/Blueprints/Car_BehaviorTree.Car_BehaviorTree'"));
if (ensureMsgf(BehaviorTreeObj.Succeeded(), TEXT("Can't find the object at the specified path.")))
{
BehaviorTree = BehaviorTreeObj.Object;
}
BehaviorTreeComp = ObjectInitializer.CreateDefaultSubobject<UBehaviorTreeComponent>(this, TEXT("BehaviorTreeComp"));
BlackboardComp = ObjectInitializer.CreateDefaultSubobject<UBlackboardComponent>(this, TEXT("BlackboardComp"));
 
}


void ACarAIController::BeginPlay()
{ Super::BeginPlay();
  RunBehaviorTree(BehaviorTree);
BehaviorTreeComp->StartTree(*BehaviorTree);
 
}

void ACarAIController::OnPossess(APawn* const OutPawn)
{ Super::OnPossess(OutPawn);

if (ensureMsgf(BlackboardComp, TEXT("BlackboardComp is null.")))
{
BlackboardComp->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
}
 }



And now the .cpp of the BTTasks

#1 Executed first



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


#include "MoveForwardTask.h"
#include "GameFramework/Pawn.h"
#include "BehaviorTree/BehaviorTreeTypes.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
#include "UObject/UObjectGlobals.h"
#include "CarAIController.h"
#include "NPCCarPawn.h"
#include "Blackboard_keys.h"
#include "Components/SplineComponent.h"
#include "SplineActor.h"
#include "Kismet/KismetMathLibrary.h"
#include "DrawDebugHelpers.h"

UMoveForwardTask::UMoveForwardTask(const FObjectInitializer& ObjectInitializer)
{ NodeName = TEXT("Move Forward");
 
}


EBTNodeResult::Type UMoveForwardTask::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{ const ACarAIController* CarController = Cast<ACarAIController>(OwnerComp.GetAIOwner());
ANPCCarPawn* CarPawn = Cast<ANPCCarPawn>(CarController->GetPawn());
FVector ClosestPoint = CarPawn->GetTrackCircuit()->GetSplineComponent()->FindLocationClosestToWorldLocation(CarPawn->GetActorLocation(), ESplineCoordinateSpace::World);
DrawDebugLine(GetWorld(), CarPawn->GetActorLocation(), ClosestPoint, FColor::Red, false, 180.f, 3.f);
bool IsCarFarAway = FVector::Dist(ClosestPoint, CarPawn->GetActorLocation()) > 100.f;
if(CarPawn->CanMove() && !IsCarFarAway)
{
  [INDENT=2]float angle = CarPawn->CalculateAngleBetween();
UE_LOG(LogTemp, Warning, TEXT("Angle between the car and the spline: %s"), *FString::Printf(TEXT("%f"), angle));
CarPawn->MoveForward(1);[/INDENT]
  }

HasEnoughGas = CarPawn->GetGasCapacity();
UE_LOG(LogTemp, Warning, TEXT("Gas capacity: %s"), *FString::FromInt(HasEnoughGas));
CarController->GetBlackboard()->SetValueAsInt(bb_keys::GasCapacity, HasEnoughGas);
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

return EBTNodeResult::Succeeded;
 }


#2 Executed later



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


#include "CalculateDotProduct.h"
#include "GameFramework/Pawn.h"
#include "BehaviorTree/BehaviorTreeTypes.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h"
#include "UObject/UObjectGlobals.h"
#include "CarAIController.h"
#include "NPCCarPawn.h"
#include "Blackboard_keys.h"
#include "Components/SplineComponent.h"
#include "SplineActor.h"
#include "Kismet/KismetMathLibrary.h"
#include "WheeledVehicleMovementComponent.h"
#include "DrawDebugHelpers.h"

UCalculateDotProduct::UCalculateDotProduct(const FObjectInitializer& ObjectInitializer)
{ NodeName = TEXT("Calculate Dot Product");
 }

EBTNodeResult::Type UCalculateDotProduct::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{ const ACarAIController* CarController = Cast<ACarAIController>(OwnerComp.GetAIOwner());
ANPCCarPawn* CarPawn = Cast<ANPCCarPawn>(CarController->GetPawn());
if(CarPawn->CanMove())
{
  [INDENT=2]float angle = CarPawn->CalculateAngleBetween();
if (CarPawn->IsTravellingReverseMode())
{[/INDENT]
  [INDENT=3]angle = angle - 180.f;[/INDENT]
  [INDENT=2]}
UE_LOG(LogTemp, Warning, TEXT("Angle between the car and the spline: %s"), *FString::Printf(TEXT("%f"), angle));
FVector ClosestPoint = CarPawn->GetTrackCircuit()->GetSplineComponent()->FindLocationClosestToWorldLocation(CarPawn->GetActorLocation(), ESplineCoordinateSpace::World);
DrawDebugLine(GetWorld(), CarPawn->GetActorLocation(), ClosestPoint, FColor::Red, false, 180.f, 3.f);
bool IsCarFarAway = FVector::Dist(ClosestPoint, CarPawn->GetActorLocation()) > 100.f;
UE_LOG(LogTemp, Warning, TEXT("Is the car too far away?: %s"), *FString(IsCarFarAway? "true" : "false"));

if(((angle >= -10 && angle <= 10) || (angle >= 170 && angle <= -170)))
{[/INDENT]
  [INDENT=3]CarPawn->MoveRight(0);[/INDENT]
  [INDENT=2]}
else //((angle > 10 && angle < 170) || (angle > -170 && angle < -10))
{[/INDENT]
  [INDENT=3]if (angle < -10 && angle > -170)
{
CarPawn->GetVehicleMovementComponent()->SetSteeringInput(-5);
CarPawn->MoveRight(-1);
//CarPawn->GetVehicleMovementComponent()->Velocity = CarPawn->GetVehicleMovementComponent()->Velocity * (angle / 180);
}[/INDENT]
  [INDENT=3]else
{[/INDENT]
  [INDENT=4]if (angle > 10 && angle < 170)
{
CarPawn->GetVehlcleMovementComponent()->
CarPawn->MoveRight(1);
//CarPawn->GetVehicleMovementComponent()->Velocity = CarPawn->GetVehicleMovementComponent()->Velocity * (angle / 180);
}[/INDENT]
  [INDENT=3]}[/INDENT]
  [INDENT=2]}[/INDENT]
  }

CarController->GetBlackboard()->SetValueAsVector(bb_keys::TargetLocation, CarPawn->GetActorLocation());
UE_LOG(LogTemp, Warning, TEXT("######################################################"));

FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

return EBTNodeResult::Succeeded;
 }



Basically what these two tasks do is to get the angle between the pawn and the closest point in the spline to do the curves. But the code is broken and I couldn’t see the problem here (in my defense my spine is sore and I have back aches )

Can you guys give me any suggestions to solve this?

I tried to use the use the FindClosestLocation method, but I couldn’t think a way to solve this. I’m still fresh with UE4.

Thanks!