In this post we’re going to create some flight paths for our character (similar to the World of Warcraft flight path system). We’re going to omit any UI interactivity for the sake of simplicity. If you would like to include some UMG behavior in the following tutorial, make sure to check out my UMG with C++ tutorial. But before we start, here is the end result:

What we’re going to create

Before we start typing any code, let’s explain what the above video demostrates. In this post, we’re going to create some Flight Stop Actors which have two spline components and two assigned float curves. The spline components describe the actual flight path that the character will follow when he selects either of them. As a convention in this post, one spline component corresponds to the next flight path while the other spline component corresponds to the previous flight path.

The assigned float curves describe the way that the character will travel the selected flight path (meaning the total travel time and therefore his travel speed).

The character contains a box component in order to identify any nearby flight stop actors. Moreover, I’ve provided two inputs in order to select the next or the previous flight path (if available).

In order to start implementiong the above mechanic, create a new C++ Third Person Template Project that comes in with UE4. In this post I’m using UE 4.13.1 so in case you’re using a different engine version you may need to edit the code a bit in order to match your API.

Creating the Flight Stop Actors

In order to create the Flight Stop Actor, create a C++ class that derives from the Actor class, name it FlightStopActor and add the following declarations:

FlightStopActor header file protected: /** The FloatCurve corresponding to the previous flight spline component */ UPROPERTY(EditAnywhere) UCurveFloat* PreviousFlightCurve; /** The FloatCurve corresponding to the next flight spline component */ UPROPERTY(EditAnywhere) UCurveFloat* NextFlightCurve; /** A static mesh for our flight stop */ UPROPERTY(VisibleAnywhere) UStaticMeshComponent* SM; /** The spline component that describes the flight path of the next flight */ UPROPERTY(VisibleAnywhere) USplineComponent* NextFlightStop; /** The spline component that describes the flight path of the previous flight */ UPROPERTY(VisibleAnywhere) USplineComponent* PreviousFlightStop; public: // Sets default values for this actor's properties AFlightStop(); /** Returns the previous flight curve */ UCurveFloat* GetPreviousFlightCurve() { return PreviousFlightCurve; }; /** Returns the next flight curve */ UCurveFloat* GetNextFlightCurve() { return NextFlightCurve; }; /** Returns the next flight spline component */ USplineComponent* GetNextFlightSplineComp() { return NextFlightStop; }; /**Returns the previous flight spline component */ USplineComponent* GetPreviousFlightSplineComp() { return PreviousFlightStop; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 protected : /** The FloatCurve corresponding to the previous flight spline component */ UPROPERTY ( EditAnywhere ) UCurveFloat * PreviousFlightCurve ; /** The FloatCurve corresponding to the next flight spline component */ UPROPERTY ( EditAnywhere ) UCurveFloat * NextFlightCurve ; /** A static mesh for our flight stop */ UPROPERTY ( VisibleAnywhere ) UStaticMeshComponent * SM ; /** The spline component that describes the flight path of the next flight */ UPROPERTY ( VisibleAnywhere ) USplineComponent * NextFlightStop ; /** The spline component that describes the flight path of the previous flight */ UPROPERTY ( VisibleAnywhere ) USplineComponent * PreviousFlightStop ; public : // Sets default values for this actor's properties AFlightStop ( ) ; /** Returns the previous flight curve */ UCurveFloat * GetPreviousFlightCurve ( ) { return PreviousFlightCurve ; } ; /** Returns the next flight curve */ UCurveFloat * GetNextFlightCurve ( ) { return NextFlightCurve ; } ; /** Returns the next flight spline component */ USplineComponent * GetNextFlightSplineComp ( ) { return NextFlightStop ; } ; /**Returns the previous flight spline component */ USplineComponent * GetPreviousFlightSplineComp ( ) { return PreviousFlightStop ; } ;

Moreover, make sure to include the following header file:

#include “Components/SplineComponent.h”

Then, type in the following code into the constructor of your actor:

FlightStopActor constructor // Sets default values AFlightStop::AFlightStop() { // 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; SM = CreateDefaultSubobject<UStaticMeshComponent>(FName("SM")); SetRootComponent(SM); //Init splines NextFlightStop = CreateDefaultSubobject<USplineComponent>(FName("SplineComp")); PreviousFlightStop = CreateDefaultSubobject<USplineComponent>(FName("PreviousFlightStop")); //Attach them to root component NextFlightStop->SetupAttachment(SM); PreviousFlightStop->SetupAttachment(SM); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // Sets default values AFlightStop :: AFlightStop ( ) { // 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 ; SM = CreateDefaultSubobject < UStaticMeshComponent > ( FName ( "SM" ) ) ; SetRootComponent ( SM ) ; //Init splines NextFlightStop = CreateDefaultSubobject < USplineComponent > ( FName ( "SplineComp" ) ) ; PreviousFlightStop = CreateDefaultSubobject < USplineComponent > ( FName ( "PreviousFlightStop" ) ) ; //Attach them to root component NextFlightStop -> SetupAttachment ( SM ) ; PreviousFlightStop -> SetupAttachment ( SM ) ; }

