<style>.lazy{display:none}</style> Skip to main content

Unreal Engine C++ Fundamentals – Moving Static Meshes along a Spline Component

By Props, Tutorial, Unreal No Comments

Hey guys,

Today we are going to take a look how to use Spline Components and modify their various properties to allow us to specify custom materials as well as determine how meshes are attached to the spline.

The project files for this video & article can be found on our GitHub page.

Looking at our previous tutorial we know that splines are not terribly hard to work with but there is a bit of magic to having something move along a spline.

The key ingredient in this is the FTimeline. This allows us to keep track of an execution of values across a specific timeframe.

The FTimeline combined with a UCurveFloat gives us all that flexibility. So let’s look at the code to see how this all breaks down.

FTimeline MovementTimeline;

FOnTimelineFloat ProgressFunction; ProgressFunction.BindUFunction(this, TEXT("ProcessMovementTimeline")); MovementTimeline.AddInterpFloat(MovementCurve, ProgressFunction); FOnTimelineEvent OnTimelineFinishedFunction; OnTimelineFinishedFunction.BindUFunction(this, TEXT("OnEndMovementTimeline")); MovementTimeline.SetTimelineFinishedFunc(OnTimelineFinishedFunction); MovementTimeline.SetTimelineLengthMode(TL_LastKeyFrame); if(bAutoActivate) { MovementTimeline.PlayFromStart(); }

Looking over the code we can see that as part of the FTimeline definition we are specifying a function that will be executed during the runtime of the timeline.

UFUNCTION()
void ProcessMovementTimeline(float Value);

As well as a function at will be executed at the end of our timeline.

UFUNCTION()
void OnEndMovementTimeline();

Additionally we need to ensure that our timeline ticks as part of our object. For this we can examine the Tick method.

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

  if(MovementTimeline.IsPlaying())
  {
    MovementTimeline.TickTimeline(DeltaTime);	
  }
}

Now let’s take a look at the magic around moving the mesh across the spline. This all happens within our ProcessMovementTimeline method.

void AMovingSplineActor::ProcessMovementTimeline(float Value)
{
  const float SplineLength = SplineComponent->GetSplineLength();

  const FVector CurrentSplineLocation = SplineComponent->GetLocationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);
  FRotator CurrentSplineRotation = SplineComponent->GetRotationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);

  CurrentSplineRotation.Pitch = 0.f;
  MeshComponent->SetWorldLocationAndRotation(CurrentSplineLocation, CurrentSplineRotation);
}

As you can see the logic is not that scary. We are simply taking the delta time by the lenght of the spline and retrieving that points location and rotation.

This is then applied to our mesh and we prevent the pitch from being corrected so our elevator / platform does not tip as it’s traversing along the spline.

Lastly let’s take a look at the function OnEndMovementTimeline() which is called at the .. you guessed it … end of our timeline.

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spline", meta=(EditCondition = "!bRestartOnEndTimeline"))
bool bReverseOnEndTimeline;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spline", meta=(EditCondition = "!bReverseOnEndTimeline"))
bool bRestartOnEndTimeline;
void AMovingSplineActor::OnEndMovementTimeline()
{
  if(bReverseOnEndTimeline)
  {
    MovementTimeline.Reverse();
  }
  else if(bRestartOnEndTimeline)
  {
    MovementTimeline.PlayFromStart();
  }
}

This function is a great spot to put in logic for things like restarting the movement or reversing the movement by injecting some simple properties into our new object.

For further reading about these topics take a look at the following links:

Unreal Engine C++ Fundamentals – Using Spline Components

By Development, Tutorial, Unreal No Comments

Hey guys,

Today we are going to take a look how to use Spline Components and modify their various properties to allow us to specify custom materials as well as determine how meshes are attached to the spline.

The project files for this video & article can be found on our GitHub page.

Splines splines splines !

So why do we care about splines ? Well splines allow us to construct various objects within our world by providing us with easily extensible and modifiable joined sets of meshes.

