Barrier Blast Particles

early particle demo

Barrier Blast has a lot of particle effects. For simple particle effects, I have a very simple particle system I use. The gem shards, fireball particles, and slime droplets all use this system. For more complex effects, I ususally just implement the particles as part of the draw code. The magic effects and explosions are implemented in the draw code of related objects.

Force field effect

First, some variables will control where the force field will be drawn on screen, and the radius of the barrier's circle.

Then we make a list which will hold the coordinates of each particle in the force field. It also holds a frame counter f for each particle. This counter controls the lifetime of the particles. Particles are colored based on their lifetime, and they die and respawn when the counter reaches 64. You can spawn any number of particles here (as long as you can draw them within the CPU limit), and set the lifetime to whatever you like.

function _init()
  ffx = 64
  ffy = 64
  distance=5
  ff = {}
  for i=1,64 do
    ff[i]={f=i,x=0,y=0}
  end
end

For this demo, we move the force field around based on button inputs. In Barrier Blast the force field tracks the player until they fire a missile. When they press the shoot button the distance is reduced to 2 to form a projectile, and ffx and ffy are updated to move the particle separately from the player.

function _update()
  if (btn(0)) ffx-=1.5
  if (btn(1)) ffx+=1.5
  if (btn(2)) ffy-=1.5
  if (btn(3)) ffy+=1.5

  if (btn(4)) distance+=1
  if (btn(5)) distance-=1
  -- _update() continued below

Now we loop over all of the particles and check if the frame counter is over 64. If it is, the particle is "dead" and should be deleted. But instead of deleting the particle object and creating a new one, we just move the particle and reset the counter.

A random angle is chosen, and some trig is used to set the X and Y location in relation to the ffx and ffy positions.

If the particle is alive, we just increment the frame counter. I ended up having it increment 4 at a time, so that the particles last 64 / 4 = 16 frames. I could just increment by 1 here and change the other checks to check for 16 directly, but I got lazy when I was tweaking particle lifetime. It was easier to change it in one place here than to update the lifetime check and the initialization code.

  -- _update() ctd.
  for p in all(ff) do
    if p.f>64 then
      local a=rnd()
      p.x=ffx+4+cos(a)*distance
      p.y=ffy+4+sin(a)*distance
      p.f=0
    else
      p.f+=4
    end
  end
end

This gradient function is super useful. As the counter increases from 0 up to the maximum, the result of this function will be a color chosen from the list. This makes it very easy to tweak your particles' color changes, because you can just update the list passed in to the gradient function.

function gradient(counter, maximum, colors)
  return colors[flr(counter*#colors/maximum)+1]
end

The draw code is simple. The only complex work is handled by the gradient function. Each particle is drawn by setting the pixel at it's location to a color from the gradient. Particles start out white, then fade to light blue, then dark blue. Gradient shows each entry in the list for an equal number of frames, so by putting 12 in the list twice the particle spends more time bright blue.

function _draw()
  cls()
  for p in all(ff) do
    if p.f<64 then
      local c = gradient(p.f,64,{7,12,12,1})
      pset(p.x, p.y, c)
    end
  end
end

Here's a demo cart showing the force field in action. See the bottom of the page for a downloadable cartridge image you can open in PICO-8.

Simple particle system

There is another particle system I used for many of the simpler particles in the game. First we'll create a list which will hold all active particle objects. In _update, we'll add a call which will update each particle.

function _init()
  particles = {}
end

function _update()
  foreach(particles, update_particle)
end

Before we look at the update_particle function, lets take a look at how particle objects are created and what properties they have. The spawn_particle function has several parameters which control the location and behavior of the particle. x and y are the particle's starting location on screen. After lifetime frames have passed, the particle will be deleted. The lifetime is used as part of the calculation when determining the color of the particle.

colors is a list of color numbers. These colors are passed to the gradient function we used above to determine what color the particle is each frame.

Finally, update_fn is the update function for this particle. It gets called each frame to control the movement of the particle. In my projects I usually end up with 2 or 3 simple movement functions for different types of particles.

The spawn function also gives each particle a randomized starting velocity. It picks a random angle and random power, and uses some math to convert that to an x and y velocity. It also starts frame, the "frame counter" at 0. This counter tracks how many frames the particle has been alive.

function spawn_particle(x ,y , lifetime, colors, update_fn)
  local angle = rnd()
  local velocity = rnd()
  local i = {
    x = x, y = y,
    lifetime = lifetime, colors = colors,
    vx = cos(angle) * velocity,
    vy = sin(angle) * velocity,
    frame = 0, update = update_fn
  }
  add(particles, i)
  return i
end

The update_particle function tracks the lifetime of each particle, so that we don't have to repeat that in each of the different particle updater functions. Then it calls the particle's update function. i:up() is a convenient shortcut in Lua when you have object oriented code. It is the same as typing i.up(i). It looks up the up function saved inside of our particle object, and calls it. It passes the particle itself as the first argument, so that the up function can access that particle's properties. For more info on the : operator, check out the Object-Oriented Programming section in Programming in Lua.

function update_particle(i)
  if i.frame > i.lifetime then
    del(particles, i)
  else
    i:update()
    i.frame += 1
  end
end

Let's take a look now at two different update functions I used in Barrier Blast. pfall is a simple function that adjusts the position based on velocity, and then updates vy to account for gravity. pfire is a bit more complex. It ignores the y component of velocity entirely. Fire particles just float upwards 0.7 pixels per frame. The x position is a blend of two components. First we include vx, because this was set randomly and we want to keep a random spread. Then we blend in a sin function, which gives it a wavy effect. Over time, the result of sin(i.f/8) moves back and for between 1 and -1. This causes the particle to move left and right in a wavy pattern.

function pfall(i)
  i.x  += i.vx
  i.y  += i.vy
  i.vy += .5
end

function pfire(i)
  i.x  += i.vx + sin(i.frame / 8)
  i.y  += -0.7
  i.vx *= 0.94
end

Now that we've seen how particles are updated each frame, lets take a look at how they are drawn. We use the gradient function to get the color of the particle. Then we use a circfill to draw the particle. Why circfill? In addition to the code shown so far, I also sometimes manually change a particle's radius property after spawning it. For example, when a slime is killed in Barrier Blast I create particles with a radius between 1 and 3 pixels, so that the particles are big and blobby. If you're only drawing 1 pixel particles, you can use pset here and it will probably be faster.

function _draw()
  foreach(particles, draw_particle)
end

function draw_particle(i)
  local color = gradient(i.frame, i.lifeteme, i.colors)
  local radius = 0.5
  if (i.radius) radius = i.radius
  circfill(i.x, i.y, radius, color)
end

PICO-8 Demo Cartridges

All code on this page is free to use in your own projects.

Force Field Demo Cartridge