[C++ VIDEO TUTORIAL] Roller Coasters, Path Following, Rail Shooters

This short tutorial will show how to use a spline to create a simple roller coaster. It code can alo be used for path following and rail shooters.

The main idea is to look for the spline component in your level and then sample points along it. You can place objects (“GoodItems”) along (or near) the spline to collect and then later move the player along it - in the “Tick()” function.

This is the code I used for the prototype in the video:

In CoasterPawn.cpp


for (TObjectIterator<USplineComponent> SplineComponent; SplineComponent; ++SplineComponent)
	{
		int numberOfSplinePoints = SplineComponent->GetNumberOfSplinePoints();
		float totalLength = SplineComponent->GetSplineLength();

		float currentLength = 0;
		int itemSpacing = 5; //spacing between items which are spawned along the spline
		int sampleLength = 150; //we will sample the spline every "sampleLength" units

		FRotator splinePointRotation = FRotator(0, 0, 0);

		if (numberOfSplinePoints > 5) {//you can also use GetName() to select the spline component you want to process

			int splinePointCount = 0;

			while (currentLength < totalLength) {

				//The next line samples the spline at "currentLength" units from the starting point
				FTransform splinePointTransform = SplineComponent->GetTransformAtDistanceAlongSpline(currentLength, ESplineCoordinateSpace::World);

				currentLength += sampleLength;//increase "currentLength" for the next sample

				pathPointRotation[splinePointCount] = splinePointTransform.GetRotation();

				FVector up = HEIGHT * pathPointRotation[splinePointCount].GetAxisZ();//this vector is above the spline by 300 units

				pathPointLocation[splinePointCount] = splinePointTransform.GetLocation() + up;//this will be used to place the player (i.e. the space ship if you don't change the model)

				splinePointRotation = FRotator(pathPointRotation[splinePointCount]);

				//Now spawn an item to collect as you move along the spline later in the Tick() function
				AGoodItem *myGoodItem;
				if (splinePointCount % itemSpacing == 0) {//only spawn an item every at the interval "itemSpacing"
					myGoodItem = GetWorld()->SpawnActor<AGoodItem>(pathPointLocation[splinePointCount] - up, splinePointRotation);//spawn objects below the spline
					myGoodItem->SetActorScale3D(FVector(3, 3, 3));
				}
				splinePointCount += 1;
			}
			totalSplinePoints = splinePointCount;
		}
	}

Then in the Tick() function, move the player along the sampled points:


void ACoasterPawn::Tick(float DeltaSeconds)
{
	RootComponent->SetWorldLocation(pathPointLocation[splinePointer]);//just move the player to the next sampled point on the spline
	RootComponent->SetWorldRotation(pathPointRotation[splinePointer]);//and give the player the same rotation as the sampled point

	splinePointer += 1;

	if (splinePointer >= totalSplinePoints)
		splinePointer = 1;
}

For the test in the video, I used these globals and includes at the top of the file:


#include "GoodItem.h"//assumes you have an Actor class called "AGoodItem" for collectable items you can spawn - see below

#include "Components/SplineComponent.h"
#include "Components/SplineMeshComponent.h"

const int HEIGHT = 300;//height of player above spline
const int MAXPOINTS = 5000;//stop sampling the spline after MAXPOINTS points
FVector pathPointLocation[MAXPOINTS];//save sampled point locations into an array
FQuat pathPointRotation[MAXPOINTS];//save sampled point rotations into an array
int totalSplinePoints = 0; //After we sampled the spline at intervals, this is the total number of sampled points on the curve
int splinePointer = 1;//this counter is incremented in the Tick() function to move us to the next point on the spline

For the chase camera, use this in the Coaster() constructor - like in the Vehicle Template code:


SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetRelativeLocation(FVector(0.0f, 0.0f, 54.0f));
	SpringArm->SetWorldRotation(FRotator(-20.0f, 0.0f, 0.0f));
	SpringArm->AttachTo(RootComponent);
	SpringArm->TargetArmLength = 1250.0f;
	SpringArm->bEnableCameraLag = false;
	SpringArm->bEnableCameraRotationLag = false;
	SpringArm->bInheritPitch = true;
	SpringArm->bInheritYaw = true;
	SpringArm->bInheritRoll = true;

	// Create the chase camera component 
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("ChaseCamera"));
	Camera->AttachTo(SpringArm, USpringArmComponent::SocketName);
	Camera->SetRelativeRotation(FRotator(10.0f, 0.0f, 0.0f));
	Camera->bUsePawnControlRotation = false;
	Camera->FieldOfView = 90.f;

And then in CoasterPawn.h use this for the camera declarations (like in the Vehicle Template code):


/** Spring arm that will offset the camera */
	UPROPERTY(Category = Camera, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
		USpringArmComponent* SpringArm;

	/** Camera component that will be our viewpoint */
	UPROPERTY(Category = Camera, VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
	UCameraComponent* Camera;

and remove references to the previous camera code at the bottom of the file, as shown in the video.

Now you just need to create an Actor class and add a StaticMeshComponent and a Collision Handler function as follows:

In GoodItem.cpp


AGoodItem::AGoodItem()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	myCollisionSphere = CreateDefaultSubobject<USphereComponent>(TEXT("CollisionSphere"));
	myCollisionSphere->InitSphereRadius(150.0f);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> GoodMesh(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));

	// Create the mesh component

	CollectableMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("GoodMesh"));
	RootComponent = myCollisionSphere;
	CollectableMeshComponent->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
	CollectableMeshComponent->SetStaticMesh(GoodMesh.Object);
	CollectableMeshComponent->SetWorldScale3D(FVector(.5f, .5f, .5f));
	CollectableMeshComponent->AttachTo(RootComponent);

	myCollisionSphere->OnComponentBeginOverlap.AddDynamic(this, &AGoodItem::HandleCollision);//for collision handling
}

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

}

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

}

//add this at the bottom of the file for collision handling:

void AGoodItem::HandleCollision(AActor* Other, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& OverlapInfo)
{
	FString name = Other->GetName();

	if (!name.Contains("Coaster"))
		return;

	UWorld* const World = GetWorld();
	if (World)
		Destroy();

}

And here is GoodItem.h


#pragma once

#include "GameFramework/Actor.h"
#include "GoodItem.generated.h"

UCLASS()
class COASTER_API AGoodItem : public AActor
{
	GENERATED_BODY()
	/** Sphere collision component */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Enemy, meta = (AllowPrivateAccess = "true"))
	UStaticMeshComponent* CollectableMeshComponent;

	USphereComponent *myCollisionSphere;

public:
	// Sets default values for this actor's properties
	AGoodItem();

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	// Called every frame
	virtual void Tick(float DeltaSeconds) override;

	UFUNCTION()
	void HandleCollision(AActor* Other, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& OverlapInfo);
};


For the track, I didn’t use a SplineMeshComponent, I just spwaned “track” StaticMeshObjects along the spline and scaled them differently at different intervals. Same code as used above for the good items. I may post a similar tutorial with SplineMeshComponents soon.

Hope it helps.

Edit: Added the code used in the video.

Waiting for code ^^ :slight_smile:

I added the code to the opening post.

Over on YouTube, I was asked about the left and right movememnt of the player along the spline. As the player rotates, you need to get the right (or left vector) and this is the code I used for that:


float offset = 500.0f;

FTransform ft = SplineIterator->GetTransformAtDistanceAlongSpline(length, ESplineCoordinateSpace::World);
FVector right = ft.GetRotation().GetAxisY();

SetActorLocation(GetActorLocation() + offset * right);//move the player right when the "D" key is pressed.

See also this thread for something similar: https://forums.unrealengine.com/showthread.php?94038-C-VIDEO-TUTORIAL-Making-a-simple-quot-Subway-Surfer-quot-game&p=435859#post435859

Hope that helps.

Hey!
Any chances of doing this directly via a blueprint instead of a C++ code?

1.png

Use your code ,I’m headed for the red road. circular spline Path . the “D” key is pressed . Can’t move right . How can I keep going on the right side?