Introduction
I created this simple game in my free time just to distract myself from what I'm requested to do on my daily job. This is also an interesting and funny way of JavaScript coding, compared to a typical DOM manipulation or Ajax calls to server. I find JavaScript really powerful when used as OOP language, especially with html5 features.
Of course, this code can be improved in many ways, as well as the main architecture probably. I'm not a videogame maker unfortunately. But it can also be a starting point for those who are interested in videogame basics. This game could be extended with power-up, different sprites, ground enemies... I personally was surprised observing how a game can have a life on its own, once you give robustness and logic, and I really hope you can find my same spirit.
Thanks to Jacob Zinman Jeanes from this page for the free images of ship and enemies.
Background
The main focus here is on the html5 Canvas
, the simple and powerful pixel matrix where you can draw your images. Generally, it has no concept of the items drawn on it, it doesn't contains graphic objects. You need an object array to maintain all the items reference and their states. Neither does it have a concept of z-index, so the priority of the items in terms of overlap, must be managed by us externally. Just consider that every item is drawn over the others, so the last drawn is on the highest layer, that is closest to the observer.
Using the Code
I am not going to comment all the code, since I'm afraid it would be boring and discouraging for a reader who doesn't have a lot of spare time. Let's see only the most important objects and logic of the game, of course, you can download the small zip file to read all the code.
imagesRepo
It contains the images for every item in the game, acting as repository and providing a single loading for every image. Properties verticalImageFrames
and horizontalImageFrames
indicate how many frames exist in the bitmap, vertically and horizontally.
The method getImageFor
returns the image corresponding to the type of the requester.
function imagesRepo() {
this.cloud = new Image();
this.cloud.src = "img/cloud.png";
this.cloud.onload = function () { this.isLoaded = true; };
this.cloud.verticalImageFrames = 1;
this.cloud.horizontalImageFrames = 1;
......
this.getImageFor = function (item, wave) {
if (item instanceof cloud) return this.cloud;
.......
}
gameObject
Every object inherits from it, to get available common methods and properties, that you see below. It loads image from the constructor, and shares some simple methods. The most important is nextImageFrame()
that animates an image in terms of sequence of frames (think at the ship reactor).
function gameObject(x, y, wave) {
this.x = x;
this.y = y;
this.currentFrame=1;
this.image = images.getImageFor(this, wave);
this.offsetLeftEnergy = 0;
}
gameObject.prototype.getFrameHeight = function () {
return this.image.height / this.image.verticalImageFrames;
}
gameObject.prototype.getFrameWidth = function () {
return this.image.width / this.image.horizontalImageFrames;
}
gameObject.prototype.nextImageFrame = function (onlyOnce) {
this.currentFrame++;
if (this.currentFrame > this.image.verticalImageFrames * this.image.horizontalImageFrames) {
if (onlyOnce)
this.tbd = true;
else
this.currentFrame = 1;
}
}
scene
scene
is the main object of the game. Everything can be referenced from here.
The most important property of its instance is gameItems
, the array containing ALL the objects of the game. The idea is to have self-sufficient objects in this array, and run their own draw()
method at every frame of the game. Every method will do something different (in case of our missile, it will go from left to right, in case of an enemy, it will follow a pattern, and so on), so we'll have to iterate the array and launch item.draw()
for each of the items. In this way, the attention of the developer is concentrated on a single object logic and lifecycle, and I can assure it becomes all very easy. If we make a good job, it is sufficient to push the object into gameItems
, and all should work. Ok, this is not a great discovery, somebody says. ;)
This is the most important part:
scene.prototype.drawAll = function () {
this.gameTicks++;
if (this.gameTicks == 1000) this.gameTicks = 0;
purgeTbd(this.gameItems);
this.gameItems.sort(compareZindex);
if (this.countOf(enemy) == 0 && this.countOf(message) == 0) {
this.wave++;
this.initEnemies();
}
if (!this.paused)
{
this.gameItems.forEach(
function (item, index) {
item.draw();
});
}
this.setScore();
if (this.ship.isDead) this.gameOver();
var t = this;
requestAnimationFrame(function () {
t.drawAll()
});
};
This method is run at every frame of the game, that is 60 times per second on my pc. As you can see, I use requestAnimationFrame
at every frame since it is optimized by the browser and it's the recommended way of doing canvas frequent updates. The alternative was a classic setInterval
, but game was less smooth than now in my attempts. By setting its callback to the same caller function (drawAll()
), you start the main loop of the game.
ship
The ship is our hero, fighting enemies in the sky :). The points to note I think could be:
movement
: Of course, you move it by the mouse, but if the ship movement coincided with the mouse pointer, it would appear a bit rough or too sharp. So the mouse movement indeed sets a target X and Y for the ship, and the ship tends to reach that point with a kind of delay, or inertia:
this.x = this.x + ((this.xTarget - this.x) / this.inertia);
this.y = this.y + ((this.yTarget - this.y) / this.inertia);
energy bar
: drawEnergy()
method displays below the ship an energy bar, decreasing at every collision with the bombs (will see later) and causing destruction if down to 0. shoots
: shootToEnemy()
is a very simple method (called by the mouse listener on click event), releasing a missile from the ship towards the right. The max number of missiles on screen is one of the constants of the game on top of the script. The missile is just added to gameItems
array with starting x
and y
coordinates.
ship.prototype.shootToEnemy = function () {
if (this.isDead) return;
if (myscene.countOf(missile) >= MAX_MISSILES) return;
myscene.gameItems.push(new missile(
this.x + this.image.width - 20,
this.y + 14));
new Audio('sound/shoot.wav').play();
}
enemy
Three colors of flying saucers alternate during the waves. At every new wave, their number increases, and their 'bomb-rate' factor decrease, to have them shooting more often, as you can deduct below.
this.shootToShip = function () {
if (getRandom(1, 1000) > this.bombRate) {
var a = new enemyBomb(
this.x, this.y,
myscene.ship.x + myscene.ship.image.width / 2, myscene.ship.y
);
this.enemyBombs.push(a);
}
}
Indeed, enemyBomb
objects are not children of gameItems
, but included in each enemy own array (enemyBombs
). In this way, in case of elimination of an enemy, its bombs also disappear from the screen, and it becomes very useful when it comes to hard levels. ;)
About the pattern or flight path of the enemies, a function, enemyPattern
, provides their coordinates at every frame depending on the wave number we are. In a very simple way, it defines which corner of the canvas the enemies arrive from, at which coord they will start their path, and which cos()
/sin()
function they will follow basing on the game tick, so they generally move smoothly and sinusoidally. In the below snippet, variantx
and varianty
cause significant changes in the path outlined by the enemy.
if (enemy.patternStarted) {
enemy.patternTick += 3;
enemy.x += enemy.speedX * Math.cos(ToRadians(enemy.patternTick) / variantx);
enemy.y += enemy.speedY * Math.sin(ToRadians(enemy.patternTick) / varianty);
}
enemyBomb
The little yellow/red balls are launched always towards us by the saucers. So you'd better keep the ship moving.
There are no limits at their amount on the screen, and as said their amount increases at every wave.
cloud
Clouds are generated one per second.
setInterval(function () { t.gameItems.push(new cloud(0, 0)); }, 1000);
They are responsible to give a touch of parallax to the scene. Cloud constructor gets a random number between 1 and 4. The greater this value, the greater the cloud speed, the cloud size, and the cloud 'zindex
', just to simulate it was closer to us.
function cloud(x, y) {
gameObject.call(this, x, y);
this.speedX = getRandom(1, 4);;
this.zindex = this.speedX;
this.x = canvas.width;
this.y = this.speedX * 10;
this.draw = function () {
if (this.image.isLoaded == false) return;
this.x -= this.speedX;
var scale = 4 / this.speedX;
if (this.x + this.image.width / scale < 0) {
this.tbd = true;
return;
}
ctx.drawImage(this.image, this.x, this.y, this.image.width / scale, this.image.height / scale);
};
}
Collisions
Collisions are one of the most important points in a shooter game, and indeed I preferred to simplify them, considering this is far away from a professional game. So this is not a 'per-pixel' collision detection, but let's say a 'per-box'.
I mean, every object is considered as a rect box, and collision can be detected between boxes.
function collisionArea(ob1, ob2) {
var overX = Math.max(0, Math.min(ob1.x + ob1.getFrameWidth(), ob2.x + ob2.getFrameWidth())
- Math.max(ob1.x, ob2.x))
var overY = Math.max(0, Math.min(ob1.y + ob1.getFrameHeight(), ob2.y + ob2.getFrameHeight())
- Math.max(ob1.y, ob2.y));
return overX * overY;
}
In the game, collisions make sense between ship/bombs, ship/enemies, missile/enemies, and for each of the relationships, a constant defines how many pixels must overlap between 2 objects for the collision to be declared.
const SHIP_ENERGY=100,
MAX_MISSILES=3,
BACKGROUND_SPEED=-4,
INITIAL_BOMB_RATE=996,
BOMB_SPEED=7,
MISSILE_SPEED=8,
SHIP_BOMB_COLLISION=20,
ENEMY_MISSILE_COLLISION=20,
SHIP_ENEMY_COLLISION=50
;
Default values seems good enough during my tests, but of course feel free to change them, as well as any other value in the code. Not the best collision system, I know, but it serves our purpose.
Mobile
My objective here was NOT to compile an app, but just to use the same files working on PC, launching the html file from a phone folder (isn't html5 a standard?). No app, no wrapper, no local www server. I used a Samsung A5 (not rooted), with Android 7 and Chrome mobile.
1. How to Run It?
If you open a file manager and tap on the html file, it will not work. Some file managers don't open it at all, others open the browser, but with an URL like content://0@media... .which is not good for us, probably because it can't see subfolders in this way.
Just open Chrome first, and point the URL to file://sdcard/. You will get a list of folders, open the one where you copied the game. Run the html file from there. (Note: I couldn't see external SD folders, so I had to use internal memory.)
2. Audio
Audio objects created at runtime and immediately played don't work on mobile. It seems they must be present on the HTML page as Audio controls to play, but there is another problem. They can't be played programmatically, at least not at the beginning. They must first be played by a direct user input, just a protection for data usage saving.
This is the reason why I introduced the 'Click/tap to start game' banner. On this event, sounds are initiated by just running play/pause method of the audio controls. After this, they can be played when we need.
3. Fullscreen
Even the fullscreen must be initiated by user gesture. So the same 'touchend
' event used for the audio is used to set the page in fullscreen.
Here is the sound and fullscreen initialization:
function initMobile()
{
if (canvas.requestFullscreen) canvas.requestFullscreen();
if (canvas.webkitRequestFullscreen) canvas.webkitRequestFullscreen
(canvas.ALLOW_KEYBOARD_INPUT);
if (canvas.mozRequestFullScreen) canvas.mozRequestFullScreen();
if (canvas.msRequestFullscreen) canvas.msRequestFullscreen();
audioexpl1.play();
audioexpl1.pause();
audiobomb.play();
audiobomb.pause();
audioshoot.play();
audioshoot.pause();
}
Points of Interest
If you notice some frame drops, just reduce the size of the browser page and restart. Or even try to go F11, have seen that on some browsers full screen makes difference.
History
- 21/10/2017: First release
- 28/10/2017: Added mobile support