Please note: this blog has been migrated to a new location at https://jakesgordon.com. All new writing will be published over there, existing content has been left here for reference, but will no longer be updated (as of Nov 2023)

Javascript Delta

Sat, May 3, 2014

A couple of weeks ago I started a prototype for a horizontal shoot-em-up game in the style of the old c64 classic Delta. It was just a simple weekend project (although it ended up taking 2 - naturally) to allow the player to shoot some swirly alien bad guys…

… but it is in a good enough state to publish the source and let you play now:

Delta

I wasted many, many hours on the original C64 version of delta memorizing the alien patterns and trying to get just that bit further with the Phillip Glass/Pink Floyd inspired music annoying the rest of my family, here’s a few screenshots from the original:

You can read more about it here, or watch someone play the original here, or even read the original Zzap64 review

Limitations

My version is just a delta-style prototype. The graphics are actually ripped from the R-Type arcade game, and the gameplay is limited to just the basics. What’s missing that would make this a real game:

Here are a couple more caveats to admit:

How-it-works

If you’ve followed any of my previous game experiments you might see I’ve re-used many pieces:

Given this basic framework, the source code can concentrate on the game mechanics, starting with some hard coded constants:

var FPS      = 60,
    WIDTH    = 1024,
    HEIGHT   = 768,
    RATIO    = WIDTH/HEIGHT,
    HITBOX   = 5,
    COOLDOWN = 15,
    HSPEED   = 200,
    VSPEED   = 300,
    PLAYER   = { X: 50, Y: 150, W: 56,  H: 28, BULLET_SPEED: 1000 },
    ALIEN    = {                W: 32,  H: 32, BULLET_SPEED: { MIN: 400, MAX: 800 } },
    ROCK     = {                W: 256, H: 128, DX: -500 }

.. and some other tweakable configuration:

var cfg = {

  fpsmeter: { anchor: 'delta', decimals: 0, graph: true, heat: true, theme: 'dark', left: 'auto', right: '-120px' },

  images: [
    { id: "sprites", url: "images/sprites.png" },
    { id: "aliens",  url: "images/aliens.png"  },
    { id: "rocks",   url: "images/rocks.png"   },
    { id: "bullets", url: "images/bullets.png" }
  ],

  sounds: [
    { id: "title",   name: "sounds/title",   formats: ['mp3', 'ogg'], volume: 0.2,  loop: true },
    { id: "game",    name: "sounds/game",    formats: ['mp3', 'ogg'], volume: 0.4,  loop: true },
    { id: "shoot",   name: "sounds/shoot",   formats: ['mp3', 'ogg'], volume: 0.01, pool: 5 },
    { id: "explode", name: "sounds/explode", formats: ['mp3', 'ogg'], volume: 0.05, pool: 5 }
  ],

  state: {
    events: [
      { name: 'boot',    from: ['none'],                 to: 'booting'   },
      { name: 'booted',  from: ['booting'],              to: 'title'     },
      { name: 'start',   from: ['title'],                to: 'preparing' },
      { name: 'play',    from: ['preparing'],            to: 'playing'   },
      { name: 'quit',    from: ['preparing', 'playing'], to: 'title'     }
    ]
  },

  keys: [
    { key: Game.Key.SPACE,                    mode: 'up',   state: 'title',                  action: function() { engine.start();             } },
    { key: Game.Key.ESC,                      mode: 'up',   state: 'playing',                action: function() { engine.quit();              } },
    { key: [Game.Key.UP,    Game.Key.W],      mode: 'down', state: ['preparing', 'playing'], action: function() { player.movingUp    = true;  } },
    { key: [Game.Key.UP,    Game.Key.W],      mode: 'up',   state: ['preparing', 'playing'], action: function() { player.movingUp    = false; } },
    { key: [Game.Key.DOWN,  Game.Key.S],      mode: 'down', state: ['preparing', 'playing'], action: function() { player.movingDown  = true;  } },
    { key: [Game.Key.DOWN,  Game.Key.S],      mode: 'up',   state: ['preparing', 'playing'], action: function() { player.movingDown  = false; } },
    { key: [Game.Key.LEFT,  Game.Key.A],      mode: 'down', state: ['preparing', 'playing'], action: function() { player.movingLeft  = true;  } },
    { key: [Game.Key.LEFT,  Game.Key.A],      mode: 'up',   state: ['preparing', 'playing'], action: function() { player.movingLeft  = false; } },
    { key: [Game.Key.RIGHT, Game.Key.D],      mode: 'down', state: ['preparing', 'playing'], action: function() { player.movingRight = true;  } },
    { key: [Game.Key.RIGHT, Game.Key.D],      mode: 'up',   state: ['preparing', 'playing'], action: function() { player.movingRight = false; } },
    { key: [Game.Key.SPACE, Game.Key.RETURN], mode: 'down', state: ['preparing', 'playing'], action: function() { player.firing      = true;  } },
    { key: [Game.Key.SPACE, Game.Key.RETURN], mode: 'up',   state: ['preparing', 'playing'], action: function() { player.firing      = false; } }
  ],

  ...

};

… some variables to manage our game entities:

var engine,
    renderer,
    sounds,
    player,
    bullets,
    aliens,
    rocks,
    effects,
    stars;

… and a function to start the whole thing up:

function run() {

  engine   = new Engine();
  renderer = new Renderer();
  sounds   = new Sounds();
  player   = new Player();
  bullets  = new Bullets();
  aliens   = new Aliens();
  rocks    = new Rocks();
  effects  = new Effects();
  stars    = new Stars();

  Game.run({
    fps:       FPS,
    fpsmeter:  cfg.fpsmeter,
    update:    engine.update.bind(engine),
    render:    engine.render.bind(engine)
  });

  engine.boot();

}

There is no need to go into detail on most of these objects (at least not in this article) because they are mostly simple and self-explanetary, they each have some initialization code, a simple update() method that moves them based on their current speed, along with a few minor helper methods.

… but I will take a closer look at the engine and the aliens

Game Engine

The game engine is the main game coordinator, it is a finite state machine that manages the transitions between the following states

The game engine provides the event handlers for these state transations, e.g:

var Engine = Class.create({

  // ...

  onboot: function() {
    Game.Load.resources(cfg.images, cfg.sounds, function(resources) {
      renderer.reset(resources.images);
      sounds.reset(resources.sounds);
      engine.booted();
    });
  },

  onstart: function() {
    player.reset();
    bullets.reset();
    aliens.reset();
    rocks.reset();
    effects.reset();
  },

  onenterbooting:   function() { $('booting').show();                                              },
  onleavebooting:   function() { $('booting').hide();                                              },
  onentertitle:     function() { $('title').fadein();  $('start').show(); sounds.playTitleMusic(); },
  onleavetitle:     function() { $('title').fadeout(); $('start').hide();                          },
  onenterpreparing: function() { $('prepare').fadein(); sounds.playGameMusic();                    },
  onleavepreparing: function() { $('prepare').fadeout(); },

  // ...

… along with the high level update() and render() methods called by the game loop:

  update: function(dt) {
    stars.update(dt);
    if (this.isPreparing()) {
      player.update(dt);
      bullets.update(dt);
      rocks.update(dt);
    }
    else if (this.isPlaying()) {
      player.update(dt);
      bullets.update(dt);
      aliens.update(dt);
      rocks.update(dt);
      effects.update(dt);
      this.detectCollisions();
    }
  },

  render: function(dt) {
    renderer.clear();
    renderer.renderStars(dt);
    if (this.isPreparing() || this.isPlaying()) {
      renderer.renderPlayer(dt);
      renderer.renderAliens(dt);
      renderer.renderRocks(dt);
      renderer.renderBullets(dt);
      renderer.renderEffects(dt);
    }
  },

});

In addition to handling events and coordinating the game loop, the engine is also where I chose to (arbitrarily) dump the collision detection code. This is a quick and dirty prototype so code cleanliness is not exactly high priority. The collision detection code is a brute force “compare everything against everything else” algorithm using rectangular bounding boxes. Since the number of entities is pretty small we don’t need the overhead of a spatial index.

The collision detection code is straight forward (comparing bounding boxes) but prototype-ugly so I wont repeat it here. You can read the detectCollisions() method in the source if you must.

Patterns - Sprites in Motion

The primary mechanic in this game is the motion of the alien waves. They can:

The original delta game had much more variety of motion, but I’ve limited this prototype to just these 2 motions. If you allow for transitions between them, along with varying the speed and direction you can provide just enough variety to make the demo interesting.

For the purposes of this demo, we can construct an array of Pattern objects, where each Pattern represents a single wave of aliens that includes the following attributes:

In addition, to declaring the attributes for each pattern, we must also construct the pattern’s movements. Each pattern can have one or more movements, and each movement is a choice between our two motion types:

The straight motion will move in a straight line for n frames at (dx, dy) speed.

The rotate motion will rotate d degrees around a point relative (nx,ny) to the aliens final position from the previous movement at the given speed.

So, for example, the following declares a wave of 8 aliens that move straight across the middle of the screen from right to left (at 500 px/s):

Pattern.construct({ count: 8, alien: 1, x: 'after', y: 'middle' }, [
  Pattern.straight(300, -500, 0)
]);

The next example declares a wave of 10 aliens that move slowly (100 px/s) up the right hand side from below the screen before turning and darting (500 px/s) to the left:

Pattern.construct({ count: 10, alien: 2, x: 'right', y: 'below' }, [
  Pattern.straight(20,     0, -100),
  Pattern.straight(300, -500,    0)
]);

Finally, the following example declares a wave of 6 aliens that move from right to left for 30 frames before rotating around a point 200px above them for 180 degrees before heading back from where they came:

Pattern.construct({ count: 6, alien: 3, x: 'after', y: 'bottom' }, [
  Pattern.straight(30, -500, 0),
  Pattern.rotate(180, 500, 0, -200)
  Pattern.straight(30,  500, 0)
]);

In a more robust game these declarations could be made clearer (and more flexible) and perhaps a higher level DSL could be built to simplify things e.g. halfCircle(…), rotateAndReturn(…), but for a weekend prototype the straight() and rotate() combinations are adequate.

In a future article I will go into more depth on the code that actually implements these Pattern objects, and more importantly the Alien entity update() method that actually moves the aliens to follow the pattern. It’s fairly simple trigonometry, but if people are interested I can draw a few diagrams and go into more detail in a future article.

For now, you can review the source code to see the ugly details.

And that’s about it folks! I might revisit this prototype again in the future and add more variety to the alien patterns, along with power ups and boss aliens.

In the mean time, you can…

Enjoy!