Save and compile your code. Then, create a Blueprint based on the above C++ class and assign a default static mesh. When you’re done with that, place some Flight Stop Actors in your level and extend their spline components. In my case, I’ve assigned a green color to the Previous Flight Paths and a blue-ish color to the Next Flight Paths.

Here is a screenshot of my map:

Spline Components come with just two points inside the editor. In order to add more, select a point in your spline and click the “Alt” button while you’re moving the point. This way, you can create as many points you like.

Let’s move on to the character’s implementation.

Character preparation

Before we edit the code that comes with the Third Person C++ Project template, add two inputs for your project, named NextFlightPath and PreviousFlightPath. Then, open up the header file of your character and add the following code:

Character declaration private: /** The flight timeline */ FTimeline FlightTimeline; /** The function that ticks the timeline */ UFUNCTION() void TickTimeline(float Value); /** The active spline component, meaning the flight path that the character is currently following */ USplineComponent* ActiveSplineComponent; /** The selected flight stop actor */ AFlightStop* ActiveFlightStopActor; /** Box overlap function */ UFUNCTION() void OnFlightBoxColliderOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); /** Executes when we're pressing the NextFlightPath key bind */ void NextFlightPathSelected(); /** Executes when we're pressing the PreviousFlightPath key bind */ void PreviousFlightPathSelected(); /** Updates the flight timeline with a new curve and starts the flight */ void UpdateFlightTimeline(UCurveFloat* CurveFloatToBind); UFUNCTION() void ResetActiveFlightStopActor(); protected: /*The Box component that detects any nearby flight stops*/ UPROPERTY(VisibleAnywhere) UBoxComponent* FlightBoxCollider; public: AWoWFlightCharacter(); virtual void BeginPlay() override; virtual void Tick(float DeltaSeconds) override; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private : /** The flight timeline */ FTimeline FlightTimeline ; /** The function that ticks the timeline */ UFUNCTION ( ) void TickTimeline ( float Value ) ; /** The active spline component, meaning the flight path that the character is currently following */ USplineComponent * ActiveSplineComponent ; /** The selected flight stop actor */ AFlightStop * ActiveFlightStopActor ; /** Box overlap function */ UFUNCTION ( ) void OnFlightBoxColliderOverlap ( UPrimitiveComponent * OverlappedComponent , AActor * OtherActor , UPrimitiveComponent * OtherComp , int32 OtherBodyIndex , bool bFromSweep , const FHitResult & SweepResult ) ; /** Executes when we're pressing the NextFlightPath key bind */ void NextFlightPathSelected ( ) ; /** Executes when we're pressing the PreviousFlightPath key bind */ void PreviousFlightPathSelected ( ) ; /** Updates the flight timeline with a new curve and starts the flight */ void UpdateFlightTimeline ( UCurveFloat * CurveFloatToBind ) ; UFUNCTION ( ) void ResetActiveFlightStopActor ( ) ; protected : /*The Box component that detects any nearby flight stops*/ UPROPERTY ( VisibleAnywhere ) UBoxComponent * FlightBoxCollider ; public : AWoWFlightCharacter ( ) ; virtual void BeginPlay ( ) override ; virtual void Tick ( float DeltaSeconds ) override ;

Before you continue any further, add the following includes right before the .generated.h library file:

#include “FlightStop.h”

#include “Components/TimelineComponent.h”

Switch up to your character’s source file. At the end of it’s constructor implementation, add the following code in order to initialize our box component:

Box Collider init FlightBoxCollider = CreateDefaultSubobject<UBoxComponent>(FName("FlightBoxCollider")); FlightBoxCollider->SetBoxExtent(FVector(150.f)); FlightBoxCollider->SetupAttachment(GetRootComponent()); 1 2 3 4 5 FlightBoxCollider = CreateDefaultSubobject < UBoxComponent > ( FName ( "FlightBoxCollider" ) ) ; FlightBoxCollider -> SetBoxExtent ( FVector ( 150.f ) ) ; FlightBoxCollider -> SetupAttachment ( GetRootComponent ( ) ) ;

Then, implement the following BeginPlay function and implement the following logic fo the OnFlightBoxColliderOverlap

