Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

A Simple Shooter Videogame in Native JavaScript - Updated for Mobile

4.93/5 (15 votes)
28 Oct 2017CPOL8 min read 21.4K   643  
Let's have a break with JavaScript and a Canvas

Introduction

Image 1

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.

JavaScript
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).

JavaScript
function gameObject(x, y, wave) {
  this.x = x;
  this.y = y;
  this.currentFrame=1;
  this.image = images.getImageFor(this, wave); //get image from images Repository
  this.offsetLeftEnergy = 0;
}

gameObject.prototype.getFrameHeight = function () {
   return this.image.height / this.image.verticalImageFrames; //get real height for a multiframe image
}

gameObject.prototype.getFrameWidth = function () {
   return this.image.width / this.image.horizontalImageFrames;//get real width for a multiframe image
}

gameObject.prototype.nextImageFrame = function (onlyOnce) {
   this.currentFrame++; //increase the frame of the object

   if (this.currentFrame > this.image.verticalImageFrames * this.image.horizontalImageFrames) {
   if (onlyOnce) //if the frames chain must be  showed only once (like explosions...)
     this.tbd = true; //mark the object to be deleted
   else
     this.currentFrame = 1; //if frames are looped (like ship..) restart from frame 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:

JavaScript
scene.prototype.drawAll = function () {
    this.gameTicks++;
    if (this.gameTicks == 1000) this.gameTicks = 0;

    purgeTbd(this.gameItems); //delete objects marked with tbd=true

    this.gameItems.sort(compareZindex); //sort the array by zindex property

    if (this.countOf(enemy) == 0 && this.countOf(message) == 0) {
       this.wave++; //if enemies are all dead, increase wave counter and reinit them
       this.initEnemies();
    }
    if (!this.paused) //if game is not paused (space bar)
    {
                //draw every items according to its own logic
                this.gameItems.forEach(
                    function (item, index) {
                        item.draw();
                 });
     }

    this.setScore(); //write score on screen
    if (this.ship.isDead) this.gameOver(); //if we are dead, print game over

    var t = this;
    requestAnimationFrame(function () {
      t.drawAll() //callback to do this same function 
    });
};

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:
    JavaScript
    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.
    JavaScript
    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.

JavaScript
this.shootToShip = function () {

    if (getRandom(1, 1000) > this.bombRate) { //the lower the bombRate, 
                                              //the higher the probability to shoot

    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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
//returns number of overlapping pixels between two rect objects
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.

JavaScript
const SHIP_ENERGY=100, //starting ship energy
      MAX_MISSILES=3, // max number of our missiles on screen
      BACKGROUND_SPEED=-4, // scrolling speed
      INITIAL_BOMB_RATE=996, //bomb rate - if random nbr (1,1000) > 996, 
                             //enemy shoots. decreasing with waves
      BOMB_SPEED=7, // speed of enemy bombs
      MISSILE_SPEED=8, // speed of our rockets
      SHIP_BOMB_COLLISION=20, //number of pixel in overlap to be a collision
      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:

JavaScript
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)