Closest position on a Spline to a Player

Except SplineMeshComponent doesn’t actually expose the underlying SplineComponent needed to call that function.
Why? Beats me, but that’s the state.

You can also just add some movement input to make the character keep walking towards the location you want. In Tick, figure out how close you are, and which way you need to walk/turn to get where you need to go, and call AddMovementInput on the CharacterMovementComponent to move/turn appropriately.

1 Like

Reading the code, it doesn’t actually use a SplineComponent; instead it implements a single spline segment with entry and exit tangents, manually.
So, the SplineMeshComponent is only really useful to deform a single static mesh, and then only when that mesh is sufficiently tesselated to do well with such a deformer.

The common case of “place a mesh (or variety of meshes) along a spline” isn’t actually natively built-in; you’ll have to use the SplineComponent on an Actor with a InstancedStaticMesh component, and add instances along the spline in the construction script.

In general, it seems there are two parameters that let you index along the SplineComponent, one is InputKey which is really a floating point index into the points of the spline – values in [0.0 .. 1.0) indexes between first and second point, [1.0 .. 2.0) indexes between second and third point, and so on. There’s also DistanceAlongSpline which indexes based on measured world-space distance. Most of the Spline value lookup functions (get transform, get direction, and so on) can take one or the other (or both.)

1 Like

Hey guys!

I stumbled across the blueprint solution by @DeathKwonDo, but I think there might be an issue regarding finding the closest point on the mesh when given a spline and a point like this:


The closest point is in between “start” and “middle”, but the blueprint solution would decide for “end” being closer than “start” at its first iteration. It would then go on and only explore the segment “middle” - “end” which would exclude the optimal solution.

Therefore I have created a C++ node with a multistart approach: First testing some sample points along the spline and then resort to the binary search solution presented by @DeathKwonDo. Still not an optimal solution, since you can still miss the closest point with too few sample points, but a bit more robust. Here is the code:

// SplineComponentUtils.h

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Components/SplineComponent.h"
#include "SplineComponentUtils.generated.h"

/**
 * 
 */
UCLASS()
class UNREALENGINETOOLS_API USplineComponentUtils : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
	

public:

	UFUNCTION(BlueprintCallable, Category = "Spline Utils")
	/**
	 * Projects the specified location to the specified spline and returns the distance on the spline that is the closest point.
 	 * Multistart approach with constant amount `SampleCount` of sample points. Points are evaluated, point with closest distance is chosen. 
	 * Binary Search with depth `MaxIterationCount` is then started with adjacent points.  
	*/
	static float ProjectLocationToSplineDistance(const USplineComponent* Spline, const FVector& Location, const ESplineCoordinateSpace::Type CoordinateSpace, const int SampleCount = 20, const int MaxIterationCount = 10);
};
// SplineComponentUtils.cpp


#include "CustomNodes/Utilities/SplineComponentUtils.h"


/**
 * Projects the specified location to the specified spline and returns the distance on the spline that is the closest point.
 * Multistart approach with constant amount `SampleCount` of sample points. Points are evaluated, point with closest distance is chosen. 
 * Binary Search with depth `MaxIterationCount` is then started with adjacent points.  
*/
float USplineComponentUtils::ProjectLocationToSplineDistance(const USplineComponent* Spline, const FVector& Location, const ESplineCoordinateSpace::Type CoordinateSpace, const int SampleCount, const int MaxIterationCount)
{
    const float Distance = Spline->GetSplineLength();

    // Multistart
    const float SampleDistanceDelta = Distance / SampleCount;
    int ClosestSampleIndex = 0;
    float ClosestDistance = Distance;
    for (int i = 0; i < SampleCount + 1; i++)
    {
        const float SampleDistance = SampleDistanceDelta * i;
        const FVector SamplePoint = Spline->GetLocationAtDistanceAlongSpline(SampleDistance, CoordinateSpace);
        const float SamplePointDistance = FVector::Distance(Location, SamplePoint);
        if (SamplePointDistance < ClosestDistance)
        {
            ClosestSampleIndex = i;
            ClosestDistance = SamplePointDistance;
        }
    }

    const int StartIndex = FMath::Max(ClosestSampleIndex - 1, 0);
    const int EndIndex = FMath::Min(ClosestSampleIndex + 1, SampleCount + 1);

    if (StartIndex == EndIndex)
        return SampleDistanceDelta * StartIndex;

    // Binary Search

    float StartDistance = SampleDistanceDelta * StartIndex;
    float EndDistance = SampleDistanceDelta * EndIndex;

    FVector StartPoint = Spline->GetLocationAtDistanceAlongSpline(StartDistance, CoordinateSpace);
    FVector EndPoint = Spline->GetLocationAtDistanceAlongSpline(EndDistance, CoordinateSpace);

    for (int j = 0; j < MaxIterationCount; j++)
    {
        if (FVector::Distance(Location, StartPoint) < FVector::Distance(Location, EndPoint))
            EndDistance = StartDistance + ((EndDistance - StartDistance) / 2);
        else
            StartDistance = StartDistance + ((EndDistance - StartDistance) / 2);

        StartPoint = Spline->GetLocationAtDistanceAlongSpline(StartDistance, CoordinateSpace);
        EndPoint = Spline->GetLocationAtDistanceAlongSpline(EndDistance, CoordinateSpace);
    }
    
    if (FVector::Distance(Location, StartPoint) < FVector::Distance(Location, EndPoint))
        return StartDistance;
    else
        return EndDistance;
}

Cheers!