Js Particles
An Intro to JS Particles, Pt. 1
About
This article will describe how you can implement a particle system in pure/vanilla javascript for use in web/html games or other web-based/browser projects.
Meet Pete
So Pete the Particle is a happy little particle running in the canvas shown above. Feel free to poke him if he isn’t showing up very well. But you may be wondering… What exactly is a particle and why would I want to implement my own? First, in my perspective at least, a particle is simply a procedurally-generated visual effect that can be added to a game (or any other visual project). Think of a shower of sparks when you shoot an enemy, a trail of dust that kicks up when your character runs, even footsteps that your character may leave in sand or snow. These can all be implemented through particles. And while there are other ways to implement visual effects, I’ll present you some advantages to using a particle system, which allows you to fire and forget visual effects so that your game logic doesn’t need to keep track of animation or effect state.
Game Hooks
Let’s take a closer look at Pete and how Pete is integrated into the game loop. To understand how the particle logic works, we need to understand how it will be hooked into your game. The game model I’m using here assumes that all logic is tied to Animation Frames. In each frame, you are making updates to your game model and then rendering out the results to display in your browser. The particle logic will follow this same model.
I’ll assume that your game loop looks something like this:
Where update(updateCtx)
and render(renderCtx)
are your entry points to your game logic. We’ll use these same entry points for operations on particles. While you could get away with a single function call, there’s reasons breaking them out makes sense. Specifically there may be cases where you want to update the state of particles (e.g.: position and such), but don’t want to render it (e.g.: offscreen or not visible). Also note that I’m passing in a few variables which will be important to particle operations. updateCtx
is an object that contains a deltaTime
attribute, and renderCtx
is the canvas context onto which stuff will be rendered. We’ll see how these are used when we dive deeper into what makes Pete tick. Let’s assume that your main update
and render
methods have been updated to call the particle’s entry points, as shown here:
Particle Logic
To implement Pete, we are going to introduce a class to store some data and define the game hooks to allow updates and rendering of the particle. What data is needed to model the behaviour we are seeing? Well, there’s a position on the screen to start with (let’s call that x
,y
coordinates), and the shape of the particle, which is a filled circle which can simply be represented by a radius (size
) as well as a color associated with the particle (color
). Those are all static properties, but what about the behaviour? Well, the particle is moving. There’s lots of ways we could represent that, but the easiest is to just consider the change of position based on x and y axis over time. Let’s call the change in x, delta X and the change in y, delta Y (dx
and dy
for short). Finally, when the particle hits the edge of the canvas it changes direction. We need some logic to keep track of the bounds of the canvas. This can be represented by a minimum and maximum x value (minx
, maxx
) and same for a y value (miny
, maxy
). Putting that into a class structure and initializing looks like:
Two notes here:
- I like to use
spec
objects in constructors. It allows you to easily change the calling parameters for object creation (which we will do later) as well as allows you a way to easily specify defaults for object creation, which I’ve done here (spec.x || 0
becomesundefined || 0
which becomes0
whenspec.x
isundefined
). So you can make a PeteParticle just by callingnew PeteParticle()
and you will get something that works. - You’ll note a
.001
multipled to thedx
anddy
values. If you look back at the game loop, thedeltaTime
attribute being passed to the update function is computed in milliseconds. I like to think of velocity in terms of pixels per second, so to get to pixels per millisecond, multiply by.001
.
Now that we have data, let’s take a closer look at the update
and render
methods that are needed to get Pete moving and rendering. If you remember, update
is being passed a context that includes a deltaTime
attribute which represents the amount of time in milliseconds since the last update
was called. This is the elapsed time since we’ve last made any changes to Pete. In the data for Pete, we expressed movement by keeping track of a delta x and y which represented the number of pixels of movement per unit time (millisecond in our case, remembering we converted dx
and dy
in seconds by multiplying by .001
converting to milliseconds). So to get how far Pete should have traveled since the last update, all we need to do is multiply the dx
and dy
values by deltaTime
and then store the results. Take a look:
Pretty simple right? Note: I’m rounding the change in position I’m adding to x
and y
as a best practice to keep x
and y
as integers. Floats work too, but are a bit more computationally heavy when rendering. This is enough to get Pete moving, but Pete would quickly become lost as he wanders off the screen we’ve provided. To keep Pete safe (and always visible) we’re going to add a fence that tells Pete how far he can go in any direction. If he goes to far in any direction, we’ll tell him to turn around and go in the opposite direction at the same speed. From our data model for Pete the fence was represented by the minx, miny, maxx, maxy
variables. Let’s take a look at how to implement the fence:
The logic for the fence is pretty straightforward. If Pete’s position ever drops below a minimum or above a maximum, we change the corresponding position delta to be negative or positive to turn Pete around. Implementation Note: using Math.abs
is required here vs. just flipping the sign of the delta (e.g.: this.dx = -this.dx
). Remember that the delta time passed in is variable as it is the actual milliseconds since last call. For example, say your dx
is -5 and current x
position is 5 and you are passed a deltaTime
of 10. Your new position would be calculated as this.x += -5*10
or this.x = 5 - 50
or this.x = -45
. Assuming our minx
is 0, -45 is well under this, so we would swap the sign of dx
which would now be 5. Now say the next frame’s deltaTime
is 5. Using the same computation, this.x += 5*5
or this.x = -45 + 25
or this.x = -20
. Uh oh… this is still below our minumum minx
, so if we were just swapping the sign, we would send the particle back in the wrong direction. I actually had this bug when I first implemented ;p.
Final step is to figure how to actually draw Pete. Using Javascript primitives, this is actually pretty easy. Pete is just a filled circle with a specific color, so we’ll use the Arc primitive:
Let’s break this down. First renderCtx.beginPath()
(reference: BeginPath) is used to start a new rendering path in Javascript. Rendering paths in Javascript allow you to build out a shape using multiple primitives (like lines, arcs, rectangles, etc) before doing a single render/draw call for the entire path you’ve laid out. Here, we’re only using a single primitive, so it doesn’t buy us much, but is still needed to setup our render state. renderCtx.arc(this.x, this.y, this.size, 0, Math.PI*2)
is where we are telling Javascript to draw our filled circle. We pass in Pete’s position using this.x, this.y
, this.size
is the radius of the circle (in pixels) which we set in our constructor, and the 0, MathPI*2
is used to identify the start and end angles (in radians) of the arc to draw (0 to 2*PI is a full circle). renderCtx.fillstyle = this.color
is used to set the color of the circle (reference: FillStyle), which can be named colors like red
or black
, RGB hex values like #808080
for a gray, or a RGB color string like rgb(127,127,127)
(reference: CSS Color Value). And finally, the renderCtx.fill()
call renders the path, our circle, to the canvas (reference: Fill).
Putting It Together
The last piece of the puzzle is the initialization code for Pete. You’ll note that in the first introduction to Pete and the canvas below, Pete starts at a random location and heads off in a random direction and a fixed speed. We also need to define what the fence parameters should be. Let’s start with the fence, as it’s easiest. We will set Pete’s boundaries to simply be the boundaries of the canvas we are using. So spec = { minx: 0, miny: 0, maxx: canvas.width, maxy: canvas.height }
will do fine. Starting position also is easy. We’ll pick a random number between 0 and 1 (using Math.random()
) and multiply by the canvas height and width, as that’s the bounds we are using for Pete. For handling movement in a random direction but at constant speed. I pick a speed, say 200 (measured in pixels per second), then pick a random angle. To get the x, y deltas, I simply use Math.sin(angle)
and Math.cos(angle)
. Let’s put this all in a function:
By putting this in a function, we can now call it multiple times to get multiple particles, all following the same rules. The only thing we haven’t accounted for is keeping track of our Pete particles. In the above function there is a petes.push()
call. I’m using an array named petes, allocated by using var petes = [];
. So that will need to be declared prior to calling our makePete()
function:
This handles creating five Pete particles (yay, he now has friends!). But we need to update our update()
and render()
functions accordingly:
So now we have Pete and a couple of friends. Use the +friend button to add more.
I’ll stop here for now, as I ended up being long winded in explaining everything. And there’s still quite a bit of ground to cover. Hopefully I’ve outlined some basic concepts: a particle really is some data and rules for updating and rendering. Logic for initialization lies outside the particle and that we need to be able to hook to the main game loop. We still need to get to the “fire and forget” point I raised at the beginning of the article. And I still want to cover more on the creation and expiration of particles and ways to handle that. And while I’m sure Pete is a fine particle, there’s a lot more we can do and I’ll provide some more exciting examples. Look for these topics to be covered in more detail in coming posts.
The full code for this example is below, or you can play around with it for yourself using this CodePen.
Tylor Allison GENERAL
gamedev, js, particles, procgen