How To Make An Arbitrarily Long Spline Mesh In Unreal Engine
The Problem
Unreal Engine has a component called USplineMeshComponent
. It does something really cool:
deforming a mesh along a spline, so you can take e.g. a straight section of pipe and make it any
length, make the ends point in any direction, distort the shape by changing the tangents or the
roll, etc. Unfortunately, spline mesh components have one hard-coded limitation: the spline can only
have two control points.
In a lot of cases, that's fine. But what if you want an arbitrarily long spline mesh? I couldn't find any good answers or tutorials online, so I ended up making my own. I had to solve the following problems:
- How do I transfer data from a
USplineComponent
, which has arbitrary length, to an array ofUSplineMeshComponent
s? - How do I create
USplineMeshComponent
s dynamically? - How do I destroy
USplineComponent
s that I don't need any more? - How do I re-use the spline mesh components, if possible?
The AChainedSplineMesh
Class
Let's start with a class declaration, and then fill it out to solve each problem one by one. Here's
the final, commented AChainedSplineMesh
class:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ChainedSplineMesh.generated.h"
class USplineComponent;
class USplineMeshComponent;
UCLASS()
class CHAINEDSPLINEMESH_API AChainedSplineMesh : public AActor
{
GENERATED_BODY()
public:
// Constructor: used to set up normal components
AChainedSplineMesh();
// Post-construction hook: used to set up our dynamic components
virtual void OnConstruction(const FTransform& Transform) override;
protected:
// The spline we'll be using to shape the chained spline mesh. This is just an ordinary component.
UPROPERTY(EditDefaultsOnly)
TObjectPtr<USplineComponent> Spline;
// The mesh we'll be using as the spline mesh's... mesh. Note that this is just a `UStaticMesh`,
// NOT a `UStaticMeshComponent`.
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> Mesh;
private:
// The dynamically-created spline mesh components will be held here, so we can manage their
// lifecycles appropriately.
UPROPERTY()
TArray<TObjectPtr<USplineMeshComponent>> SplineMeshes;
// Called every time we need to rebuild the spline mesh chain (e.g. when the root spline changes).
void PopulateSplineMeshes();
};
I'm going to fill in this class piece by piece to arrive at the final result. Here's the full source code if you're in a hurry.
Querying The USplineComponent
This part is actually easy. All we need to do is iterate through each segment of the spline and obtain the following information:
- The position, in world space, of the segment start and end points
- The tangent vector, representing roughly where the spline is "pointing towards" at this point
Splines provide easy access to this information through the GetNumberOfSplineSegments()
and
GetSplinePointAt()
methods. So our PopulateSplineMeshes()
method will look, overall, something
like this:
void AChainedSplineMesh::PopulateSplineMeshes()
{
for (int32 SegmentIndex = 0; SegmentIndex < Spline->GetNumberOfSplineSegments(); ++SegmentIndex)
{
const FSplinePoint Start = Spline->GetSplinePointAt(SegmentIndex, ESplineCoordinateSpace::World);
const FSplinePoint End = Spline->GetSplinePointAt(SegmentIndex + 1, ESplineCoordinateSpace::World);
// Add or change our spline mesh components
}
// If we lost any segments from the root spline, we need to clean up the associated spline mesh
// components.
for (int32 SegmentIndex = Spline->GetNumberOfSplineSegments(); SegmentIndex < SplineMeshes.Num(); ++SegmentIndex)
{
// Clean up any extra spline mesh components, if we removed segments from the spline
}
// Shrink the component array down to the correct size, so that UE will garbage collect the
// components we just destroyed.
SplineMeshes.SetNum(Spline->GetNumberOfSplineSegments());
}
Creating Dynamic Spline Mesh Components
So, the first hurdle is that spline mesh components are, well, components, and we need to make an
unknown amount of them depending on how many segments our spline has. UE generally likes the number
of components to be static information; you declare one member variable per component, and then
populate them with
CreateDefaultSubobject<UTheTypeOfComponentYouWantToMake>(TEXT("TheComponentName"))
. Then you get a
nice hierarchical view in the editor when you examine a derived Blueprint class, or in the
inspector.
Now, there is a way to create components dynamically, but it's not an especially well-documented way (I'm not even 100% certain I've done it the "intended" way, to be honest). What I do know is that, as of UE 5.4, the following steps will successfully create and register a dynamic component:
- Create the component using
NewObject<T>()
, notCreateDefaultSubobject<T>()
.NewObject<T>()
is used to instantiate many different kinds of objects (components being one of them), whereasCreateDefaultSubobject<T>()
is used specifically to create CDO's (class default objects), which are not something I want to talk about today, but the important part is it only works in constructors. - If you have a scene component, attach it to another component as normal with
SetupAttachment()
. - Call
RegisterComponent()
. This method does several things, and I am honestly not 100% clear what its purpose is, but leaving it out is a great way to have nothing work at all.
So, we can update our loop from the previous section with the following code:
void AChainedSplineMesh::PopulateSplineMeshes()
{
for (int32 SegmentIndex = 0; SegmentIndex < Spline->GetNumberOfSplineSegments(); ++SegmentIndex)
{
const FSplinePoint Start = Spline->GetSplinePointAt(SegmentIndex, ESplineCoordinateSpace::World);
const FSplinePoint End = Spline->GetSplinePointAt(SegmentIndex + 1, ESplineCoordinateSpace::World);
// Change any existing spline mesh components
// If we reach this point, we must have added some new points to the spline mesh chain, so we
need to create, attach, register, and track a new segment.
TObjectPtr<USplineMeshComponent> SplineMesh = NewObject<USplineMeshComponent>(this);
SplineMesh->SetupAttachment(GetRootComponent());
SplineMesh->RegisterComponent();
SplineMeshes.Add(SplineMesh);
// Bring the position and tangent data from the root spline, and tell the spline mesh which
// asset it's supposed to use.
SplineMesh->SetStartAndEnd(Start.Position, Start.LeaveTangent, End.Position, End.ArriveTangent);
SplineMesh->SetStaticMesh(Mesh.LoadSynchronous());
}
// If we lost any segments from the root spline, we need to clean up the associated spline mesh
// components.
for (int32 SegmentIndex = Spline->GetNumberOfSplineSegments(); SegmentIndex < SplineMeshes.Num(); ++SegmentIndex)
{
// Clean up any extra spline mesh components, if we removed segments from the spline
}
// Shrink the component array down to the correct size, so that UE will garbage collect the
// components we just destroyed.
SplineMeshes.SetNum(Spline->GetNumberOfSplineSegments());
}
Destroying Dynamic Spline Mesh Components
This part is, fortunately, also very easy. All you have to do is call DestroyComponent()
in the
second loop. We can also wrap this part in #if WITH_EDITOR
, because we should not be changing the
spline in a non-editor build.
void AChainedSplineMesh::PopulateSplineMeshes()
{
// ...
#if WITH_EDITOR
// If we lost any segments from the root spline, we need to clean up the associated spline mesh
// components.
for (int32 SegmentIndex = Spline->GetNumberOfSplineSegments(); SegmentIndex < SplineMeshes.Num(); ++SegmentIndex)
{
SplineMeshes[SegmentIndex]->DestroyComponent();
}
// Shrink the component array down to the correct size, so that UE will garbage collect the
// components we just destroyed.
SplineMeshes.SetNum(Spline->GetNumberOfSplineSegments());
#endif
}
Updating Existing Spline Mesh Components
If we're actively editing a spline, then chances are we will be running PopulateSplineMeshes()
a
lot. Ideally, we should avoid the performance penalties (and memory churn) of destroying and
re-creating the whole spline mesh component chain every time we edit a spline point. So, instead of
that, we can add a step to our first loop that reconfigures an existing spline mesh component, if
there is one in that segment index.
void AChainedSplineMesh::PopulateSplineMeshes()
{
for (int32 SegmentIndex = 0; SegmentIndex < Spline->GetNumberOfSplineSegments(); ++SegmentIndex)
{
const FSplinePoint Start = Spline->GetSplinePointAt(SegmentIndex, ESplineCoordinateSpace::World);
const FSplinePoint End = Spline->GetSplinePointAt(SegmentIndex + 1, ESplineCoordinateSpace::World);
#if WITH_EDITOR
// When we change the spline, we can probably re-use most of the spline mesh segments. Just
// bring the new data from the root spline and move on to the next segment.
if (SegmentIndex < SplineMeshes.Num())
{
SplineMeshes[SegmentIndex]->SetStartAndEnd(Start.Position, Start.LeaveTangent, End.Position, End.ArriveTangent);
continue;
}
#endif
// ...
}
// ...
}
We also used #if WITH_EDITOR
again, for the same reason: we will not be changing any spline
segments outside of an editor build.
Finishing Up
And that's the tough part done! We only need a couple of other things: the constructor and the post-construction hook (they're different things, I promise).
The Constructor
Nothing complicated here, it's a very standard UE actor constructor.
AChainedSplineMesh::AChainedSplineMesh()
{
// We don't need tick, so we save the engine the trouble
PrimaryActorTick.bCanEverTick = false;
// The spline component is just a reference, not something we want as the root of all our other
// components, so we just make an empty scene component to serve as the root.
const auto RootSceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootSceneComponent"));
RootSceneComponent->Mobility = EComponentMobility::Static;
SetRootComponent(RootSceneComponent);
// Create the spline component, which we will reference when we build the spline mesh chain.
Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
Spline->SetupAttachment(RootSceneComponent);
}
The Post-Construction Hook
This hook is called right after the "construct" Blueprint event. It can be used to run code outside
of the fragile environment of the constructor, but still before BeginPlay()
. Importantly for us,
this includes at edit time: every time we move, rotate, scale, undo, redo, etc., this code will run.
void AChainedSplineMesh::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
// Create the initial set of spline meshes
PopulateSplineMeshes();
// Only do this code if we're an editor build; otherwise, there's no way the spline can change,
// and so no reason to monitor it for changes
#if WITH_EDITOR
// Remove any previously registered callbacks, so they don't stack up
Spline->OnDeselectedInEditor.RemoveAll(this);
// Add a callback so that, when we click off the spline in the editor, the spline mesh chain is
// rebuilt. I wish there was a better hook for this, but splines only give us this one. You could
// probably subclass `USplineComponent` to add better hooks, if you like.
Spline->OnDeselectedInEditor.AddLambda([this](TObjectPtr<USplineComponent>) { PopulateSplineMeshes(); });
#endif
}
Done!
I'll get some pictures in here soon, but really, you should test this code out with your own meshes. You should be able to manipulate the root spline as normal (this can be confusing; click the actor once to select it, then click a control point to select the spline, then right click somewhere on the spline to add or delete points. I know, I hate it too). Then as soon as you de-select the actor, the spline mesh chain should update, deforming the relevant parts of the mesh or adding and removing segments as you add and remove control points.
If you made it all the way to the end, I hope you found this useful! Shoot me an email if you have any ideas to make this solution better. Thanks for reading!
Addendum: Full Source Code
ChainedSplineMesh.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ChainedSplineMesh.generated.h"
class USplineComponent;
class USplineMeshComponent;
UCLASS()
class CHAINEDSPLINEMESH_API AChainedSplineMesh : public AActor
{
GENERATED_BODY()
public:
// Constructor: used to set up normal components
AChainedSplineMesh();
// Post-construction hook: used to set up our dynamic components
virtual void OnConstruction(const FTransform& Transform) override;
protected:
// The spline we'll be using to shape the chained spline mesh. This is just an ordinary component.
UPROPERTY(EditDefaultsOnly)
TObjectPtr<USplineComponent> Spline;
// The mesh we'll be using as the spline mesh's... mesh. Note that this is just a `UStaticMesh`,
// NOT a `UStaticMeshComponent`.
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> Mesh;
private:
// The dynamically-created spline mesh components will be held here, so we can manage their
// lifecycles appropriately.
UPROPERTY()
TArray<TObjectPtr<USplineMeshComponent>> SplineMeshes;
// Called every time we need to rebuild the spline mesh chain (e.g. when the root spline changes).
void PopulateSplineMeshes();
};
ChainedSplineMesh.cpp
#include "ChainedSplineMesh.h"
#include "Components/SplineComponent.h"
#include "Components/SplineMeshComponent.h"
AChainedSplineMesh::AChainedSplineMesh()
{
// We don't need tick, so we save the engine the trouble
PrimaryActorTick.bCanEverTick = false;
// The spline component is just a reference, not something we want as the root of all our other
// components, so we just make an empty scene component to serve as the root.
const auto RootSceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootSceneComponent"));
RootSceneComponent->Mobility = EComponentMobility::Static;
SetRootComponent(RootSceneComponent);
// Create the spline component, which we will reference when we build the spline mesh chain.
Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
Spline->SetupAttachment(RootSceneComponent);
}
void AChainedSplineMesh::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
// Create the initial set of spline meshes
PopulateSplineMeshes();
// Only do this code if we're an editor build; otherwise, there's no way the spline can change,
// and so no reason to monitor it for changes
#if WITH_EDITOR
// Remove any previously registered callbacks, so they don't stack up
Spline->OnDeselectedInEditor.RemoveAll(this);
// Add a callback so that, when we click off the spline in the editor, the spline mesh chain is
// rebuilt. I wish there was a better hook for this, but splines only give us this one. You could
// probably subclass `USplineComponent` to add better hooks, if you like.
Spline->OnDeselectedInEditor.AddLambda([this](TObjectPtr<USplineComponent>) { PopulateSplineMeshes(); });
#endif
}
void AChainedSplineMesh::PopulateSplineMeshes()
{
for (int32 SegmentIndex = 0; SegmentIndex < Spline->GetNumberOfSplineSegments(); ++SegmentIndex)
{
const FSplinePoint Start = Spline->GetSplinePointAt(SegmentIndex, ESplineCoordinateSpace::World);
const FSplinePoint End = Spline->GetSplinePointAt(SegmentIndex + 1, ESplineCoordinateSpace::World);
#if WITH_EDITOR
// When we change the spline, we can probably re-use most of the spline mesh segments. Just
// bring the new data from the root spline and move on to the next segment.
if (SegmentIndex < SplineMeshes.Num())
{
SplineMeshes[SegmentIndex]->SetStartAndEnd(Start.Position, Start.LeaveTangent, End.Position, End.ArriveTangent);
continue;
}
#endif
// If we reach this point, we must have added some new points to the spline mesh chain, so we
need to create, attach, register, and track a new segment.
TObjectPtr<USplineMeshComponent> SplineMesh = NewObject<USplineMeshComponent>(this);
SplineMesh->SetupAttachment(GetRootComponent());
SplineMesh->RegisterComponent();
SplineMeshes.Add(SplineMesh);
// Bring the position and tangent data from the root spline, and tell the spline mesh which mesh
// it's supposed to use.
SplineMesh->SetStartAndEnd(Start.Position, Start.LeaveTangent, End.Position, End.ArriveTangent);
SplineMesh->SetStaticMesh(Mesh.LoadSynchronous());
}
#if WITH_EDITOR
// If we lost any segments from the root spline, we need to clean up the associated spline mesh
// components.
for (int32 SegmentIndex = Spline->GetNumberOfSplineSegments(); SegmentIndex < SplineMeshes.Num(); ++SegmentIndex)
{
SplineMeshes[SegmentIndex]->DestroyComponent();
}
// Shrink the component array down to the correct size, so that UE will garbage collect the
// components we just destroyed.
SplineMeshes.SetNum(Spline->GetNumberOfSplineSegments());
#endif
}