Sprite Motion Paths Tutorial

By Richard Davey on 2nd March 2015   @photonstorm

Welcome!

In the last issue we covered how to make a bullet pool and various shoot-em-up weapon types. This time we're exploring a way to create waveforms, or paths for your baddies to follow. Motion paths don't apply to just shoot-em-ups of course. They're useful for nearly all types of game, and as you'll learn are quick to create.

Get the source

I'm only going to highlight the most important parts of the code here. So please always look at the source first. If you've questions about something I didn't cover then ask on the forum.

Run / Edit the code on jsbin or codepen

Clone the phaser-coding-tips git repo.

WaveForms

Hidden away within the Phaser.Math class are three rather innocuous methods: linearInterpolation, bezierInterpolation and catmullRomInterpolation. Unless you've ever had a specific need to investigate these they have probably sat there unloved, even though they've existed since the beginning of Phaser.

Yet use them in the right way and you can create paths just like this one:

path

Pretty neat huh?

So how can we get them to work for us?

Interpolation for the Nation

Each function works by taking two parameters: An array of values and a percentage between 0 and 1. The array should contain numeric values, for example:

[ 0, 128, 256, 384, 512, 640 ]

.. and that's all it needs. It doesn't matter how large the gaps are between the values, or if they are positive or negative, floats or integers.

The second parameter is a value between 0 and 1 which dictates how far along the 'path' the interpolation value will return. A value of 0 is the start, 1 is the end, 0.5 is the middle, etc. Here is a complete example of a call:

var points = [ 0, 128, 256, 384, 512, 640 ];
var distance = 0.5;
var result = this.math.linearInterpolation(points, distance);

In the above result will equal 320. Which is exactly half-way between 0 and 640 in a linear interpolation.

So the function works. But how do we take this and turn it into a motion path?

It's to do with how many times you call it

If we call an interpolation function once, we get a single value out of it. By varying the percent parameter we can iterate through the path in as many steps as we need. In this example we will create a path going from the left to the right of the game, a total of 640 pixels, passing through 6 points. We will also use a BitmapData object to plot the path to.

var PhaserGame = function () {

    this.bmd = null;

    this.points = {
        'x': [ 32, 128, 256, 384, 512, 608 ],
        'y': [ 240, 240, 240, 240, 240, 240 ]
    };

};

PhaserGame.prototype = {

    create: function () {

        this.stage.backgroundColor = '#204090';

        this.bmd = this.add.bitmapData(this.game.width, this.game.height);
        this.bmd.addToWorld();

        var py = this.points.y;

        for (var i = 0; i < py.length; i++)
        {
            py[i] = this.rnd.between(32, 432);
        }

        this.plot();

    },

    plot: function () {

        this.bmd.clear();

        var x = 1 / game.width;

        for (var i = 0; i <= 1; i += x)
        {
            var px = this.math.linearInterpolation(this.points.x, i);
            var py = this.math.linearInterpolation(this.points.y, i);

            // var px = this.math.bezierInterpolation(this.points.x, i);
            // var py = this.math.bezierInterpolation(this.points.y, i);

            // var px = this.math.catmullRomInterpolation(this.points.x, i);
            // var py = this.math.catmullRomInterpolation(this.points.y, i);

            this.bmd.rect(px, py, 1, 1, 'rgba(255, 255, 255, 1)');
        }

        for (var p = 0; p < this.points.x.length; p++)
        {
            this.bmd.rect(this.points.x[p]-3, this.points.y[p]-3, 6, 6, 'rgba(255, 0, 0, 1)');
        }

    }

};

game.state.add('Game', PhaserGame, true);

You can copy and paste the above code into a file or jsbin to test it. All it does create a points object with two properties x and y. These are arrays. When the code starts it randomises the y array contents.

The resulting path is drawn to the BitmapData. We determine how many times to iterate through the path with the line: var x = 1 / game.width. For a 640 pixel wide game x will equal 0.0015625, so the for loop in the plot function will run 640 times.

We plot a single white rectangle to the BitmapData for each interpolation result. At the end we plot yellow rectangles to represent the points through which the path has moved.

You can see the two other forms of interpolation commented out in the plot function. Swap them around as needed to see the differences between the 3 types.

Linear, Bezier and Catmull Rom

By taking the above code and evolving it slightly we can randomise the path on start-up and step through the three different modes. You'll find this example in the github repo and on jsbin. With just 6 points in our path look at the differences between the three:

linear

This is linear interpolation. As you expect it moves directly from one point to the next.

bezier

This is bezier interpolation. Here you'll get a much smoother spline, with the additional points on the line acting as control nodes for the bezier curves. If you're interested in the theory behind it there's a great piece on Wikipedia.

catmull

Finally we have Catmull-Rom interpolation. The main difference here, as can be seen in the screen shot, is that the points along the spline also make up the control points. As such you can see the path pass through each control point, giving a smooth interpolated result. Incidentally it's named after Edwin Catmull and Raphael Rom and again has a fascinating history worth reading about.

Alien Splines

Splines can be used for all kinds of things. But in this example we're going to use them as a motion path for some aliens. While we could take the plot function we have now and use it to update a sprite in real-time, that is also highly inefficient. Instead we'll trade a tiny amount of memory for CPU time and copy the path data to an array. You can see this in the file aliens.html in the repo. In the plot function the point data is added to a path array:

this.path.push( { x: px, y: py });

and in the update method we have a single sprite following it:

this.alien.x = this.path[this.pi].x;
this.alien.y = this.path[this.pi].y;

this.pi++;

if (this.pi >= this.path.length)
{
    this.pi = 0;
}

Of course this is about as basic as you can make it. You could vary the speed, you could reverse the path, you could release 20 sprites instead of 1, all following the same pre-calculated point data. The important part is that now you've got a way to make something follow the spline we created.

alien

About Face

What if we want our sprite to face the direction it is travelling? We can accomplish this by adding an extra property to the object we add in the paths array: angle.

Within the plot method we calculate the angle between the current node and the one before it:

var node = { x: px, y: py, angle: 0 };

if (ix > 0)
{
    node.angle = this.math.angleBetweenPoints(this.path[ix - 1], node);
}

this.path.push(node);

ix++;

and then in update we make the sprite use it:

this.arrow.rotation = this.path[this.pi].angle;

face

The result is our sprite perfectly following the spline as it traverses it.

The WaveForms Path Editor

logo

By now you can surely see the power of using splines in your games. Yet so far all we've done is create random paths using a fixed 6 points, varying on the y axis alone. What if you could edit the paths? Dragging each node around in real-time, adding and deleting nodes at will, until you've got the perfect path for your sprites?

Well I'm glad you asked - because that's exactly what WaveForms is :)

waveforms

I spent quite a while putting this editor together and it has some nice features:

  • Easily Add and Remove points on the path
  • Toggle grid snapping
  • Keyboard short-cuts for all functions
  • Swap between and edit 8 different paths at once
  • Export as JSON data
  • Send a sprite down a path to test it
  • Manipulate the paths in real-time, even as sprites follow them
  • Set a path as being a 'closed loop' or open

Use WaveForms for free at phaser.io/waveforms/

You can also buy the complete source code to WaveForms for $10 USD. With the source you can run WaveForms locally and of course disable the splash screen. You're then free to add whatever additional features you'd like: tweak the export code, merge it into your own tools, adjust the grid or game size or whatever you require.

If you feel WaveForms would be useful then please support my on-going work with Phaser and buy the source code.