Splines can also provide support for navigation by being used as a path for something or someone to move along. In our case though we are going to use it for prop building.

Let’s dive into how to initialize a Spline Component.

#include "Components/SplineComponent.h"

UPROPERTY(VisibleAnywhere, Category = "Spline")
USplineComponent* SplineComponent;

Inside our constructor we simply instantiate it.

// Sets default values
ASplineActor::ASplineActor()
{
 	// 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;

  SplineComponent = CreateDefaultSubobject<USplineComponent>("Spline");
  if(SplineComponent)
  {
    SetRootComponent(SplineComponent);	
  }	
}

With our spline component setup we can proceed with attaching Spline Mesh Components by iterating over the available spline points.

To do this we want to override the OnConstruction method in order to allow us to specify the Spline Mesh Components during the process of modifying our spline within the editor.

void ASplineActor::OnConstruction(const FTransform& Transform)
{	
  Super::OnConstruction(Transform);

  if(SplineComponent && SplineMeshMap.Num() > 0)
  {
    // lookup all pertinent values
    FSplineMeshDetails* StartMeshDetails = nullptr;
    if(SplineMeshMap.Contains(ESplineMeshType::START))
    {
      StartMeshDetails = SplineMeshMap.Find(ESplineMeshType::START);	
    }

    FSplineMeshDetails* EndMeshDetails = nullptr;
    if(SplineMeshMap.Contains(ESplineMeshType::END))
    {
      EndMeshDetails = SplineMeshMap.Find(ESplineMeshType::END);
    }

    FSplineMeshDetails* DefaultMeshDetails = nullptr;
    if(SplineMeshMap.Contains(ESplineMeshType::DEFAULT))
    {
      DefaultMeshDetails = SplineMeshMap.Find(ESplineMeshType::DEFAULT);	
    }
    else
    {
      // exit if we don't have a default mesh to work with
      return;
    }
    
    const int32 SplinePoints = SplineComponent->GetNumberOfSplinePoints();

    for(int SplineCount = 0; SplineCount < (SplinePoints - 1); SplineCount++)
    {
      USplineMeshComponent *SplineMesh = NewObject<USplineMeshComponent>(this, USplineMeshComponent::StaticClass());

      UStaticMesh* StaticMesh = DefaultMeshDetails->Mesh;
      UMaterialInterface* Material = nullptr;
      ESplineMeshAxis::Type ForwardAxis = DefaultMeshDetails->ForwardAxis;
  
      // start mesh
      if(StartMeshDetails && StartMeshDetails->Mesh && SplineCount == 0)
      {
        StaticMesh = StartMeshDetails->Mesh;
        ForwardAxis = StartMeshDetails->ForwardAxis;
        
        if(StartMeshDetails->DefaultMaterial)
        {
          Material = StartMeshDetails->DefaultMaterial;
        }					
      }
      else if(EndMeshDetails && EndMeshDetails->Mesh && SplinePoints > 2 && SplineCount == (SplinePoints - 2))
      {
        // end mesh
        StaticMesh = EndMeshDetails->Mesh;
        ForwardAxis = EndMeshDetails->ForwardAxis;
        
        if(EndMeshDetails->DefaultMaterial)
        {
          Material = EndMeshDetails->DefaultMaterial;
        }	
      }
      else
      {
        // default assignment - middle mesh
        if(DefaultMeshDetails->AlternativeMaterial && SplineCount > 0 && SplineCount % 2 == 0)
        {
          Material = DefaultMeshDetails->AlternativeMaterial;
        }
        else if(DefaultMeshDetails->DefaultMaterial)
        {
          Material = DefaultMeshDetails->DefaultMaterial;
        }			
      }

      // update mesh details
      SplineMesh->SetStaticMesh(StaticMesh);
      SplineMesh->SetForwardAxis(ForwardAxis, true);
      SplineMesh->SetMaterial(0, Material);
      
  
      // initialize the object
      SplineMesh->RegisterComponentWithWorld(GetWorld());
  
      SplineMesh->CreationMethod = EComponentCreationMethod::UserConstructionScript;
      SplineMesh->SetMobility(EComponentMobility::Movable);
  
      SplineMesh->AttachToComponent(SplineComponent, FAttachmentTransformRules::KeepRelativeTransform);
  
      // define the positions of the points and tangents
      const FVector StartPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
      const FVector StartTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
      const FVector EndPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
      const FVector EndTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
      SplineMesh->SetStartAndEnd(StartPoint, StartTangent, EndPoint, EndTangent, true);
  
      // query physics
      SplineMesh->SetCollisionEnabled(ECollisionEnabled::Type::QueryAndPhysics);
    }
  }
}

