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.