java script building a game

Time to get started on the core components of the game engine. We should be able to render something on the Canvas before the end of this article!

Note: please be aware that I’ll add links to specific pieces of code in the GIT repository history tree. This will make it easier for you to run the same code I did when describing a specific feature.

We’ll start by working on the classes that have no dependencies like Game, AssetLoader, and EventDispatcher then progress onto others that might extend these.

The Game class could:

  • accept a Scene as an optional constructor argument
  • keep a reference to all created scenes
  • have public methods for adding and removing scene(s)
  • have a public loop() method which will trigger the update() method on all stored scenes
  • have public methods that pause and resume the loop cycle; we’ll call these: play(), pause() and togglePause()
export default class Game {
  constructor(scenes) {
    // check if the browser supports requestAnimationFrame
    if(!(typeof requestAnimationFrame)) {
      throw new Error('Your browser doesn\'t support requestAnimationFrame :(');
    }
    
    this.scenes = [];
    this.addScenes(scenes);
    this.play();
    this.loop();
  }
  addScenes(elem) {
    if(elem.constructor === Array) {
      this.scenes = this.scenes.concat(elem);
    } else {
      this.scenes.push(elem);
    }
  }
  removeScene(elem) {
    let idx = this.scenes.indexOf(elem);
    if(idx > -1) {
      this.scenes.splice(idx, 1);
    }
  }
  pause() {
    this.running = false;
  }
  play() {
    this.running = true;
  }
  togglePause() {
    this.running = !this.running;
  }
  loop() {
    if(this.running) {
      for(let scene of this.scenes) {
        scene.update();
      }
    }

    requestAnimationFrame(this.loop.bind(this));
  }
};

There are some improvements to be made here like checking if the added scenes are actual Scene instances, adding support for browsers that don’t have requestAnimationFrame, and maybe implementing a flux architecture to store data and prevent state mutation.

The flux architecture might come at a later time but for now, we’re treating the engine as a P.O.C. (proof of concept). That implies that I also won’t bother adding support for older browsers. But you can definitely use the rAF polyfill if you need it.

The AssetLoader class could:

  • be a singleton so we can retrieve the assets from any other class
  • expose a load method that will receive an array of paths to local assets; the method should return a Promise so we can detect when all assets are loaded
  • store the assets based on their name converted to camel case
  • support loading image and audio files
import Utils from './Utils';

let assetLoaderInstance;

export default class AssetsLoader {
  constructor() {
    if(!assetLoaderInstance) {
      console.log('AssetsLoader instance created');
      this.assets = {};
      assetLoaderInstance = this;
    }

    return assetLoaderInstance;
  }
  load(assets) {
    let self = this;
    let imageFiles = /jpe?g$|gif$|png$|svg$/;
    let audioFiles = /wav$|mp3$/;
    let files = [];
    let details;
    let ext;
    let name;
    let file;

    for(let asset of assets) {
      details = asset.split('/').pop().split('.');
      ext = details.pop();
      name = details.shift();
      if(ext.match(imageFiles)) {
        // load an image file
        file = new Image();
        file.src = asset;
      } else if(ext.match(audioFiles)) {
        // load an audio file
        file = new Audio(asset)
      }
      files.push(file);

      this.assets[Utils.camelize(name)] = file;
    }

    return Promise.all(files).then(function() {
      return self.assets;
    });
  }
};

You would generally implement a getInstance() method on a singleton class, here we’re simply returning the previously created instance whenever we’re calling the class constructor.

We also have a dependency here on a Utils class, it currently just handles the asset “name to camel case” logic; it looks like this:

export default class Utils {
  constructor() {
    let e = new Error('is a static class, no need to instantiate!');
    e.name = 'Utils';

    throw e.toString();
  }

  static toCamelCase(str) {
    return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
      return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
    }).replace(/(\s|-|_)+/g, '');
  }
};

This one is actually a static class, and to enforce that we’re simply throwing an error in its constructor. That means trying to call new Utils() will fail. The class is fairly simple for now as it just has a single static method; we’ll probably add more later.