WHOA that’s a lot of stuff , slow down !

That was a whole pile of code so let’s break it up bit by bit.

void ASplineActor::OnConstruction(const FTransform& Transform)
{	
  Super::OnConstruction(Transform);
}

Here we end up overwriting the OnConstruction method, ensuring we call the parent’s OnConstruction method before we proceed with any of our code. This method is going to be called when our blueprint is being modified in the editor or manipulated in the level.

To provide greater flexibility in our spline actor we introduce a struct and an enum to let us handle multiple different types of meshes / materials and forward axis definitions for our spline mesh components.

UENUM(BlueprintType)
enum class ESplineMeshType: uint8 {
  DEFAULT		UMETA(DisplayName = "Default Mesh"),
  START		UMETA(DisplayName = "Starting Mesh"),
  END			UMETA(DisplayName = "EndingMesh"),
};

USTRUCT(BlueprintType)
struct FSplineMeshDetails : public FTableRowBase
{
  GENERATED_BODY()
  
  UPROPERTY(EditAnywhere, BlueprintReadOnly)
  UStaticMesh* Mesh;

  UPROPERTY(EditAnywhere, BlueprintReadOnly)
  TEnumAsByte<ESplineMeshAxis::Type> ForwardAxis;
  
  UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
  class UMaterialInterface* DefaultMaterial;

  UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
  class UMaterialInterface* AlternativeMaterial;

  FSplineMeshDetails() : ForwardAxis(ESplineMeshAxis::Type::X)
  {
  }
};

We convert this enum / struct combination into a map that will allow us to configure multiple versions of these definitions.

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Spline")
TMap<ESplineMeshType, FSplineMeshDetails> SplineMeshMap;

From which we can load the appropriate details and make any necessary checks to determine if our map and the contents are valid.

if(SplineComponent && SplineMeshMap.Num() > 0)
{
  // lookup all pertinent values
  FSplineMeshDetails* StartMeshDetails = nullptr;
  if(SplineMeshMap.Contains(ESplineMeshType::START))
  {
    StartMeshDetails = SplineMeshMap.Find(ESplineMeshType::START);	
  }

  FSplineMeshDetails* EndMeshDetails = nullptr;
  if(SplineMeshMap.Contains(ESplineMeshType::END))
  {
    EndMeshDetails = SplineMeshMap.Find(ESplineMeshType::END);
  }

  FSplineMeshDetails* DefaultMeshDetails = nullptr;
  if(SplineMeshMap.Contains(ESplineMeshType::DEFAULT))
  {
    DefaultMeshDetails = SplineMeshMap.Find(ESplineMeshType::DEFAULT);	
  }
  else
  {
    // exit if we don't have a default mesh to work with
    return;
  }
}

The spline is made up of spline points that which allow for transformation of the spline component using the level editor but it also provides you with information that we require for our construction script as each spline point can change.

We need to capture these changes by iterating over the spline points and then update your spline component accordingly.

const int32 SplinePoints = SplineComponent->GetNumberOfSplinePoints();

for(int SplineCount = 0; SplineCount < (SplinePoints - 1); SplineCount++)
{
}