BeginPlay and OnFlightBoxColliderOverlap void AWoWFlightCharacter::BeginPlay() { Super::BeginPlay(); //Register a function that gets called when the box overlaps with a component FlightBoxCollider->OnComponentBeginOverlap.AddDynamic(this, &AWoWFlightCharacter::OnFlightBoxColliderOverlap); } void AWoWFlightCharacter::OnFlightBoxColliderOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (OtherActor->IsA<AFlightStop>()) { //Store a reference of the nearby flight stop actor ActiveFlightStopActor = Cast<AFlightStop>(OtherActor); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void AWoWFlightCharacter :: BeginPlay ( ) { Super :: BeginPlay ( ) ; //Register a function that gets called when the box overlaps with a component FlightBoxCollider -> OnComponentBeginOverlap . AddDynamic ( this , &AWoWFlightCharacter :: OnFlightBoxColliderOverlap ) ; } void AWoWFlightCharacter :: OnFlightBoxColliderOverlap ( UPrimitiveComponent * OverlappedComponent , AActor * OtherActor , UPrimitiveComponent * OtherComp , int32 OtherBodyIndex , bool bFromSweep , const FHitResult & SweepResult ) { if ( OtherActor -> IsA < AFlightStop > ( ) ) { //Store a reference of the nearby flight stop actor ActiveFlightStopActor = Cast < AFlightStop > ( OtherActor ) ; } }

When you’re done with that, add the following logic for the UpdateFlightTimeline function and ResetActiveFlightStopActor:

UpdateFlightTimeline and ResetActiveFlightStopActor logic void AWoWFlightCharacter::UpdateFlightTimeline(UCurveFloat* CurveFloatToBind) { //Initialize a timeline FlightTimeline = FTimeline(); FOnTimelineFloat ProgressFunction; //Bind the function that ticks the timeline ProgressFunction.BindUFunction(this, FName("TickTimeline")); //Assign the provided curve and progress function for our timeline FlightTimeline.AddInterpFloat(CurveFloatToBind, ProgressFunction); FlightTimeline.SetLooping(false); FlightTimeline.PlayFromStart(); //Set the timeline's length to match the last key frame based on the given curve FlightTimeline.SetTimelineLengthMode(TL_LastKeyFrame); //The ResetActiveFlightStopActor executes when the timeline finishes. //By calling ResetActiveFlightStopActor at the end of the timeline we make sure to reset any invalid references on ActiveFlightStopActor FOnTimelineEvent TimelineEvent; TimelineEvent.BindUFunction(this, FName("ResetActiveFlightStopActor")); FlightTimeline.SetTimelineFinishedFunc(TimelineEvent); } void AWoWFlightCharacter::ResetActiveFlightStopActor() { ActiveFlightStopActor = nullptr; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void AWoWFlightCharacter :: UpdateFlightTimeline ( UCurveFloat * CurveFloatToBind ) { //Initialize a timeline FlightTimeline = FTimeline ( ) ; FOnTimelineFloat ProgressFunction ; //Bind the function that ticks the timeline ProgressFunction . BindUFunction ( this , FName ( "TickTimeline" ) ) ; //Assign the provided curve and progress function for our timeline FlightTimeline . AddInterpFloat ( CurveFloatToBind , ProgressFunction ) ; FlightTimeline . SetLooping ( false ) ; FlightTimeline . PlayFromStart ( ) ; //Set the timeline's length to match the last key frame based on the given curve FlightTimeline . SetTimelineLengthMode ( TL_LastKeyFrame ) ; //The ResetActiveFlightStopActor executes when the timeline finishes. //By calling ResetActiveFlightStopActor at the end of the timeline we make sure to reset any invalid references on ActiveFlightStopActor FOnTimelineEvent TimelineEvent ; TimelineEvent . BindUFunction ( this , FName ( "ResetActiveFlightStopActor" ) ) ; FlightTimeline . SetTimelineFinishedFunc ( TimelineEvent ) ; } void AWoWFlightCharacter :: ResetActiveFlightStopActor ( ) { ActiveFlightStopActor = nullptr ; }

In case you’re interested more in what’s going on inside the UpdateFlightTimeline function, make sure to check out my tutorial on how to consume timelines using C++ here.

When you’re done with that, make sure to bind the action inputs that you have provided in the beginning of this section, using the following code inside the SetupPlayerInputComponent:

Action binding //Bind the functions that execute on key press PlayerInputComponent->BindAction("NextFlightPath", IE_Pressed, this, &AWoWFlightCharacter::NextFlightPathSelected); PlayerInputComponent->BindAction("PreviousFlightPath", IE_Pressed, this, &AWoWFlightCharacter::PreviousFlightPathSelected); 1 2 3 //Bind the functions that execute on key press PlayerInputComponent -> BindAction ( "NextFlightPath" , IE_Pressed , this , &AWoWFlightCharacter :: NextFlightPathSelected ) ; PlayerInputComponent -> BindAction ( "PreviousFlightPath" , IE_Pressed , this , &AWoWFlightCharacter :: PreviousFlightPathSelected ) ;

Then, provide the logic of the above inputs, using the following code:

Input logic void AWoWFlightCharacter::NextFlightPathSelected() { if (ActiveFlightStopActor) { //Get the next flight path's spline component and update the flight timeline with the corresponding curve ActiveSplineComponent = ActiveFlightStopActor->GetNextFlightSplineComp(); UpdateFlightTimeline(ActiveFlightStopActor->GetNextFlightCurve()); } } void AWoWFlightCharacter::PreviousFlightPathSelected() { if (ActiveFlightStopActor) { //Get the previous flight path's spline component and update the flight timeline with the corresponding curve ActiveSplineComponent = ActiveFlightStopActor->GetPreviousFlightSplineComp(); UpdateFlightTimeline(ActiveFlightStopActor->GetPreviousFlightCurve()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void AWoWFlightCharacter :: NextFlightPathSelected ( ) { if ( ActiveFlightStopActor ) { //Get the next flight path's spline component and update the flight timeline with the corresponding curve ActiveSplineComponent = ActiveFlightStopActor -> GetNextFlightSplineComp ( ) ; UpdateFlightTimeline ( ActiveFlightStopActor -> GetNextFlightCurve ( ) ) ; } } void AWoWFlightCharacter :: PreviousFlightPathSelected ( ) { if ( ActiveFlightStopActor ) { //Get the previous flight path's spline component and update the flight timeline with the corresponding curve ActiveSplineComponent = ActiveFlightStopActor -> GetPreviousFlightSplineComp ( ) ; UpdateFlightTimeline ( ActiveFlightStopActor -> GetPreviousFlightCurve ( ) ) ; } }

In the UpdateFlightTimeline function, we declare that we want to play the provided timeline from start (line 14 in the provided code snippet). However, we need to explicitly tick the timeline inside the tick function. To do so, type in the following code:

Character's tick function void AWoWFlightCharacter::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); //If the timeline has started, advance it by DeltaSeconds if (FlightTimeline.IsPlaying()) FlightTimeline.TickTimeline(DeltaSeconds); } 1 2 3 4 5 6 7 void AWoWFlightCharacter :: Tick ( float DeltaSeconds ) { Super :: Tick ( DeltaSeconds ) ; //If the timeline has started, advance it by DeltaSeconds if ( FlightTimeline . IsPlaying ( ) ) FlightTimeline . TickTimeline ( DeltaSeconds ) ; }

Then, provide the following implementation for the TickTimeline function:

TickTimeline implementation void AWoWFlightCharacter::TickTimeline(float Value) { float SplineLength = ActiveSplineComponent->GetSplineLength(); //Get the new location based on the provided values from the timeline. //The reason we're multiplying Value with SplineLength is because all our designed curves in the UE4 editor have a time range of 0 - X. //Where X is the total flight time. FVector NewLocation = ActiveSplineComponent->GetLocationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World); SetActorLocation(NewLocation); FRotator NewRotation = ActiveSplineComponent->GetRotationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World); //We're not interested in the pitch value of the above rotation so we make sure to set it to zero NewRotation.Pitch = 0; SetActorRotation(NewRotation); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void AWoWFlightCharacter :: TickTimeline ( float Value ) { float SplineLength = ActiveSplineComponent -> GetSplineLength ( ) ; //Get the new location based on the provided values from the timeline. //The reason we're multiplying Value with SplineLength is because all our designed curves in the UE4 editor have a time range of 0 - X. //Where X is the total flight time. FVector NewLocation = ActiveSplineComponent -> GetLocationAtDistanceAlongSpline ( Value * SplineLength , ESplineCoordinateSpace :: World ) ; SetActorLocation ( NewLocation ) ; FRotator NewRotation = ActiveSplineComponent -> GetRotationAtDistanceAlongSpline ( Value * SplineLength , ESplineCoordinateSpace :: World ) ; //We're not interested in the pitch value of the above rotation so we make sure to set it to zero NewRotation . Pitch = 0 ; SetActorRotation ( NewRotation ) ; }

Save and compile your code. Then, make sure to provide timelines in each FlightStopActor. These timelines need to have an initial value of 0 and a target value of 1. The time value equals the total flight time so you can temper with that value to match your needs.

For example, here is the timeline that get’s played on the large spline component in the video shown above:

Once you have provided the timelines, play with your character and once you get near a FlightStopActor press the keybind that you provided. In case your character won’t fly, make sure that your BoxComponent is aligned with your character (sometimes UE4 places these components by default in 0,0,0 – world space coordinates)

Update: Since a lot people ask me about the whole project, you can download it from here.