The EventDispatcher class could:

  • be a singleton, as all game classes need to be able to reference the same EventDispatcher instance. This is generally frowned upon as its basically a global event dispatcher but we’ll need it if we want to implement the flux architecture later on.
  • expose the on() and trigger() methods which will help us subscribe to and emit events.
let pubSubInstance;
const subjects = {};
const hOP = subjects.hasOwnProperty;

class PubSub {
  constructor() {
    if(!pubSubInstance) {
      console.log('EventDispatcher instance created');
      pubSubInstance = this;
    }

    return pubSubInstance;
  }

  on(topic, callback) {
    if(!hOP.call(subjects, topic)) {
      subjects[topic] = [];
    }

    let index = subjects[topic].push(callback) - 1;

    return {
      remove: () => {
        delete subjects[topic][index];
      }
    }
  }
  trigger(topic, data) {
    if(!hOP.call(subjects, topic)) return;

    subjects[topic].forEach((entry) => {
      if(entry) {
        entry(data !== undefined ? data : {});
      }
    });
  }
};

export default class EventDispatcher {
  constructor() {
    pubSubInstance = new PubSub();
  }
  on() {
    pubSubInstance.on.call(pubSubInstance, ...arguments);
  }
  trigger() {
    pubSubInstance.trigger.call(pubSubInstance, ...arguments);
  }
}

You might think this one looks weirder than the rest, and there’s a good reason for that. It’s a bit tricky to extend a singleton using the ES6 syntax, read my article describing the issue here.

We’re not done yet; we still have a bit of coding to do before we can draw anything. According to our schema in the previous article we can now focus on implementing the Keyboard class which extends EventDispatcher.

The Keyboard class could:

  • be a singleton
  • expose getters which tell us if a certain key is pressed

It should be sufficient to expose getters for the keys A to Z, space, arrow keys, tab, enter, shift, ctrl, alt, esc and functional keys F1 to F12.

import { KeyboardEvents, EventDispatcher } from '../'

let keyboardInstance;
let keysMap = {};

class KeyboardSingleton {
  constructor() {
    if(!keyboardInstance) {
      console.log('Keyboard instance created');
      keyboardInstance = this;
    }

    return keyboardInstance;
  }
  handleKeys(e) {
    if(e.type === KeyboardEvents.KEY_DOWN) {
      keysMap[e.keyCode] = true;
      this.trigger(KeyboardEvents.KEY_DOWN, e);
    }

    if(e.type === KeyboardEvents.KEY_UP) {
      this.trigger(KeyboardEvents.KEY_UP, e);
      keysMap[e.keyCode] = false;
    }
  }
};

export default class Keyboard extends EventDispatcher {
  constructor() {
    super();
    keyboardInstance = new KeyboardSingleton();
    onkeydown = onkeyup = onkeypress = keyboardInstance.handleKeys.bind(this);
  }

