Unity curved path following with easing at constant speed



Download

The Problem

I have a character than needs to follow paths curved through a series of control points as it moves around the world. I also want to ease in and out to speed of movement as the path starts and ends. Sometimes in the middle of a path follow I need to change my mind and have some other path taken or perhaps change from path following to another state. This isn’t possible with iTween at constant speed unless you use PutOnPath, which doesn’t allow easing functions. I would also like to be able to move at a constant speed – either by ignoring the length of individual path elements (and thereby specifying at time fraction for the whole path) or by having a maximum speed my object can travel at.

Solution

Building on the great work done by Andeeee and Renaud, I’ve made a path following class that enables you to specify from 2 to N points and have it build a curved path (if it has more than 2 points!), you can then follow that path using a time from 0…1 and also apply an easing function to make the start and the end points smooth.

It supports Linear, Sine, Quadratic, Cubic, Quintic and Quartic easing functions with control on over whether the path is eased in and out separately.


Line 1 – EaseIn, Line 2 – EaseOut, Line 3 – EaseIn and EaseOut

This allows fine grained control of an eased, spline path in an Update function that is not possible using iTween – this enables you to abort a movement halfway through if something more important is happening .

To use it you download and import the unity package.

Eased spline paths

To use no easing call Spline.Interp(arrayOfPoints, time). Where time is a float between 0 and 1.

The array of points can be an array of Vector3s, GameObjects or Transforms which are implicitly converted to a Spline.Path instance containing an array of Vector3s. DON’T BOTHER to create your own Spline.Path instances!

e.g. transform.position = Spline.Interp(myPathObject.GetComponentsInChildren<Transform>(), time);

This would use the myPathObject and all of its children to define the path. (For example only, don’t go calling GetComponentsInChildren every frame).

To ease the function you can supply one or more of the easing parameters:

Spline.Interp(arrayOfPoints, time, easingType /* e.g. EasingType.Sine */, easeIn /* e.g. true (default) */, easeOut /*e.g. false (default true) */ );

The Spline class also supports Andeeee’s Velocity and GizmoDraw functions to help you with debugging.

PLEASE NOTE: the function uses a different algorithm for short paths so that you can get a number of different effects. If your path has 2 points it does a linear interpolation, 3 points does a Quadratic spline and 4 points does a Cubic spline. The 5 or more point version uses Catmul Rom splines and ensures that the path passes through every point.

You can double up start and end points easily with the Spline.Wrap() call on your path array – you may want to do this for short paths if you want to ensure that all of the points are passed through – or just do it yourself. There should be no effect if you are using InterpConstantSpeed or MoveOnPath when you do this (See below).

Constant Speed

I’ve added an InterpConstantSpeed function too, this tries to make the speed of movement constant between sections. It works in the same way as Interp, with a time value between 0 and 1.

Now just a note on that.  If your path points are moving then you need to watch constant speed interpolations as one section getting much longer or shorter could cause the position to vary wildly.  If your path sections are increasing or decreasing in size then you are better off using Interp and trying to make sure that each path section is roughly the same size (short sections will appear to have lower velocity than large sections, so they need to be “kind of” the same size).  Interp says if your path has 5 sections, each one takes 1/5 of the time to cross.  If you are in section 2 and section 3 gets bigger, you will complete section 2 in the normal time.  Using constant speed, the position at time “t” is along the magnitude of the whole path, if it gets longer or shorter then the position at “t” will change between sections moving the Vector unrealistically forwards or backwards. This is a problem if you are in an early stage of the path and a later path point, far off screen, is moving.  The Vector will stutter for apparently no reason.  If you want constant speed then you need to ensure that your path points maintain exactly the same distance from each other when they move (they need to move in an arc centered on the previous end point).

The second point is that applying “Constant Speed” and “Easing” appears to create a paradox; but InterpConstantSpeed accepts easing functions! Well the “Constant” bit refers to the sizes of path sections being converted to a path magnitude and the easing is applied to the “time” fed into the path calculator: so you can have your cake and eat it 🙂

Path Movement and Character Speed

I’ve also added a speed limited version, this also solves the constant speed issue by having a maximum possible movement speed, you can therefore move those path elements as often as you like – but you can’t specify the time it takes to complete the path. It works kind of like a SmoothDamp – here’s how you go about it:

