The Re-Inventing the Wheel Thread

right here is the very first incarnation of a 4 wheeled car in code.
this is basically a sketch, the code is heavily un-optimised and should not be a used as an example, however, maybe it will be a first step to something good.
please take note, im very much still feeling my way round ue4 and have barely touched its weird c++, let alone all this vehicle simulation stuff.
(saying that, already it performs infinitely better than the standard wheeled vehicle in some aspects)
theres probably lots of things i did wrong, i can already see a few so feel free to help improve it if you wish.
no wheel animation yet, unused variables ect

  1. create a c++ project
  2. create a new c++ class based on pawn called ‘TegCar_Pawn’ to keep it simple
  3. add inputs for ‘MoveForward’ and ‘MoveRight’ in your project properties
  4. copy/paste the code below to your new c++ class* (.h and .ccp files)

*change ‘CARPLUGIN_API’ in TegCar_Pawn.h to your project name or (dont overwrite that part)
*change #include “CarPlugin.h” to your project name in TegCar_Pawn.cpp (or dont overwrite that part)

  1. compile code and open editor
  2. create a blueprint based on TegCar_Pawn
  3. open the blueprint and give a mesh to the skeletal mesh component, and a camera if you want
  4. change the default pawn in your game mode to the new blueprint, build ramps ect
  5. raz, tweak, raz :slight_smile:

TegCar_Pawn.h


#pragma once

#include "GameFramework/Pawn.h"
// Needed for custom physics
#include "PhysicsPublic.h"

#include "TegCar_Pawn.generated.h"



