…in which we use more than one sprite for our particles.So, we have a particle system that uses sprites. Or have we? Actually we have a particle system that uses one sprite, the one in the top left corner of the sprite sheet:

See how we’re just grabbing sprite(0, 0) here? That won’t do! We want two things:

  • A particle should be assigned one of the rows of the sprite sheet as its animation; and
  • A particle’s sprite should change over the course of its life, following the frames laid out horizontally along its row.

I see some code quality issues around here, by the way, but I really want to push the functionality forward a bit before doing more tidying up. The constructor is long and complicated and takes a lot of arguments that are hard to keep straight: we’ll fix that and other related problems another time as they’re aesthetic rather than functional.

Let’s start by assigning one spritesheet row to each particle, but continue just using the first sprite in the row for its whole lifetime. This would be super-easy except we had to do all that awful caching last time, which complicates matters slightly. But only slightly.

Currently we have one cache of images that all the particles use — I called it spriteCache:

Instead of this, let’s make spriteCache a 2D array. The first number will be the row-number for the animation, the second will be the age and will behave exactly as before. Then we’ll randomly assign one row to each particle and pick out the right one when we draw it.

We’ll declare a 2D array in the usual way:

Now we need to tweak the caching code accordingly:

You can play spot-the-difference with this one if you like but I would draw your attention to the following:

  • The way we use spriteSheet.rowCount on line 33 to set up the array.
  • The extra loop we now have (line 34) enclosing everything else; this creates a separate age-based cache of images for each animation row.
  • The use of spriteSheet.sprite(r, 0) on line 39 to get the first sprite from row r, rather than row 0.
  • Every time we had spriteCache[i] we now have spriteCache[r][i].
  • There’s an extra curly bracket at the bottom for the new loop (out of shot in the above image).

We’d better now change the places where we use spriteCache, since they will all be broken. Fortunately that’s just one place — where we place the image in drawParticleSystem. But how does it know, at that point, which animation to use? It depends on the particle, right? So the particle needs to know which animation is attached to it. We can represent this by a number — the row of the spritesheet, a.k.a. the r in particles[r][i].

So let’s add an animation property to Particle so it can remember which row it’s supposed to be using:

and let’s set that to a random value when we create a particle in init():

and finally let’s use this when we draw the particle:

A quick test — it works! It’s a bit of a subtle effect at the moment, since everything is kind of blurry and cloudy, but it’s definitely doing the right thing.

OK let’s continue by choosing the right frame for the animation by age. Of course, this is more complexity to load into the caching code, but at least it only runs at startup. On my first attempt at this I used lerp but the result wasn’t very good so I rolled my own lerping calculation (line 39):

We can read the formula from the inside out. This: spriteSheet.colCount * agePct just gets some number between 0 and the number of columns. In our example, there are 3 columns. We use floor on it to round it down to the whole number below it. In our example this gives us an integer whose value is almost always 0, 1 or 2 — but it will be 3 at the very end when agePct = 1.

This is a problem, because there is no column with index number 3 — we have 3 columns, indexed 0 1 and 2. So I wrap the whole thing in a min that chooses the smaller of the two options — either use colCount – 1, which is the biggest value I’ll accept, or the calculated value if it’s smaller. This gives a nicely-spaced set of changes between the available sprites for the animation, with the last sprite getting an extra frame (but who cares about that?).

Here’s how it looks — because my sprites are stupid and blobby, with no sense of proper animation, you can see them changing as they grow:


There’s one more thing I’d like to do before we call it a day. If you watch closely you’ll see that each sprite always appears the same way up, no matter which way the particle is moving. I want to be able to rotate the sprite for a particle so it faces in the direction of travel. Maybe this is something we’ll want to be able to turn on and off later, but I’d just like to get it working for now.

This part will use some slightly opaque techniques that I don’t want to get into here. I’m going at add a getAngle function to Particle that uses its movement IntablyFloatPair and a bit of trignometry to get the angle of rotation (you can read about atan2 here):

Then we apply the rotation in drawParticleSystem — these transformations are quite confusing at first but this is a good explanation of them.

A bit of trigonometry isn’t hard to acquire and will really help you solve problems like this that are seemingly impossible otherwise.

The results are worth it, I think — and on the laptop it still runs very fast (the GIFs all run at the same speed so they’re not a great indication of how it looks when you run it in real life):

Code is on GitHub as always. We’ve definitely passed a milestone this week, and we have a basic particle system working, running well and looking decent. You should definitely experiment with other sprite sheets and colours to see what effects you can get.We’ve done the basics: it’s up to us now where we take it.

Personally, I think the next step is to make the particles’ movement more interesting. They currently just travel in boring straight lines, all coming from the same spot. We won’t be building a physics engine here or trying to simulate any natural processes; rather, we’ll be looking to add behaviour that gives us interesting visual effects.

All the code for this series is available on GitHub.