Unexpected results from GetLocationAtDistanceAlongSpline() when spline changes height

We’re setting up a “rail grinding” skateboard behavior which works as expected when the spline on which the player is “grinding” is level, but which returns anomalous values when the spline changes elevation, specifically when it crests a hill - ascending and then descending again.

Here’s the relevant snippet from our movement component’s PhysSkateboardGrinding():

What we’ve observed:

  • Incoming InVelocity contains expected value
  • SpeedAlongTangent contains reasonable & expected values
  • DeltaDistance appears correct
  • FutureDistanceAlongSpline seems to represent an expected new position
  • Clamping doesn’t appear to be the problem as we can get stuck far from the spline’s end.
  • NewPositionOnSpline, when the spline is changing elevation, seems to get “stuck” at certain points, returning roughly the same location the character already occupies even though FutureDistanceAlongSpline appears properly updated.

Are we doing something incorrect in the way we’re advancing along the spline?

`// Project the input velocity onto the spline tangent
const float SpeedAlongTangent{
static_cast(FVector::DotProduct(InVelocity, GrindData.CurrentGrindTangent))
};
GrindData.CurrentGrindSpeed = SpeedAlongTangent; // Write to the CurrentGrindSpeed member variable

// #TODO: TEST - The hypothesis to test here is whether instead of calculating delta by scaling the spline tangent
// on which we’re currently standing by the speed along the tangent, we can instead project ahead on the spline to
// find the next point we would be at based on our grind speed, and then calculate a delta to that point.
const float DeltaDistance{ GrindData.CurrentGrindSpeed * DeltaSeconds };
const float FutureDistanceAlongSpline{ GrindData.CurrentGrindPoint + DeltaDistance };

const float ClampedDistanceAlongSpline{ FMath::Clamp(FutureDistanceAlongSpline, 0.0f, GrindData.SplineLength) };
// #TODO: NewPositionOnSpline appears to be where we’re getting unexpected values when the spline crests a hill.
const FVector NewPositionOnSpline{
GrindData.GrindSpline->GetLocationAtDistanceAlongSpline(ClampedDistanceAlongSpline, ESplineCoordinateSpace::World)
};
constexpr float GrindClearanceBuffer{ 10.0f }; // Buffer to ensure character is above the rail
const float OffsetZ = CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() + GrindClearanceBuffer;
const FVector TargetLocation = NewPositionOnSpline + FVector(0, 0, OffsetZ);

#if ENABLE_DRAW_DEBUG
if (UHooliganSkateboardComponent::DrawSkateboardDebugInfo(this))
{
DrawDebugDirectionalArrow(World,
UpdatedComponent->GetComponentLocation(),
UpdatedComponent->GetComponentLocation() + InVelocity,
32.0f,
FColor::Blue,
false,
.1f);

const FString GrindSpeedText{
FString::Printf(TEXT(“CurrentGrindSpeed: %.2f, DeltaDistance: %.2f, FutureGrindPoint: %.2f”),
GrindData.CurrentGrindSpeed,
DeltaDistance,
FutureDistanceAlongSpline)
};
DrawDebugString(World,
UpdatedComponent->GetComponentLocation() + FVector(0, 0, 50.0f),
GrindSpeedText,
nullptr,
FColor::Emerald,
0.1f);

DrawDebugPoint(World, NewPositionOnSpline, 30.0f, FColor::Magenta, false, .1f);
DrawDebugPoint(World, TargetLocation, 30.0f, FColor::Green, false, .1f);
}
#endif

// Compute delta based on adjusted velocity
const FVector AdjustedVelocity{ GrindData.CurrentGrindTangent * SpeedAlongTangent };
FVector Delta{
bTestNewGrindMethod
? (TargetLocation - UpdatedComponent->GetComponentLocation())
: (DeltaSeconds * AdjustedVelocity)
};
// END TEST, with A|B switch in place to allow us to retain the old math until this gets worked out.`

Hi Kevin,

Sorry about the delay. I am looking into this now and should get back to you soon.

Best regards,

Vitor

Hi Kevin,

I tested the behavior of functions USplineComponent::Get*AtDistanceAlongSpline(), and they all seemed to return reasonable values. I have attached a tiny test project to this reply containing a spline tester blueprint which allows you to observe the behavior visually: just enter PIE, and a sphere with axes and tangent representations will follow along a spline with constant world-space speed. You can also migrate that blueprint to your project if you find it helpful.

Now, in the snippet you provided, some things called my attention:

A) You did not include how GrindData.CurrentGrindTangent is calculated, but note that methods USplineComponent::GetTangent*() return the tangent vectors used to define the spline, which can vary greatly in length depending on the local spline curvature and the distance between its control points. To get the direction along the spline at a given point, prefer USplineComponent::GetDirection*()

B) The calculation of SpeedAlongTangent projects a velocity onto the spline. Besides being affected by the length of GrindData.CurrentGrindTangent, note that this will also have the effect of killing any velocity component that is not aligned with that tangent vector (instead of turning the velocity to force following the tangent vector with unaltered scalar speed). Depending on the other calculations performed, this might dampen the resulting speed where the spline has greater curvature.

C) If you have very long spline segments between pairs of control points, try increasing the value of ReparamStepsPerSegment on the spline component to see if it improves the behavior.

Please let me know if this is helpful or if you still need further assistance.

Best regards,

Vitor

Hi Vitor,

Thank you so much! This I think was the crucial distinction - I had been treating the tangent vector as a unit vector, and would certainly be getting unexpected results. This insight seems to have been the key.

Thank you!

_Kevin