UCLASS()
class CARPLUGIN_API ATegCar_Pawn : public APawn
{
	GENERATED_BODY()

private_subobject:
	/**  The main skeletal mesh associated with this Vehicle */
	DEPRECATED_FORGAME(4.6, "Mesh should not be accessed directly, please use GetMesh() function instead. Mesh will soon be private and your code will not compile.")
		UPROPERTY(Category = Vehicle, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	class USkeletalMeshComponent* Mesh;

	UPROPERTY(Category = VehicleSetup, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	class UArrowComponent* Arrow0;
	UPROPERTY(Category = VehicleSetup, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	class UArrowComponent* Arrow1;
	UPROPERTY(Category = VehicleSetup, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	class UArrowComponent* Arrow2;
	UPROPERTY(Category = VehicleSetup, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	class UArrowComponent* Arrow3;
	//arrow array
	UPROPERTY(EditAnywhere, Category = VehicleSetup)
		TArray<UArrowComponent*> ArrowArray;

private:
	FCalculateCustomPhysics OnCalculateCustomPhysics;
	void CustomPhysics(float DeltaTime, FBodyInstance* BodyInstance);
	FHitResult Trace(FVector TraceStart, FVector TraceDirection);
	FBodyInstance *MainBodyInstance;

	void ApplyWheel(float DeltaTime, FBodyInstance* BodyInstance, int32 Index);

	UPROPERTY()
		TArray<float> PreviousPosition;

	UPROPERTY()
		FVector ArrowLocation;

public:
	// Sets default values for this pawn's properties
	ATegCar_Pawn();

	

	/** Name of the MeshComponent. Use this name if you want to prevent creation of the component (with ObjectInitializer.DoNotCreateDefaultSubobject). */
	static FName VehicleMeshComponentName;

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	
	// Called every frame
	virtual void Tick( float DeltaSeconds ) override;

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

	/** Handle pressing forwards */
	void MoveForward(float Val);

	/** Handle pressing right */
	void MoveRight(float Val);

	void AddDrive(float DeltaTime, FBodyInstance* BodyInstance, FVector Loc, FVector Dir, int32 Index);
	void AddLatGrip(float DeltaTime, FBodyInstance* BodyInstance, FVector Loc, FVector Dir, int32 Index);

	UPROPERTY(EditAnywhere, Category = "Suspension")
		float TraceLength = 50.0f;
	UPROPERTY(EditAnywhere, Category = "Suspension")
		float SpringValue = 800000.0f;
	UPROPERTY(EditAnywhere, Category = "Suspension")
		float DamperValue = 750.0f;
	UPROPERTY(BlueprintReadOnly, Category = "Suspension")
		TArray<bool> bOnGround;
	UPROPERTY(BlueprintReadOnly, Category = "Suspension")
		TArray<float> SpringPosition;
	UPROPERTY(BlueprintReadOnly, Category = "Suspension")
		FVector SpringLocation;
	UPROPERTY(EditAnywhere, Category = "Suspension")
		TArray<FVector> SpringTopLocation;

	//engine
	UPROPERTY(EditAnywhere, Category = "Engine")
		float EnginePower = 500000.0f;
	UPROPERTY(BlueprintReadOnly, Category = "Engine")
		float CurrentPower = 0.0f;
	UPROPERTY(EditAnywhere, Category = "Engine")
		float EngineBrake = 50.0f;

	//steering
	UPROPERTY(EditAnywhere, Category = "Steering")
		TArray<FVector> WheelDirection;
	UPROPERTY(EditAnywhere, Category = "Steering")
		float SteerAngle = 45.0f;
	UPROPERTY(BlueprintReadOnly, Category = "Steering")
		float CurrentAngle = 0.0f;
	UPROPERTY(EditAnywhere, Category = "Steering")
		float SteerSpeed = 3.0f;

	//grip
	UPROPERTY(EditAnywhere, Category = "Wheels")
		float Grip = 400.0f;

	/** Returns Mesh subobject **/
	class USkeletalMeshComponent* GetMesh() const;
	
};


TegCar_Pawn.cpp


#include "CarPlugin.h"
#include "TegCar_Pawn.h"
#include "Kismet/KismetMathLibrary.h"

FName ATegCar_Pawn::VehicleMeshComponentName(TEXT("VehicleMesh"));

// Sets default values
ATegCar_Pawn::ATegCar_Pawn()
{

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

	Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(VehicleMeshComponentName);
	GetMesh()->SetCollisionProfileName(UCollisionProfile::Vehicle_ProfileName);
	GetMesh()->BodyInstance.bSimulatePhysics = true;
	GetMesh()->BodyInstance.bNotifyRigidBodyCollision = true;
	GetMesh()->BodyInstance.bUseCCD = true;
	GetMesh()->bBlendPhysics = true;
	GetMesh()->bGenerateOverlapEvents = true;
	GetMesh()->bCanEverAffectNavigation = false;
	RootComponent = GetMesh();

//add to array for easier handling, array of wheel structs would be better
	
	//arrows used as trace start locations
	SpringTopLocation.Add(FVector(120.0f, 90.0f, 0.0f));
	SpringTopLocation.Add(FVector(120.0f, -90.0f, 0.0f));
	SpringTopLocation.Add(FVector(-120.0f, 90.0f, 0.0f));
	SpringTopLocation.Add(FVector(-120.0f, -90.0f, 0.0f));

	Arrow0 = CreateDefaultSubobject<UArrowComponent>(TEXT("arrow0"));
	Arrow0->AttachParent = RootComponent;
	Arrow0->SetRelativeLocation(SpringTopLocation[0]);

	Arrow1 = CreateDefaultSubobject<UArrowComponent>(TEXT("arrow1"));
	Arrow1->AttachParent = RootComponent;
	Arrow1->SetRelativeLocation(SpringTopLocation[1]);

	Arrow2 = CreateDefaultSubobject<UArrowComponent>(TEXT("arrow2"));
	Arrow2->AttachParent = RootComponent;
	Arrow2->SetRelativeLocation(SpringTopLocation[2]);

	Arrow3 = CreateDefaultSubobject<UArrowComponent>(TEXT("arrow3"));
	Arrow3->AttachParent = RootComponent;
	Arrow3->SetRelativeLocation(SpringTopLocation[3]);
	
	ArrowArray.Emplace(Arrow0);
	ArrowArray.Emplace(Arrow1);
	ArrowArray.Emplace(Arrow2);
	ArrowArray.Emplace(Arrow3);

	WheelDirection.Init(FVector(0.0f, 0.0f, 0.0f), 4);//unused, intended for wheel animation later

	PreviousPosition.Init(0.0f,4);

	bOnGround.Init(false,4);

	SpringPosition.Init(0.0f, 4);


	// Bind function delegate
	OnCalculateCustomPhysics.BindUObject(this, &ATegCar_Pawn::CustomPhysics);

}

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

	// Get the static mesh from attached actors root

	if (GetMesh() != NULL){
		MainBodyInstance = GetMesh()->GetBodyInstance();

	}
	
}

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

	// Add custom physics on MainBodyMesh
	if (MainBodyInstance != NULL){
		MainBodyInstance->AddCustomPhysics(OnCalculateCustomPhysics);
		
	}
}

// Called to bind functionality to input
void ATegCar_Pawn::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
	Super::SetupPlayerInputComponent(InputComponent);

	// set up gameplay key bindings
	check(InputComponent);

	InputComponent->BindAxis("MoveForward", this, &ATegCar_Pawn::MoveForward);
	InputComponent->BindAxis("MoveRight", this, &ATegCar_Pawn::MoveRight);

}

void ATegCar_Pawn::MoveForward(float Val)
{
	CurrentPower = EnginePower * Val;
}

void ATegCar_Pawn::MoveRight(float Val)
{
	CurrentAngle = FMath::Lerp(CurrentAngle, SteerAngle * Val, SteerSpeed * GetWorld()->DeltaTimeSeconds);
//rotate first 2 arrow components, front wheels
	ArrowArray[0]->SetRelativeRotation(FRotator(0.0f, CurrentAngle, 0.0f));
	ArrowArray[1]->SetRelativeRotation(FRotator(0.0f, CurrentAngle, 0.0f));
}

// Called every substep for selected body instance
void ATegCar_Pawn::CustomPhysics(float DeltaTime, FBodyInstance* BodyInstance)
{
	if (ArrowArray.Num()>0){

	
	//trace and apply force for each arrow component	
	for (int32 b = 0; b < ArrowArray.Num(); b++)
		{
			//~~~~~~~~~~~~~~~~~~~~~~
			ApplyWheel(DeltaTime, BodyInstance, b);
			
		}
	}
}

void ATegCar_Pawn::ApplyWheel(float DeltaTime, FBodyInstance* BodyInstance, int32 Index){

	// BodyLocation is arrow location, the top of the spring
	FVector BodyLocation = ArrowArray[Index]->GetComponentLocation();

	FHitResult Hit = Trace(BodyLocation, -GetActorUpVector());
	if (Hit.bBlockingHit) {

		SpringLocation = Hit.ImpactPoint;

		float SpringPosition = (Hit.Location - BodyLocation).Size();

		// If previously on air, set previous position to current position
		if (!bOnGround[Index]){
			PreviousPosition[Index] = SpringPosition;
		}
		float DamperVelocity = (SpringPosition - PreviousPosition[Index]) / DeltaTime;
		PreviousPosition[Index] = SpringPosition;
		bOnGround[Index] = true;

		// Calculate spring force
		float SpringForce = (1 - (SpringPosition / TraceLength)) * SpringValue;

		// Apply damper force
		SpringForce -= DamperValue * DamperVelocity;

		FVector TotalForce = SpringForce * Hit.ImpactNormal;

		// Just as example, enabling following line would just cancel the gravity:
		//		TotalForce = BodyInstance->GetBodyMass() * 980.0f * FVector::UpVector;
		//spring
		BodyInstance->AddImpulseAtPosition(TotalForce * DeltaTime, BodyLocation);

		//drive, lateral grip and steering forces, should do it all in 1 go
		AddDrive(DeltaTime, BodyInstance, BodyLocation, GetActorForwardVector(), Index);
		AddLatGrip(DeltaTime, BodyInstance, BodyLocation, GetActorForwardVector(), Index);
	}
	else {
		bOnGround[Index] = false;
		SpringPosition[Index] = TraceLength;
	}
}

FHitResult ATegCar_Pawn::Trace(FVector TraceStart, FVector TraceDirection){
	FHitResult Hit(ForceInit);
	FCollisionQueryParams TraceParams(true);
	TraceParams.bTraceAsyncScene = true;
	TraceParams.bReturnPhysicalMaterial = false;
	FVector TraceEnd = TraceStart + (TraceDirection * TraceLength);
	GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_WorldDynamic, TraceParams);
	return Hit;
}

void ATegCar_Pawn::AddDrive(float DeltaTime, FBodyInstance* BodyInstance, FVector Loc, FVector Dir, int32 Index){

	FVector TempVel = BodyInstance->GetUnrealWorldVelocityAtPoint(Loc);
	FVector TempVec = ArrowArray[Index]->GetForwardVector().GetSafeNormal();

         //engine 'drag'
	float ForwardSpeed = FVector::DotProduct(TempVec, TempVel) * EngineBrake;
	Dir = -TempVec * ForwardSpeed;
        //engine power
	Dir += ArrowArray[Index]->GetForwardVector().GetSafeNormal() * CurrentPower;

	BodyInstance->AddImpulseAtPosition(Dir * DeltaTime, Loc);
}

void ATegCar_Pawn::AddLatGrip(float DeltaTime, FBodyInstance* BodyInstance, FVector Loc, FVector Dir, int32 Index){

	FVector TempVel = BodyInstance->GetUnrealWorldVelocityAtPoint(Loc);
	FVector TempVec = ArrowArray[Index]->GetRightVector().GetSafeNormal();
	
	float SideSpeed = FVector::DotProduct(TempVec, TempVel) * Grip;
        //- sideways speed of the component
	Dir = -TempVec * SideSpeed;

	BodyInstance->AddImpulseAtPosition(Dir * DeltaTime, Loc);
}

/** Returns Mesh subobject **/
USkeletalMeshComponent* ATegCar_Pawn::GetMesh() const { return Mesh; }