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
- create a c++ project
- create a new c++ class based on pawn called ‘TegCar_Pawn’ to keep it simple
- add inputs for ‘MoveForward’ and ‘MoveRight’ in your project properties
- 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)
- compile code and open editor
- create a blueprint based on TegCar_Pawn
- open the blueprint and give a mesh to the skeletal mesh component, and a camera if you want
- change the default pawn in your game mode to the new blueprint, build ramps ect
- raz, tweak, raz
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; }