Javascript Delta
Sat, May 3, 2014A 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:
- arrow keys to fly
- space to shoot.
- shoot aliens
- survive!
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:
- Power Ups
- More Alien Variety
- More Graphics
- Polish
Here are a couple more caveats to admit:
- Modern browsers only - Chrome, Firefox, IE9+
- NO mobile support - If I wanted to build mobile I would go 100% native.
- NO touch support - no mobile, no touch
- NO endgame - it’s just a prototype, there is no endgame
How-it-works
If you’ve followed any of my previous game experiments you might see I’ve re-used many pieces:
- a basic asset loader
- a basic game loop
- simple keyboard player input
- simple math
- simple DOM manipulation
- simple CANVAS rendering
- simple HTML5 audio helpers
- a finite state machine
- a starfield
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();
}
-
The Engine - is the main game coordinator described in more details in the next section.
-
The Renderer - is an object that knows how to render all the other entities on the canvas.
-
The Sound Manager - is an object that knows how to play one of our sound effects.
-
The Player - is a simple class that has a position, a speed, and a cooldown counter that tracks when they can fire another bullet.
-
A Bullet - is a simple object with position and speed. By maintaining a simple ObjectPool of bullets we can re-use expired ones and avoid garbage collection issues.
-
Rocks - are simple objects with position and speed, once a rock scrolls off the left side of the screen it is re-used and repositioned just offscreen on the right hand side.
-
Stars - are simple objects with position, size, color, and speed. Again by maintaining a simple ObjectPool we can re-use the stars that scroll offscreen by repositioning them on the right hand side.
-
Aliens - are the only real complex entity involved because of their movement patterns, I’ll describe those in more details later.
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
booting
- loading the game assetstitle
- the title screenpreparing
- ready player one ?playing
- game in progress
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:
- move in a straight line
- rotate around a point
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:
- count - the number of aliens in the wave
- alien - the type of alien (in this demo simply a sprite number 1, 2, or 3)
- x - the horizontal starting position (left, right, center, before, or after)
- y - the vertical starting position (top, middle, bottom, above, or below)
- rocks - scroll rocks along the top/bottom during this alien wave (true or false)
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:
- straight(n, dx, dy)
- rotate(d, speed, nx, ny)
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.
Related Links
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…
- play the game
- view the source code
- read more about javascript game foundations
- read more about javascript starfields
- play more with some javascript starfields
- Learn more about Delta
- Watch someone play the the original Stages 1 - 11, 12 - 21, or 22-29.
- Read the original Zzap64 review
Enjoy!