  get A() {
    return !!keysMap[65];
  }
  get B() {
    return !!keysMap[66];
  }
  get C() {
    return !!keysMap[67];
  }
  get D() {
    return !!keysMap[68];
  }
  get E() {
    return !!keysMap[69];
  }
  get F() {
    return !!keysMap[70];
  }
  get G() {
    return !!keysMap[71];
  }
  get H() {
    return !!keysMap[72];
  }
  get I() {
    return !!keysMap[73];
  }
  get J() {
    return !!keysMap[74];
  }
  get K() {
    return !!keysMap[75];
  }
  get L() {
    return !!keysMap[76];
  }
  get M() {
    return !!keysMap[77];
  }
  get N() {
    return !!keysMap[78];
  }
  get O() {
    return !!keysMap[79];
  }
  get P() {
    return !!keysMap[80];
  }
  get Q() {
    return !!keysMap[81];
  }
  get R() {
    return !!keysMap[82];
  }
  get S() {
    return !!keysMap[83];
  }
  get T() {
    return !!keysMap[84];
  }
  get U() {
    return !!keysMap[85];
  }
  get V() {
    return !!keysMap[86];
  }
  get W() {
    return !!keysMap[87];
  }
  get X() {
    return !!keysMap[88];
  }
  get Y() {
    return !!keysMap[89];
  }
  get Z() {
    return !!keysMap[90];
  }
  get SPACE() {
    return !!keysMap[32];
  }
  get UP() {
    return !!keysMap[38];
  }
  get DOWN() {
    return !!keysMap[40];
  }
  get LEFT() {
    return !!keysMap[37];
  }
  get RIGHT() {
    return !!keysMap[39];
  }
  get TAB() {
    return !!keysMap[9];
  }
  get ENTER() {
    return !!keysMap[13];
  }
  get SHIFT() {
    return !!keysMap[16];
  }
  get CTRL() {
    return !!keysMap[17];
  }
  get ALT() {
    return !!keysMap[18];
  }
  get ESC() {
    return !!keysMap[27];
  }
  get F1() {
    return !!keysMap[112];
  }
  get F2() {
    return !!keysMap[113];
  }
  get F3() {
    return !!keysMap[114];
  }
  get F4() {
    return !!keysMap[115];
  }
  get F5() {
    return !!keysMap[116];
  }
  get F6() {
    return !!keysMap[117];
  }
  get F7() {
    return !!keysMap[118];
  }
  get F8() {
    return !!keysMap[119];
  }
  get F9() {
    return !!keysMap[120];
  }
  get F10() {
    return !!keysMap[121];
  }
  get F11() {
    return !!keysMap[122];
  }
  get F12() {
    return !!keysMap[123];
  }
};

This class also uses KeyboardEvents, which looks like this:

export default class KeyboardEvents {
  static get KEY_UP() {
    return 'keyup'; // actual event name, don't change
  }
  static get KEY_DOWN() {
    return 'keydown'; // actual event name, don't change
  }
};

This class holds some static methods which return event names that basically serve as constants. You might be wondering why I didn’t simply do:

export const KEY_UP = 'keyup';

It’s because this approach allows for the import of the class and through that class we gain access to all the Keyboard related static getters. We don’t need to import each and every constant separately.

Since we finally have the Keyboard class, we can focus on the Canvas next.

The Canvas class could:

  • be a singleton
  • be able to resize itself on window resize
  • allow access to the <canvas> tag and all its properties
  • expose the canvas ctx (the 2D context) which is used for drawing
import { Keyboard } from '../';

let canvasInstance;

class CanvasSingleton {
  constructor() {
    if(!canvasInstance) {
      console.log('Canvas instance created');
      let canv = document.createElement('canvas');

      this.canvas = canv;
      this.ctx = canv.getContext('2d');
      document.body.appendChild(canv);
      window.addEventListener('resize', this.resize.bind(this));
      this.resize();

      canvasInstance = this;
    }

    return canvasInstance;
  }
  resize() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
  }
};

export default class Canvas extends Keyboard {
  constructor() {
    super();
    canvasInstance = new CanvasSingleton();
    this.canvas = canvasInstance.canvas;
    this.ctx = canvasInstance.ctx;
  }
};

At this point, we could draw on the Canvas directly by using the ctx property but that’s boring and silly because it wouldn’t be using any of the logic we worked so had to build in the other classes. Let’s focus on building one more class which will help us out, namely: Scene.

The Scene class could:

  • extend Canvas and keep a reference to itself for access in other classes that extend Scene
  • allow for initialization with the x, y, width and height properties which will need to have some defaults
  • allow for adding and removing of DisplayObjects
  • export an update() method which will in turn call the update() method of all stored DisplayObjects
  • clear itself on every update based on the provided x, y, width and height
  • make all drawing positions relative to the Scene instance — this means that if the Scene has { x: 20, y: 20 } and we add a DisplayObject with { x: 0, y: 0 } to it, that DisplayObject will be drawn on the Canvas at { x: 20, y: 20 }
  • hide any elements that are drawn outside of its viewport which is defined by x, y, width and height
  • provide a static wrap() method which will reset an objects x and y position if it exceeds the Scene boundaries
import Canvas from './Canvas';

