This document provides guidance on creating a basic, customizable particle system using the Hengine web game engine and the JavaScript programming language. In particular, it covers the basics of 2D particle systems, including their graphics, physics, and implementation in one engine. By the end of this process, you will have created a particle system similar to the one shown in Figure 1. Notably, this does not discuss JavaScript syntax or semantics, nor the fundamentals of computer graphics or classical physics.
The fundamental concept behind creating a particle effect like the one in Figure 1 is that of the "particle spawner". A particle spawner is an imaginary "object" which exists at a given location and emits particles over time. Each particle then becomes an independent entity with its own motion and behavior which (in most cases) eventually decays and is deleted. By repeating this process, an infinite stream of particles can be created without consuming infinite memory and processing power.
You need a few things before you can get started. Unfortunately, some of these take months or years to acquire. The prerequisites are listed below, in decreasing order of difficulty:
If you don't meet the first few requirements but are interested in doing so, the websites MDN and Learn OpenGL are excellent resources for learning the basics of JavaScript and computer graphics.
Now that you have gathered your supplies, you can begin the project. The process is divided into four broad phases. An overview of those phases is given here, but if you are okay with surprises, you can safely skip this summary.
Let's begin!
Creating a new project in the Hengine is extremely simple if you have consistent internet access:
.html extension (e.g. myParticles.html) using your text editor of choice.<script src="https://elkwizard.github.io/Hengine/Hengine.js">
// this is where the rest of your code will go
</script>
Keep this browser tab open for the duration of the project. If you make changes to your code and the program seems to behave the same, remember to save your file and reload the tab. Additionally, if things don't seem to be working properly, open the developer console (with Right Click + Inspect, Ctrl/Cmd + Shift + J, or F12) to see possible error messages.
If you do not have consistent internet access, then your program will fail to run when using the steps above. If you have Git installed, you can avoid this by running git clone https://www.github.com/Elkwizard/Hengine.git to get a local copy of the engine. Then, update the above HTML to reference the Hengine.js file in the root of your local copy, rather than the one at https://.... This avoids the need for repeated network requests.
In the Hengine, the world consists of a set of WorldObjects, each of which can appear on the screen and respond to various events from the user or the engine. Collections of event-responses or "behaviors" are encapsulated into the ElementScript class. To create your particle system, you will create an object to act as a spawner, and then give it the Hengine's built-in particle management behaviors.
<script> tags of your HTML file from phase 1:const spawner = scene.main.addElement(
"particleSpawner",
width / 2, height / 2
);
The second and third arguments to addElement() specify the x- and y-coordinates of the object, which in this case is the middle of the screen.spawner.scripts.add(PARTICLE_SPAWNER, {
// options will go here
});
After completing this phase and reloading the page, you should still just see a white page. This is expected behavior! The spawner is invisible and so are its particles, so there's nothing to see yet.
Now that you have a particle spawner, invisible though it may be, you've reached the fun part! The rest of this guide will be spent in the object literal argument to spawner.scripts.add(), which is used to provide custom particle behavior.
Add a function called draw to the object literal to render your particles. It needs two arguments:
renderer: This provides methods for drawing shapes on the screen.particle: This is an object representing an individual particle. This draw method is be called every frame for every particle, and should render that particle to the screen.A good starting point is this function, which draws each particle as a black filled-in circle with a 5-pixel radius. If you have done everything correctly, you see a small black dot (Figure 2).
draw(renderer, particle) {
renderer
.draw(Color.BLACK)
.circle(particle.position, 5);
},
Give your particles velocity by creating an init function in the object literal. init is be called to initialize each particle when it first spawns. The init shown below gives each particle a velocity in a random direction with a speed between 3 and 5 pixels per frame. After reloading, you now see moving dots (Figure 3).
init(particle) {
particle.velocity = Vector2.polar(
Random.angle(),
Random.range(3, 5)
);
},
Give your particles variation by adding custom per-particle data. Each particle object has an embedded .data object which can be used to store whatever you want. To wrap up this phase, add .color and .radius data to your particles in init, and then use that information in draw. By again using Random, we can give each particle a random color and a radius between 2 and 8 pixels. After implementing this correctly, you should see something very similar to Figure 1!
// in init (after current code)
particle.data.color = Random.color();
particle.data.radius = Random.range(2, 8);
// in draw (replacing current code)
renderer
.draw(particle.data.color)
.circle(particle.position, particle.data.radius);
For more information on the particle system API, as well as the available methods on renderer and particle, see the Hengine Documentation. In particular, information about renderer can be found under Artist2D, and information about particle can be found under PARTICLE_SYSTEM.
If you like your particle system, but want to make it more unique, this section provides several strategies for exploring more elaborate graphical possibilities. However, this section is unordered and optional. Feel free to complete any of these tasks that appeal to you, in any order.
To make your particles fall, simply:
falls: true, to the object literal, alongside init and draw.// values between 0.1 and 1 typically work best
scene.physics.gravity.y = 0.1;
// the rest of the file (no changes needed)
const spawner = ...
spawner.scripts.add(PARTICLE_SPAWNER, ...
If you have completed these steps correctly, your particles are now moving in downward parabolic arcs, as shown in Figure 5.
To make your particles fade, you need information about where they are in their life cycle. This comes in the form of particle.timer, a value which interpolates from 0 to 1 over the course of the particle's life. Thus, to make the particles fade:
renderer.alpha = 1 - particle.timer; to the beginning of draw's body. This causes the opacity to linearly decrease over the particle's lifetime.Interpolation class. For example, the following code concentrates the fade-out later in the particle's life:renderer.alpha = 1 - Interpolation.increasing(particle.timer);
If you have followed these steps correctly, your particles now gradually fade out rather than vanishing.
If you have been exploring various other options of the particle system, this example code may produce an unsightly "popping" effect where nearly-invisible particles reappear for a single frame. This is due to floating-point errors and can be avoided by wrapping your 1 - ... in Math.max(0, 1 - ...).
The Hengine's support for many non-circle shapes can be found under Artist2D, DrawRenderer, and StrokeRenderer. To get started with them, the following example explains how to transform your particles into small "vectors" pointing in their direction of motion.
draw(). Conveniently enough, this is just the direction (normalization) of particle.velocity:const direction = particle.velocity.normalized;
renderer.draw...circle statement with a renderer.stroke...arrow statement. You can re-use your custom data.radius property to make the arrows have varying lengths:// in draw (replacing renderer.draw...)
// length is scaled up to be more visible
const length = particle.data.radius * 3;
const start = particle.position;
const end = start.plus(direction.times(length));
renderer
.stroke(particle.data.color)
.arrow(start, end);
If you have followed these steps correctly, you now have a colorful vector field as shown in Figure 6!
One good way to diversify the appearance of your particles is to change their movement after they spawn. This is achieved in the Hengine through the use of an update(particle) function which is called on each time step. Within this function, the entire JavaScript language and Hengine libraries at your disposal. Below is one example of this function:
update(particle) function to the object literal.update(particle) {
// this is run every frame
},
update(particle):// in update
if (Random.bool(0.1)) // 10% chance each frame
particle.velocity.angle = Random.angle();
If you have followed these steps correctly, you now have a cloud of redirecting particles, as seen in Figure 7 (shown with the previous section's custom shapes). Crucially, this is just one example; there are infinitely many ways to use update and experimentation is a great way to learn them all.