loading pixels tweaking margins squinting eyes edging borders shifting elements correcting my posture

Recreating Games in Code (P5.js) – Asteroids

8 April 2024 · Recreates

I’m on a personal development journey. I’m passionate about games, and I want to learn the fundamentals to create engaging experiences on the web and in players’ hands. That learning journey has to start somewhere, so what better place than trying to recreate a classic, simple, game as Asteroids.

Spoiler alert: It was not simple, and I fell victim to feature-creep!

The objective of Asteroids is to destroy asteroids. The player controls a triangular ship that can rotate left and right, fire shots straight forward, and thrust forward. Once the ship begins moving in a direction, it will continue in that direction for a time without player intervention unless the player applies thrust in a different direction. The ship eventually comes to a stop when not thrusting. The player can also send the ship into hyperspace, causing it to disappear and reappear in a random location on the screen, at the risk of self-destructing or appearing on top of an asteroid.

Wikipedia

So, what do I need to get started? Aside from needing a graphics library – I’m using P5.js which allows quick and easy creative coding on an HTML5 canvas element – I need to determine which objects and math I need to recreate Asteroids in it’s most simple form:

  • A ship object
  • Many asteroid objects
  • Many bullet/projectile objects
  • Newton’s second law (F = m * a)
  • Vector Math
  • Collision Detection
  • Spawning Algorithm

The Ship

Drawing the ship was simple, a quick triangle located at its position, and the rotation based on the velocity heading. I did have to offset the starting angle by -PI/2, as by default an angle of 0 faces right instead of up. The LEFT and RIGHT arrows control changing the vector angle, and the UP and DOWN arrow adjusts the acceleration.

To give the ship a floaty-movement feel, as if low gravity, once a key was released, instead of setting the movement direction to 0, I dampened the value by multiplying the vector by 0.99 until it reached 0 on its own through enough draw() calls which happen every frame of animation. Ranging from 0.0 to 1.0, a higher dampening value created more drifting, and a lower value created less; less control vs. more control.

Asteroids

Asteroids are an Array, as we’re potentially holding many of these objects at once. Each asteroid has a random position, random speed, and random velocity. Each asteroid moves in this direction until being hit by a projectile, or hitting the player Ship.

To draw the asteroids, this was a neat little trick of using Polar to Cartesian coordinates and taking n points around a position, offset by the size (or radius) of the asteroid, offset by a displacement map which also was an array of values from -1.0 to 1.0. The displacement map had to be a predefined array instead of generated random numbers the fly, as each call of draw() would create new values and cause a visual jitter – something you might desire if making a game about shooting viruses?

let angle = TWO_PI / npoints; 
let dIndex = 0;
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let displace = 1;
if(displacement) {
displace = radius * displacement[dIndex];
}
let sx = x + cos(a) * radius + displace;
let sy = y + sin(a) * radius + displace;
vertex(sx, sy);
dIndex++;
}
endShape(CLOSE);

Projectiles

Like the Asteroids, the Bullets were also an Array as we’re potentially having many at once.

The bullets, or projectiles, were parented to the Ship object. This was so I could spawn them from the ship’s position and give them a forward velocity. This forward velocity is the same as the heading of the ship’s velocity, meaning wherever the ship looks is where a new bullet is spawned.

To handle too many bullets (something that can often slow down or crash a browser) I had to check which were still needed. In this scenario, we know a projectile is no longer in use once it’s left the playable screen or has hit an object. But how do we check if it’s hit an object? Circle colliders!

Newton’s Law of Motion (F = m * a)

Throughout this challenge, I was completely ignoring Acceleration. Why was it needed, when I could set velocity directly? Well, it turns out it’s required to create the feeling of accelerating up to a speed, instead of immediately being that fast. Who’d have thought!

There’s no concept of mass in my game world, so we can further simplify Netwon’s equation to F = 1 * a, or F = a.

For each frame of animation, we add Acceleration to Velocity, and Velocity to Position. Using Limit() to set the max speed, I created a ship that accelerates believably until hitting a max speed, and changing the direction to reverse, instead of immediately going in the other direction, first slowed it’s forward movement until beginning to reverse.

this.velocity.add(this.acceleration);
this.velocity.mult(this.movingDampening);
this.velocity.limit(this.movingSpeed);
this.pos.add(this.velocity);

Vector Math

This was the most challenging to understand. How can adding or subtracting two vectors create a new angle? Well, it turns out most of the hard math is done for you in Vector objects, so ensure you use Vector.add() or Vector.sub() as these create the expected new vector.

Why did I need this? As the game progressed, I found there was a rare chance the Asteroid’s velocity could be parallel to the edge of the screen, and as I spawned them slightly offscreen (this.pos – this.size*2) they could indefinitely keep moving forward without ever being seen by the player.

I needed to track the player Ship position, and slightly influence each asteroid to track towards the player. I achieved this by subtracting the Ship.position and Asteroid.position to create a new Vector pointing at the Ship.

this.acceleration = p5.Vector.sub(ship.pos, this.pos);
this.acceleration.normalize();
this.acceleration.mult(0.01);

The above code adds a small attraction force to the Asteroid’s velocity pointing toward the target, which in this case was the ship. It was important to normalize() the vector though, as the magnitude of that vector was equal to the distance between both positions, which would have equated to a larger acceleration in that direction.

Collision Detection

How was I going to calculate overlap? Well, Vector objects also have a function Vector.dist(vector1, vector2) to calculate the distance between two Vectors, and as all my objects have a defined size or radius, I could work out if 2 shapes were overlapping assuming a circular collider by a simple equation:

let d = p5.Vector.dist(this.pos, other.pos);
return (d < this.size + other. size);

We know if two objects are overlapping if the distance between them is less than both their radii added together.

Feature Creep 👀

I wasn’t intending to add more, but ideas came thick and fast! A Wave Manager to incrementally add more enemies after defeating the current wave, a Score Counter, and Power Ups (which are enabled by hitting hot-keys for now) were some of the features added to improve the overall experience.

Wave Manager: For each wave we increment the number of enemies that spawn by using 1 + floor( log(this.wave) * 5). We know a wave has completed once there are no enemies left alive on the screen.

Score Counter: Each defeated Asteroid increments the score by +10. On death, we reset the player Ship, Wave Manager, and Score Counter.

Power Ups: Who doesn’t love more bullets? I created 3 powerups, a Split Shot, a Radial Shot, and a Homing Missile.

To create a homing missile, similar to how the asteroids track the player ship, we instead loop through all the asteroids and find the closest one to the bullet. If it is the closest, we use that same equation to influence the bullet to accelerate toward that direction.

let enemy = wavemanager.enemies[bestIndex].pos.copy();
this.acceleration = p5.Vector.sub(enemy, this.pos);
this.acceleration.normalize();
this.acceleration.mult(strength);

Let's talk - 3D rendered graphic, bubble text

15 years of experience in one inbox, ready for your message.

I'm a Kent-based freelance brand designer and website developer with 15 years of experience, bringing creativity and technical expertise to clients across the UK and beyond.