It’s time to cover asteroid generation. I bet you never thought playing with rock would be fun… well, you were wrong!

Before we code, let’s visualize and put into words what we want to achieve:

  • we want a class that will draw an asteroid (stroke of genius right here)
  • it should be drawn randomly within a given set of parameters. Seeing the same rock over and over again will get a tad boring really quick.

In terms of actual steps, such an object can be drawn as follows:

drawing approach game development java script

asteroid drawing approach

It all starts with two concentric circles. You can think of these as two simple parameters that our class will receive; a minimum and maximum radius if you will.

In step two, we split the circle evenly into slices. The number of slices will give us the asteroid “resolution”… its granularity. The bigger the number, the finer our rock details will be. Increasing this number also comes at a performance cost.

Moving on to steps three and four, we show the area of interest. We want to generate a set of random points within that range on our slices. To be more exact, each of those points will be positioned at a radius greater than or equal to the minimum radius we provide, but not greater than the maximum radius.

Steps five and six represent the easy part. Connecting those points with a line and applying a fill color, resulting in a randomly generated asteroid. Pretty neat huh?

Calling this class will create a plethora of rocks shapes but be careful of those min and max radius values… you don’t want your asteroids looking too pointy; or do you? We’ll keep the logic above in a RockModel class which will extend VerletModel:

import { VerletModel } from '../../engine';

export default class RockModel extends VerletModel {
  constructor(minRadius=30, maxRadius=40, granularity=25) {
    super();

    let tau = Math.PI*2;
    let increment = tau / granularity;
    let points = [];
    let xMin;
    let xMax;
    let yMin;
    let yMax;
    let radius;
    let x;
    let y;

    for (let ang = 0; ang < tau; ang += increment) {
      radius = this.getRandom(minRadius, maxRadius);
      x = Math.sin(ang) * radius;
      y = -Math.cos(ang) * radius;

      xMin = xMin !== undefined ? Math.min(xMin, x) : x;
      xMax = xMax !== undefined ? Math.max(xMax, x) : x;
      yMin = yMin !== undefined ? Math.min(yMin, y) : y;
      yMax = yMax !== undefined ? Math.max(yMax, y) : y;

      points.push({x, y});
    }

    // close the rock shape by connecting
    // final point to starting point
    points.push(points[0]);

    this.temporaryX = 0;
    this.temporaryY = 0;
    this.angle = 0;
    this.color = '#aaa';
    this.rotateBy = Math.random()*0.03 - 0.015 || 0.01;
    this.vx = Math.random() - 0.5;
    this.vy = Math.random() - 0.5;
    this.width = xMax - xMin;
    this.height = yMax - yMin;
    this.radius = minRadius;
    this.points = points;
    this.xMin = xMin;
    this.xMax = xMax;
    this.yMin = yMin;
    this.yMax = yMax;
  }

  update() {
    this.temporaryX = this.x;
    this.temporaryY = this.y;

    this.x += this.vx;
    this.y += this.vy;
    this.angle += this.rotateBy;

    // set the previous positions to temporary values
    this.previousX = this.temporaryX;
    this.previousY = this.temporaryY;
  }

  getRandom(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
  }
};

We’re also doing some extra work to calculate the width and height of the resulting rock based on the points we generate. The rock will have a (very small) random rotation and velocity applied to it.

Since we want the rock randomly positioned in the scene, and since that scene instance is not available within the models, we’ll set its x and y positions in the Rock class.

The actual display object Rock class will look like this:

import { Scene, DisplayObject } from '../../engine';

export default class Rock extends DisplayObject {
  constructor(model) {
    super();

    this.model = model;
  }

  update() {
    if (!this.model.x) {
      this.model.x = Math.random() * this.scene.width;
    }
    if (!this.model.y) {
      this.model.y = Math.random() * this.scene.height;
    }
    this.model.scene = this.scene;
    this.model.update();
    Scene.wrap(this.model);
  }

  render() {
    const { x, y, angle, points, color } = this.model;

    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.translate(x, y);
    this.ctx.rotate(angle);
    this.ctx.lineWidth = 2;
    this.ctx.strokeStyle = '#111';
    this.ctx.fillStyle = color;

    for(let i=0; i<points.length; i++) {
      if(i === 0) {
        this.ctx.moveTo(points[i].x, points[i].y);
      } else {
        this.ctx.lineTo(points[i].x, points[i].y);
      }
    }

    this.ctx.fill();
    this.ctx.stroke();
    this.ctx.closePath();
    this.ctx.restore();
  }
};

It just renders the lines between all those points which it gets from the model.

By this point it should already be clear to everyone that working with scripted shapes is more processor intensive that working with sprites. While traversing a few points and drawing lines between them is no big deal, it quickly becomes one if you’re working with thousands of such objects. Long story short, this won’t scale well… so consider yourself warned.

We said we wanted an asteroid field, much like the star field we already have. Those rock move, but their movement needs to be affected by the space ship velocity, so let’s do that next:

import { Scene, DisplayObject } from '../../engine';
import Rock from './Rock';
import RockModel from '../models/RockModel';

export default class RockField extends DisplayObject {
  constructor(model, amount=10) {
    super();
    this.model = model;
    this.amount = amount;
    this.rocks = [];
  }

  generateRocks() {
    const { amount, rocks } = this;
    for (let i = 0; i < amount; i++) {
      const rockModel = new RockModel();
      const rock = new Rock(rockModel);
      rocks.push(rock);
    }

    this.add(rocks);
  }