public class TestFollow : MonoBehaviour {

	public Transform[] path;

	float t = 0;

	// Update is called once per frame
	void Update () {
		transform.position = Spline.MoveOnPath(path, transform.position, ref t, 0.5f);

	}
}

Basically you pass the path, the current position and a reference to a float that will contain the currently targeted path point, the final parameter shown here is the number of world units per second to move.

This routine also has the added benefit that it will move to the start of the path before following it.

Here are all of the parameters to MoveOnPath:

Parameter Meaning
pts An array of transforms, Vector3s or GameObjects that describe the path
currentPosition The current position of the object being moved
pathPosition The position along the path, this is updated by the call so must be a variable of the class
(Optional)rotation A quaternion that will be updated to show the rotation that should be used. This can change quickly for slow moving objects, so you might want to smooth it. See below.
maxSpeed The maximum number of world units to move in one second, default is 1
smoothnessFactor The smoothing factor is the number of steps on the path that will be used, default is 100
ease The type from EasingType.Linear, EasingType.Sine, EasingType.Cubic, EasingType.Quadratic, EasingType.Quartic and EasingType.Quintic. Default is Linear
easeIn true if you want to ease in the function or false if you don’t. Default is true
easeOut true if you want to ease out the function or false if you don’t. Default is true

Here’s an example of the version that returns a rotation, in this example I use the SmoothQuaternion from a previous post to smooth out the rotations.

#pragma strict

var pathPoints : Transform[];

var t : float;
var sr : SmoothQuaternion;

function Start() {
	sr = transform.rotation;
	sr.Duration = 0.5f;
}

function Update () {
	var q : Quaternion;
	transform.position = Spline.MoveOnPath(pathPoints, transform.position, t, q, 0.5f, 100, EasingType.Sine, true, true);
	sr.Value = q;
	transform.rotation = sr;

}

Javascript

You can use the Spline class from JavaScript, as long as the .cs files are somewhere inside a Assets/Plugins folder (the download does this) and that JavaScript is not inside a Plugins folder.

This example will follow a constant speed path for 20 seconds.

#pragma strict

var pathPoints : Transform[];

var t : float;

function Update () {
	transform.position = Spline.InterpConstantSpeed(pathPoints, t, EasingType.Sine, true, true);
	t += Time.deltaTime/20;
}

The next one moves an item on a path at a maximum of 0.5 world units per second

#pragma strict

var pathPoints : Transform[];

var t : float;

function Update () {
	transform.position = Spline.MoveOnPath(pathPoints, transform.position, t, 0.5f, 100, EasingType.Sine, true, true);

}