export default class Scene extends Canvas {
  constructor(x=0, y=0, width, height) {
    super();
    this.children = [];
    this.x = x;
    this.y = y;

    // store initial width and height
    this.initWidth = width;
    this.initHeight = height;
    this.width = width;
    this.height = height;
  }
  add(elem) {
    if(elem.constructor === Array) {
      for(let el of elem) {
        el.scene = this;
        this.children.push(el);
      }
    } else {
      elem.scene = this;
      this.children.push(elem);
    }
  }
  remove(elem) {
    let idx = this.children.indexOf(elem);
    if(idx > -1) {
      this.children.splice(idx, 1);
    }
  }
  update() {
    // resize scene to fit canvas if no initial size was provided
    this.width = this.initWidth || this.canvas.width;
    this.height = this.initHeight || this.canvas.height;

    // clear screen
    this.ctx.clearRect(this.x, this.y, this.width, this.height);

    // clip screen size
    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.rect(this.x, this.y, this.width, this.height);
    this.ctx.stroke();
    this.ctx.clip();

    // translate canvas to screen x and y
    // to make all drawings' start positions, relative to this screen
    this.ctx.translate(this.x, this.y);

    // update and render all scene elements
    for(let elem of this.children) {
      if(elem.update) {
        elem.update();
      }
      if(elem.render) {
        elem.render();
      }
    }

    // close path opened for screen clip and restore
    this.ctx.closePath();
    this.ctx.restore();
  }

  static wrap(object) {
    let width = (object.width || object.size);
    let height = (object.height || object.size);

    if(object.x > object.scene.width) {
      object.x = -width;
    } else if (object.x < -width) {
      object.x = object.scene.width;
    } else if(object.y > object.scene.height) {
      object.y = -height;
    } else if (object.y < -height) {
      object.y = object.scene.height;
    }
  }
};

We have a Scene and based on its logic we can add an element to it, which will be updated and rendered… provided that the element has the update() and render() methods.

And now for the moment we’ve all been waiting for; this is how you draw a couple of moving squares the super complicated way…

Create a new folder in the js directory (on the same level as the engine directory) and name it square. Create an index.js file with this logic:

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

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

let movementMultiplier = 1;

class Square extends Scene {
  constructor(x=0, y=0, size=40, color='#900') {
    super();
    this.x = x;
    this.y = y;
    this.size = size;
    this.color = color;
  }
  update() {
    this.x += 1 * movementMultiplier;
    this.y += 1.5 * movementMultiplier;
    if(this.SHIFT) {
      movementMultiplier = 2;
    } else {
      movementMultiplier = 1;
    }
    Scene.wrap(this);
  }
  render() {
    this.ctx.save();
    this.ctx.beginPath();
    this.ctx.fillStyle = this.color;
    this.ctx.fillRect(this.x, this.y, this.size, this.size);
    this.ctx.closePath();
    this.ctx.restore();
  }
};

// create scenes
const sceneA = new Scene(20, 33, 400, 200);
const sceneB = new Scene(sceneA.x+sceneA.width+10, 33, 400, 200);

// create squares
const squareA = new Square();
const squareB = new Square(130, 100, 20, '#090');

// add scenes to game
const game = new Game([
  sceneA,
  sceneB
]);

// add squares to scene
sceneA.add(squareA);
sceneB.add(squareB);

For the sake of brevity I created the Square class in the same file but we now have a basic engine for creating and rendering static or moving objects on the canvas.

The squares move on the x and y axis at a predefined speed and if you press SHIFT, that speed doubles. The objects’ position is wrapped to the parent Screen so you can play around with it. Don’t forget that you’re inheriting all the Keyboard functionality there, so have fun with that.

Final Thoughts

Grab the files and test them out here:

Obviously, there is room for improvement, like using “const” instead of “let” for all singleton instances, figuring out how to use private variables in classes that have multiple instances without causing overwrites (and if you’re thinking “WeakMap”… that will probably do more harm than good) and using a store for our game engine; just to mention a few.

We’ll continue with a discussion on how movement works in games, cover sprite sheets, sprites, animated sprites, controlling frame rate, display object pivot points, and another implementation example in the next article.

Radu B. Gaspar

The original article: here.