I have a CatmullRomSpline, and using the very good example at https://github.com/libgdx/libgdx/wiki/Path-interface-%26-Splines I have my object moving at an even pace over the spline.

Using a simple train and carriage example, I now want to have the carriage follow the train at the same speed as the train (not jolting along as it does with my code below). This leads into my main questions:

  1. How can I make the carriage have the same constant speed as the train and make it non jerky (it has something to do with the derivative I think, I don't understand how that part works)?

  2. Why do I need to divide by the line length to convert to metres per second, and is that correct? It wasn't done in the linked examples?

I have used the example I linked to above, and modified for my specific example:

    private void process(CatmullRomSpline catmullRomSpline) {
        // Render path with precision of 1000 points
        renderPath(catmullRomSpline, 1000);
        float length = catmullRomSpline.approxLength(catmullRomSpline.spanCount * 1000);

        // Render the "train"
        Vector2 trainDerivative = new Vector2();
        Vector2 trainLocation = new Vector2();
        catmullRomSpline.derivativeAt(trainDerivative, current);
        // For some reason need to divide by length to convert from pixel speed to metres per second but I do not
        // really understand why I need it, it wasn't done in the examples???????
        current += (Gdx.graphics.getDeltaTime() * speed / length) / trainDerivative.len();
        catmullRomSpline.valueAt(trainLocation, current);
        if (current >= 1) {
            current -= 1;

        // Render the "carriage"
        Vector2 carriageLocation = new Vector2();
        float carriagePercentageCovered = (((current * length) - 1f) / length); // I would like it to follow at 1 metre behind
        carriagePercentageCovered = Math.max(carriagePercentageCovered, 0);
        catmullRomSpline.valueAt(carriageLocation, carriagePercentageCovered);

    private void renderPath(CatmullRomSpline catmullRomSpline, int k) {
        // catMulPoints would normally be cached when initialising, but for sake of example...
        Vector2[] catMulPoints = new Vector2[k];
        for (int i = 0; i < k; ++i) {
            catMulPoints[i] = new Vector2();
            catmullRomSpline.valueAt(catMulPoints[i], ((float) i) / ((float) k - 1));

        for (int i = 0; i < k - 1; ++i) {
            SHAPE_RENDERER.line((Vector2) catMulPoints[i], (Vector2) catMulPoints[i + 1]);

    private void renderCircleAtLocation(Vector2 location) {
        SHAPE_RENDERER.circle(location.x, location.y, .5f);

To create a decent sized CatmullRomSpline for testing this out:

Vector2[] controlPoints = makeControlPointsArray();
CatmullRomSpline myCatmull = new CatmullRomSpline(controlPoints, false);


private Vector2[] makeControlPointsArray() {
    Vector2[] pointsArray = new Vector2[78];
    pointsArray[0] = new Vector2(1.681817f, 10.379999f);
    pointsArray[1] = new Vector2(2.045455f, 10.379999f);
    pointsArray[2] = new Vector2(2.663636f, 10.479999f);
    pointsArray[3] = new Vector2(3.027272f, 10.700000f);
    pointsArray[4] = new Vector2(3.663636f, 10.939999f);
    pointsArray[5] = new Vector2(4.245455f, 10.899999f);
    pointsArray[6] = new Vector2(4.736363f, 10.720000f);
    pointsArray[7] = new Vector2(4.754545f, 10.339999f);
    pointsArray[8] = new Vector2(4.518181f, 9.860000f);
    pointsArray[9] = new Vector2(3.790908f, 9.340000f);
    pointsArray[10] = new Vector2(3.172727f, 8.739999f);
    pointsArray[11] = new Vector2(3.300000f, 8.340000f);
    pointsArray[12] = new Vector2(3.700000f, 8.159999f);
    pointsArray[13] = new Vector2(4.227272f, 8.520000f);
    pointsArray[14] = new Vector2(4.681818f, 8.819999f);
    pointsArray[15] = new Vector2(5.081817f, 9.200000f);
    pointsArray[16] = new Vector2(5.463636f, 9.460000f);
    pointsArray[17] = new Vector2(5.972727f, 9.300000f);
    pointsArray[18] = new Vector2(6.063636f, 8.780000f);
    pointsArray[19] = new Vector2(6.027272f, 8.259999f);
    pointsArray[20] = new Vector2(5.700000f, 7.739999f);
    pointsArray[21] = new Vector2(5.300000f, 7.440000f);
    pointsArray[22] = new Vector2(4.645454f, 7.179999f);
    pointsArray[23] = new Vector2(4.136363f, 6.940000f);
    pointsArray[24] = new Vector2(3.427272f, 6.720000f);
    pointsArray[25] = new Vector2(2.572727f, 6.559999f);
    pointsArray[26] = new Vector2(1.900000f, 7.100000f);
    pointsArray[27] = new Vector2(2.336362f, 7.440000f);
    pointsArray[28] = new Vector2(2.590908f, 7.940000f);
    pointsArray[29] = new Vector2(2.318181f, 8.500000f);
    pointsArray[30] = new Vector2(1.663636f, 8.599999f);
    pointsArray[31] = new Vector2(1.209090f, 8.299999f);
    pointsArray[32] = new Vector2(1.118181f, 7.700000f);
    pointsArray[33] = new Vector2(1.045455f, 6.880000f);
    pointsArray[34] = new Vector2(1.154545f, 6.100000f);
    pointsArray[35] = new Vector2(1.281817f, 5.580000f);
    pointsArray[36] = new Vector2(1.700000f, 5.320000f);
    pointsArray[37] = new Vector2(2.190908f, 5.199999f);
    pointsArray[38] = new Vector2(2.900000f, 5.100000f);
    pointsArray[39] = new Vector2(3.700000f, 5.100000f);
    pointsArray[40] = new Vector2(4.372727f, 5.220000f);
    pointsArray[41] = new Vector2(4.827272f, 5.220000f);
    pointsArray[42] = new Vector2(5.463636f, 5.160000f);
    pointsArray[43] = new Vector2(5.554545f, 4.700000f);
    pointsArray[44] = new Vector2(5.245453f, 4.340000f);
    pointsArray[45] = new Vector2(4.445455f, 4.280000f);
    pointsArray[46] = new Vector2(3.609091f, 4.260000f);
    pointsArray[47] = new Vector2(2.718181f, 4.160000f);
    pointsArray[48] = new Vector2(1.990908f, 4.140000f);
    pointsArray[49] = new Vector2(1.427272f, 3.980000f);
    pointsArray[50] = new Vector2(1.609090f, 3.580000f);
    pointsArray[51] = new Vector2(2.136363f, 3.440000f);
    pointsArray[52] = new Vector2(3.227272f, 3.280000f);
    pointsArray[53] = new Vector2(3.972727f, 3.340000f);
    pointsArray[54] = new Vector2(5.027272f, 3.360000f);
    pointsArray[55] = new Vector2(5.718181f, 3.460000f);
    pointsArray[56] = new Vector2(6.100000f, 4.240000f);
    pointsArray[57] = new Vector2(6.209091f, 4.500000f);
    pointsArray[58] = new Vector2(6.118181f, 5.320000f);
    pointsArray[59] = new Vector2(5.772727f, 5.920000f);
    pointsArray[60] = new Vector2(4.881817f, 6.140000f);
    pointsArray[61] = new Vector2(5.318181f, 6.580000f);
    pointsArray[62] = new Vector2(6.263636f, 7.020000f);
    pointsArray[63] = new Vector2(6.645453f, 7.420000f);
    pointsArray[64] = new Vector2(6.681817f, 8.179999f);
    pointsArray[65] = new Vector2(6.627272f, 9.080000f);
    pointsArray[66] = new Vector2(6.572727f, 9.699999f);
    pointsArray[67] = new Vector2(6.263636f, 10.820000f);
    pointsArray[68] = new Vector2(5.754546f, 11.479999f);
    pointsArray[69] = new Vector2(4.536363f, 11.599998f);
    pointsArray[70] = new Vector2(3.572727f, 11.700000f);
    pointsArray[71] = new Vector2(2.809090f, 11.660000f);
    pointsArray[72] = new Vector2(1.445455f, 11.559999f);
    pointsArray[73] = new Vector2(0.936363f, 11.280000f);
    pointsArray[74] = new Vector2(0.754545f, 10.879999f);
    pointsArray[75] = new Vector2(0.700000f, 9.939999f);
    pointsArray[76] = new Vector2(0.918181f, 9.620000f);
    pointsArray[77] = new Vector2(1.463636f, 9.600000f);
    return pointsArray;

Disclaimer: My math is very rusty, so please explain in lay mans terms....

  • \$\begingroup\$ possible duplicate of Determine arc-length of a Catmull-Rom spline \$\endgroup\$
    – msell
    Jun 1 '14 at 13:58
  • \$\begingroup\$ BTW railroads and roads mostly use circular arcs instead of catmull-rom or bezier splines. The math is simpler in some ways, especially the math for following the paths and determining lengths of sections. \$\endgroup\$
    – amitp
    Jun 1 '14 at 16:35
  • \$\begingroup\$ Thanks amitp, that is a useful article, but I would really like to understand what is wrong with my above code. It is VERY close to working and the train itself moves at a very consistant speed. I am sure it is just something simple around the "Render the carriage" part. \$\endgroup\$
    – Simon
    Jun 1 '14 at 17:06
  • \$\begingroup\$ msell, it is a very similar problem but this is a specific example. I have the train moving at a constant speed, it is just the carriage - it is related to the derivative for sure but I cannot see how I need to plug it in. \$\endgroup\$
    – Simon
    Jun 1 '14 at 17:10

You're using a fixed offset from the train, rather than taking into account the fact that a parameter difference of 1/length is not 1 meter along the whole spline. In places with a large derivative, this offset will be greater than 1 meter. In places with a small derivative, it will be less.

Try something like this instead:

float followOffset = 1f/trainDerivative.len();

float carriagePercentageCovered = (((current * length) - followOffset) / length);

Note that even this is only a first approximation, and you may continue to see some variation in spacing without further iterations. If this proves to be an issue, let me know in a comment and I can show you how to refine the movement further.

  • \$\begingroup\$ Unfortunately the "carriage" just bounces backwards and forwards quite abrubtly along behind the (smoothly running) train using the suggested approach. I know you are right, and I definately need to take into account the derivative, but eveything I try just makes it look worse! I was wondering if I should be using the carriages derivative somehow? Many thanks. \$\endgroup\$
    – Simon
    Jun 2 '14 at 21:11
  • \$\begingroup\$ DMGregory? Anyone? :S \$\endgroup\$
    – Simon
    Jun 15 '14 at 20:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.