  update() {
    // render all children
    super.update();

    const { rocks, model } = this;

    if (!rocks.length) {
      this.generateRocks();
    }

    for (let i=0; i < rocks.length; i++) {
      // move rocks based on model velocity
      rocks[i].model.x -= model.vx;
      rocks[i].model.y -= model.vy;
    }
  }
}

We want our Map to track these new objects, but it doesn’t really know how to do that yet. We also want the map to accept DisplayObject classes and if any of them have children, it should recursively iterate and display them.

import { DisplayObject } from '../../engine';

export default class Map extends DisplayObject {
  constructor(markerSize=4, mapScaleFactor=0.2) {
    super();
    this.markerSize = markerSize;
    this.mapScaleFactor = mapScaleFactor;
  }

  update() {
    this.mapW = this.scene.width * this.mapScaleFactor;
    this.mapH = this.scene.height * this.mapScaleFactor;
  }

  renderMarker(set) {
    for(let child of set) {
      if (child.children.length) {
        this.renderMarker(child.children);
      } else {
        this.ctx.save();
        this.ctx.beginPath();
        this.ctx.fillStyle = child.model.color;
        this.ctx.rect(...this.getSize(child.model));
        this.ctx.fill();
        this.ctx.closePath();
        this.ctx.restore();
      }
    }
  }

  render() {
    // draw map container
    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.globalAlpha = 0.1;
    this.ctx.fillStyle = '#fff';
    this.ctx.rect(0, 0, this.mapW, this.mapH);

    this.ctx.fill();
    this.ctx.globalAlpha = 1;
    this.ctx.closePath();
    this.ctx.restore();

    this.renderMarker(this.children);

  }
  getSize(target) {
    const { mapW, mapH, mapScaleFactor, markerSize } = this;
    let tx = target.x * mapScaleFactor;
    let ty = target.y * mapScaleFactor;
    let markerW = mapW - tx < markerSize ? mapW - tx : markerSize;
    let markerH = mapH - ty < markerSize ? mapH - ty : markerSize;
    markerW = markerW < 0 ? 0 : markerW;
    markerH = markerH < 0 ? 0 : markerH;

    return [
      tx,
      ty,
      markerW,
      markerH
    ];
  }
};

The only difference here is basically the renderMarker() method which accepts a set of objects as a parameter. If it finds any children within a given set entry, it will iterate over them.

Next up it’s time to actually shoot some bullets. The Player class needs to handle that, as it’s a player specific action. So, we’ll be adding in this piece of code:

import { Scene, DisplayObject, AssetsLoader, KeyboardEvents } from '../../engine';
import Bullet from './Bullet';

export default class Player extends DisplayObject {
  constructor(model) { /* ... */ }

  update() {
    /* ... */ 
    if(this.SPACE) {
      this.model.fire = true;
      this.assets.laserThum.play();
      this.scene.add(new Bullet(this.model));
    }

    // wrap the square to the scene bounds
    Scene.wrap(this.model);
  }

  render() { /* ... */ }
}

As you can see, whenever the SPACE bar is pressed, we’re generating a new Bullet, that class looks like this:

import { Scene, Particle } from '../../engine';

export default class Bullet extends Particle {
  constructor(model) {
    super(3, '#ff0');
    this.x = model.x;
    this.y = model.y;
    this.angle = model.angle + Math.PI*0.5;
    this.speed = 5;
    this.created = Date.now();
    this.lifeTime = 3000;
    this.removeBullet = this.removeBullet.bind(this);
  }

  removeBullet() {
    this.scene.remove(this);
  }

  update() {
    if (Date.now() - this.created >= this.lifeTime) {
      this.removeBullet();
    }
    this.x += Math.sin(this.angle) * this.speed;
    this.y -= Math.cos(this.angle) * this.speed;

    Scene.wrap(this);
  }
}

The Bullet class extends Particle so we don’t need to replicate the render() method. Instead on update() we’re just setting the x and y based on the ships angle as we always want to fire the bullet in the direction the ship’s facing.

We’re also automatically destroying a bullet after 3 seconds. You can also achieve this with a setTimeout() method in the constructor, but we’re not savages 😀 (and it would be nice if everything works on the same frame rate as we said it would).

Putting everything together:

import { Game, Scene, AssetsLoader } from '../engine';
import Player from './objects/Player';
import StarField from './objects/StarField';
import RockField from './objects/RockField';
import Map from './objects/Map';
import PlayerModel from './models/PlayerModel';

require('../../scss/styles.scss');

let assetsLoader = new AssetsLoader();
let scene = new Scene();
let game = new Game(scene);
let playerModel = new PlayerModel();
let player = new Player(playerModel);

// generate a starfield with 300 stars
let starField = new StarField(playerModel, 300);

// generate a rockfield
let rockField = new RockField(playerModel);

// create the game map
let map = new Map();

// track the game objects
map.add([
  player,
  rockField,
]);

let initGame = () => {
  // center player
  playerModel.x = (scene.width - playerModel.size) * 0.5;
  playerModel.y = (scene.height - playerModel.size) * 0.5;

  // render starfield, player and map
  scene.add([
    starField,
    rockField,
    player,
    map
  ]);
};

// load the laser sound
assetsLoader.load([
  './assets/laser-thum.mp3'
]).then(initGame);

You can now fire more bullets than you’ll ever need and with no collision to speak of (yet) it makes for a pretty impressive display. Just fly around a fire away (the SPACE and J keys can be used to fire).

You might notice a small quirk in the rock wrapping logic, as it’s a bit wonky at the moment but we’ll get around to it eventually.

Next, we should probably start discussing collision. This is a larger topic that involves quite a bit of math, so I’m expecting it to span over the course of a few articles. A refresher on vector mathematics is required, so we’ll continue with that.

Radu B. Gaspar

The original article.