In order to render any meshes along our spline we need to create spline mesh components during our OnConstruction call.

USplineMeshComponent *SplineMesh = NewObject<USplineMeshComponent>(this, USplineMeshComponent::StaticClass());

With the object instantiated we can determine which of the meshes we want to assign.

Do we want a mesh at the start of our spline ? Do we want one at the end ? Do we want to apply some in the middle ? This type of logic fits extremely well with assets like pipes, fences, ladders and various other common props that need to be adjusted to fit their environment as well as provide quick work flow.

Here we set some environments to hold our mesh details. We use a UMaterialInterface in order to provide flexibility of using master or instance materials.

UStaticMesh* StaticMesh = DefaultMeshDetails->Mesh;
UMaterialInterface* Material = nullptr;
ESplineMeshAxis::Type ForwardAxis = DefaultMeshDetails->ForwardAxis;

In order for us to determine which part of our spline we are working on we can simply keep count of the spline points and react accordingly.

// start mesh
if(StartMeshDetails && StartMeshDetails->Mesh && SplineCount == 0)
{
  StaticMesh = StartMeshDetails->Mesh;
  ForwardAxis = StartMeshDetails->ForwardAxis;
  
  if(StartMeshDetails->DefaultMaterial)
  {
    Material = StartMeshDetails->DefaultMaterial;
  }					
}
else if(EndMeshDetails && EndMeshDetails->Mesh && SplinePoints > 2 && SplineCount == (SplinePoints - 2))
{
  // end mesh
  StaticMesh = EndMeshDetails->Mesh;
  ForwardAxis = EndMeshDetails->ForwardAxis;
  
  if(EndMeshDetails->DefaultMaterial)
  {
    Material = EndMeshDetails->DefaultMaterial;
  }	
}
else
{
  // default assignment - middle
  if(DefaultMeshDetails->AlternativeMaterial && SplineCount > 0 && SplineCount % 2 == 0)
  {
    Material = DefaultMeshDetails->AlternativeMaterial;
  }
  else if(DefaultMeshDetails->DefaultMaterial)
  {
    Material = DefaultMeshDetails->DefaultMaterial;
  }			
}

With our assignment determined we register which mesh / material and forward axis we are going to be assigning to our spline mesh component.

// update mesh details
SplineMesh->SetStaticMesh(StaticMesh);
SplineMesh->SetForwardAxis(ForwardAxis, true);
SplineMesh->SetMaterial(0, Material);

Before the spline component is available to our editor and blueprint we need to register it with the world it’s going to be present in.

// initialize the object
SplineMesh->RegisterComponentWithWorld(GetWorld());

We also tell this component that it needs to be processed at construction time.

SplineMesh->CreationMethod = EComponentCreationMethod::UserConstructionScript;

We let the component know it can be moved and adjusted. This can also be static for specific needs.

SplineMesh->SetMobility(EComponentMobility::Movable);

Then finally we attach the spline mesh component to our spline component by keeping it’s transformation relative to the spline component.

SplineMesh->AttachToComponent(SplineComponent, FAttachmentTransformRules::KeepRelativeTransform);

Lastly we need to define the starting / ending points as well as the tangents that control the curvature of the spline points along said spline.

This allows the spline to determine the position of our new mesh along that spline so everything appears one after the other as we extend our spline points.

// define the positions of the points and tangents
const FVector StartPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
const FVector StartTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
const FVector EndPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
const FVector EndTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
SplineMesh->SetStartAndEnd(StartPoint, StartTangent, EndPoint, EndTangent, true);

Lastly we specify the collision response for each mesh component. This allows us to specify and register different overlap / hit events for each part of the spline providing a lot of flexbility.

SplineMesh->SetCollisionEnabled(ECollisionEnabled::Type::QueryAndPhysics);

See not too bad at all and at the end of this little implementation you should have a fairly modular spline actor for various uses.

For further reading about these topics take a look at the following links: