Make an object follow a Spline with a lateral offset

Hello!

In my game, I want the enemies to follow a path defined by a Spline from the Splines Package but I want them to have a slight left or right offset when following them, so some will follow the Spline line, others will follow it but 0.1 meters to right, others 0.2 meters to the left and so on.

As far as I can tell, the SplineAnimate script doesn’t have an option to define an offset and only supports objects following the lines, so I have implemented my own script:

public class FollowSpline : MonoBehaviour
{
    [SerializeField]
    private SplineContainer splineContainer;

    public float Speed = 1;
    public float HorizontalOffset = 0;

    private void Update()
    {
        var localPoint = splineContainer.transform.InverseTransformPoint(transform.position);
        // High resolution and iterations to see if it improves the situation.
        SplineUtility.GetNearestPoint(splineContainer.Spline, localPoint, out var nearest, out var ratio, 10, 10);

        var tangent = SplineUtility.EvaluateTangent(splineContainer.Spline, ratio);

        var rotation = Quaternion.LookRotation(tangent);
        transform.rotation = rotation;

        var globalNearest = splineContainer.transform.TransformPoint(nearest);
        var perpendicular = Vector3.Cross(tangent, Vector3.up);
        var position = globalNearest + (perpendicular.normalized * HorizontalOffset);
        transform.position = position;

        transform.Translate(Vector3.forward * Speed * Time.deltaTime, Space.Self);
    }
}

This code works well if the object is moving at a relatively high speed (10 m/s for example). If the object moves at a slower speed, say 2 m/s, it gets stuck when going around a turn in the Spline path, staying still on the same spot. My guess is that the object is not moving fast enough so the GetNearestPoint evaluates to roughly the same point in the Spline every time, due to the curvature of the line.

I have tried increasing the resolution and iteration values, but with no luck. Can you folks see a way around this?

Thanks in advance!

That’s actually an excellent and likely observation. Should be pretty easy to prove / disprove through debugging.

As for fixing it (if that is truly the issue), you need to not “feed back” your positioning information that way.

One alternate way is to have the agent itself stay on the spline, following it normally, but be simply an invisible GameObject with your follow script. This agent would be used to “find closest on spline” and it would then be placed on the spline continuously.

Then you would have the visible portion of the agent offset by the sidestep you want. The visuals could be a child of the agent and have a reference dragged right into the main script on the invisible root object. You might want to put colliders in there too, if it’s that kind of game.

I did consider that, the problem with that approach and why I’m trying to make it work this way is that the agent will move at different speeds while making a turn than when going on a straight line, since the angular velocity of the invisible and the visible part of the agent would be the same when turning, resulting on a higher or lower speed for the visible part depending if it is an outer turn or an inner turn, respectively.

@Kurt-Dekker Your mention to not “feeding back” the position in that way gave me the idea to actually solve it. Now I demand that two consecutive detected nearest points must be at least some distance apart in order to set the position taking the spline as reference. The code looks as follows:

public class FollowSpline : MonoBehaviour
{
    [SerializeField]
    private SplineContainer splineContainer;

    public float Speed = 1;
    public float HorizontalOffset = 0;

    private float3 previousNearest = float3.zero;

    private void Update()
    {
        var localPoint = splineContainer.transform.InverseTransformPoint(transform.position);

        SplineUtility.GetNearestPoint(splineContainer.Spline, localPoint, out var nearest, out var ratio, 10, 4);

        var tangent = SplineUtility.EvaluateTangent(splineContainer.Spline, ratio);

        var rotation = Quaternion.LookRotation(tangent);
        transform.rotation = rotation;

        if (Vector3.SqrMagnitude(previousNearest - nearest) >= 0.0001)
        {
            var globalNearest = splineContainer.transform.TransformPoint(nearest);
            var perpendicular = Vector3.Cross(tangent, Vector3.up);
            var position = globalNearest + (perpendicular.normalized * HorizontalOffset);
            transform.position = position;

            previousNearest = nearest;
        } else
        {
            Debug.LogWarning("Same nearest point twice in a row! Previous: " + previousNearest + ", New: " + nearest);
        }

        transform.Translate(Vector3.forward * Speed * Time.deltaTime, Space.Self);
    }
}

I will remove the LogWarning once I have finished tweaking all the values in the script, but your suggestion really gave me a push. Thanks!