, , , , , , ,

  1. #1 by Hayden Scott-Baron (@docky) on April 15, 2012 - 2:50 pm

    Thanks for sharing this! There’s some useful work here. 🙂

    I tried the Spline.InterpConstantSpeed, and the speed I got was anything but Constant. The MoveOnPath method works great for moving at a max speed, however.

    Also, is there any way to create a smooth circular-style spline curve with this method? The final item in the path point doesn’t pay any attention to the start of the path.

    • #2 by whydoidoit on April 15, 2012 - 2:57 pm

      Hmmm, looks like I must keep uploading the wrong version! Constant speed definitely works for me 🙂

      I’ll check out why that is happening with the uploads and extract it from my project again. Constant speed attempts to work on the euclidean distance between path elements, so if some points are very close together but the control points are far from them then I guess you would not get constant speed at those positions – would this seem to be the effect you are experiencing? I.e. it covers the euclidean distance of the path at constant speed but not the smoothed path? It’s pretty much why I wrote the MoveOnPath stuff, I didn’t want to figure out the spline lengths any further.

      Let me think about the issue of circular paths – would it not work if you doubled up the final point in the path and the first point?

      • #3 by whydoidoit on April 15, 2012 - 3:00 pm

        You can double up the points by calling Spline.Wrap() on your path btw.

  2. #4 by veri on April 21, 2012 - 2:45 pm

    I dont understand. I try to use your class but it doesnt work.

    My code here :

    public class moveonpath : MonoBehaviour {

    private float t = 0;
    private Vector3[] path1;

    void start () {

    path1 = iTweenPath.GetPath(“New Path 1”);
    }

    void Update () {
    transform.position = Spline.MoveOnPath(path1, transform.position, ref t, 0.5f);
    }
    }

    I just try to move an object along a itween path at constant speed, can you help me ?

    • #5 by whydoidoit on April 21, 2012 - 4:28 pm

      Have you checked that you have a valid array of vectors coming back from thay itweenpath call? How many points are in the path?

      What happens when you call this?

  3. #6 by veri on April 21, 2012 - 5:14 pm

    when i call this my object move from his pos to (0,0,0), he s not following the path.

    I try with :
    Vector3[] path1 = new Vector3[6];
    path1[0] = new Vector3( 10,0,10 );
    path1[1] = new Vector3( 5,0,0 );
    path1[2] = new Vector3( 5,5,0 );
    path1[3] = new Vector3( -2,8,0 );
    path1[4] = new Vector3( -5,5,0 );
    path1[5] = new Vector3( -5,0,0 );

    instead of path1 = iTweenPath.GetPath(“New Path 1″); and it isnt better.

    i have 5 points the first and the last are the same.

    • #7 by whydoidoit on April 21, 2012 - 5:28 pm

      How strange, looks fine. So if you debug the code what happens to the transform.position? What happens to t? This is almost exactly what I use if for – the behaviour is attached to the right object?

  4. #8 by whydoidoit on April 21, 2012 - 5:30 pm

    Something else isn’t updating the position of the object is it?

  5. #9 by veri on April 21, 2012 - 5:47 pm

    I have just this code on my object nothing else on the scene.

    I’m a beginner ( and a 3d artist not a real code maker). Is there a way for me to send you my scene ?

  6. #11 by veri on April 21, 2012 - 5:51 pm

    Transform.position is decreasing from (0,2.5,0) to (0,0,0) and t stay to 0 until end of the move and switch to 1 for no reason.

    • #12 by whydoidoit on April 23, 2012 - 2:22 am

      It turn’s out that veri’s start method for his class had a lowercase “s” and hence wasn’t being called by Unity.

  7. #13 by veri on April 30, 2012 - 5:47 pm

    I don t undertand something,

    i wrote this code :

    public class moveonpath : MonoBehaviour {

    public float t = 0;
    public Quaternion q;
    public Vector3[] path1;
    public Vector3[] path2;

    public float speed=20.0f;

    void Start () {

    path2 = new Vector3[3];
    path2[0] = new Vector3( -50.0f,0.0f,-50.0f );
    path2[1] = new Vector3( 50.0f,0.0f,50.0f );
    path2[2] = new Vector3( 100.0f,0.0f,-50.0f );

    transform.position= path2[0];
    }
    void Update () {

    transform.position = Spline.MoveOnPath(path2, transform.position, ref t,ref q, speed, 100, EasingType.Linear, true, true);
    transform.rotation = q;
    if (t<1)print (transform.position);
    }
    }

    My object doesn't pass through the second point of my vector3 array. It seems to pass trough 50,0f,0.0f,25.0f instead. Is this normal ? How can i make my object to pass through all my points ?

    It's the same thing if i make an array with more points.

    Thx.

    • #14 by veri on April 30, 2012 - 6:04 pm

      I try to use GizmoDraw but i don’t know how it works ?

      • #15 by whydoidoit on April 30, 2012 - 6:11 pm

        To use Gizmo drawing you need to call the Spline class’s GizmoDraw from within an OnDrawGizmos() call on the behaviour. See here for details.

    • #16 by whydoidoit on April 30, 2012 - 6:09 pm

      Actually it does behave differently when you have more points.

      If you have less than 5 points then the path is treated as a basic spline and will not pass through all of the points. If the path has 5 or more points then it will pass through the central points.

      You need to double up your start and end points to make it work the way you want as the first and last points in the path are treated as modifiers – so you can do this by calling Spline.Wrap() on your points array.

      • #17 by veri on April 30, 2012 - 6:56 pm

        i’ll try this thx.

  8. #18 by veri on May 1, 2012 - 2:10 pm

    “Let me think about the issue of circular paths – would it not work if you doubled up the final point in the path and the first point?”

    Circular paths don’t seem to work. I tried with same start point and end point, it doesn’t work and i try with Spline.Wrap() on my array and it’s worst.

    Can you look at this issue ? I send you my scene.

    Thx in advance.

    • #19 by whydoidoit on May 1, 2012 - 2:40 pm

      Unfortunately on reflection I’ve realised that a circular closed loop version requires a different algorithm for getting the points near the start and end. The current version will stop and the end and start again at the start, this will not be a smoothed transition.

      Without waiting for me to write a closed loop version the only way around this would be to manually add points at the start and end in the reasonable shape of curve you want there – that or accept that the transition will be angular as the loop completes.

      • #20 by veri on May 1, 2012 - 4:05 pm

        Ok thx, it s not a big deal, i just want to be sure i’m not doing something bad.

  9. #21 by Ziboo on May 17, 2012 - 11:41 am

    Hi,
    Thanks you so much for this class, very usefull.

    I’m wondering what is the best way to make a “big spline” with this class.
    I mean, I like to have a series of bezier curves attached together to make a path (for character or traffic path),
    and being able to animate an object along those lines.

    Any idea ?

    Thanks so much

    • #22 by whydoidoit on May 17, 2012 - 11:47 am

      So basically you are after multiple paths that you can join together? I currently use the class as it is to manage a simple road “track” system.

      At the moment it uses Catmull-Rom splines and I’m thinking of also adding B-Splines which have a different effect (the character does not pass through every point but makes it’s way from start to end, affected by the points but never exceeding the boundaries of the convex hull that is defined by the points).

      • #23 by Ziboo on May 17, 2012 - 11:49 am

        Yeah, but with the abilities to manage bezier for every points.
        I’m not sure I’m clear :s

        It will be a series of bezier lines stitched together.

      • #24 by Ziboo on May 17, 2012 - 11:52 am

      • #25 by whydoidoit on May 17, 2012 - 11:52 am

        Well using MoveOnPath when you reach the end of a path you should be able to start the next – the only thing might be that you will want to speed to remain constant between the two parts. I’ll think about it 🙂

      • #26 by Ziboo on May 17, 2012 - 11:56 am

        Yep I thout about this,

        Like

        if(t > 0.99f)
        move to next path

        but I’m quite sure that if bad framerate append it will bug the thing, cause when “t > 1” the script throw an error, which is normal I guess

      • #27 by whydoidoit on May 17, 2012 - 11:58 am

        Yeah should probably Mathf.Clamp01() that t value – think I’ve added that in my code.

      • #28 by whydoidoit on May 17, 2012 - 11:59 am

        You could measure the distance that has been moved since the last transform (which is likely to be less than “speed” on the last node) then use the difference between this and speed in a call to follow the second path, reverting to normal “speed” thereafter.

      • #29 by Ziboo on May 17, 2012 - 12:06 pm

        Yep thanks for the quick reply,
        I need to dig a bit more to really now how to do that.
        I think i’ll use InterpConstantSpeed cause I don’t have much control with MoveOnPath.
        If i’m right MoveOnPath is automatic right ?

      • #30 by whydoidoit on May 17, 2012 - 12:08 pm

        Actually I think MoveOnPath is the easier and more controlled routine – you provide it with a speed and it moves that fast on the path (or towards it if the object is currently distant).

        Basically it says move a maximum of speed per second along the path.

      • #31 by whydoidoit on May 17, 2012 - 12:09 pm

        It doesn’t automatically move the object BTW – it just returns you a Vector3 (and optionally a rotation)

      • #32 by Ziboo on May 17, 2012 - 12:11 pm

        Allright I will try this 😉
        Thanks for the reply

  10. #33 by Genjin on June 9, 2012 - 11:33 pm

    I hope I’m not bothering you too much with my question. Could you please provide an example on how I’d use this to align multiple objects along a path created from your class? I’m basically trying to instantiate a bunch of objects along a quadratic bezier path with a definable offset between each object. I know I need to make a loop to iterate through my objects, but I’m having trouble grasping how to do this with your class exactly. Your help would be most appreciated 🙂 Thank you for this class!

    • #34 by whydoidoit on June 10, 2012 - 3:55 am

      No problem – just on my iPhone at the moment – I’ll figure something out for you…

      • #35 by Genjin on June 10, 2012 - 4:13 pm

        Thank you so much in advance for your efforts. I’m looking forward to seeing and learning how to do it 🙂

  11. #36 by Chris on June 28, 2012 - 12:43 pm

    Hi, the link to the SmoothQuaternion script above is not working, can anyone provide another link please?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: