HTML: Space Game

Overview

In this HTML lesson, we demonstrate how to use HTML, CSS, and JavaScript together to program a space game. The CSS and JavaScript portions are embedded in the head at the top of the code file, while the HTML is in the body at the bottom. We make use of CSS for all of the game animations, and the events and interactivity are programmed in JavaScript. HTML is used to code the static elements of the game. This code ties together the material from previous lessons.

Space Game

 
 

Code Summary

The CSS section is the first code section, which is contained in the style element. It has two class selectors for the asteroid and explosion elements. These selectors are used to apply the keyframe animations to these elements to create appearence of asteroid movements and explosions. This sections handles all of the animations for the game.

The second section is the JavaScript section, which is contained in the script element. This code handles all of the game state and interactivity: The scores, damage, and asteroids are maintained in JavaScript, and the mouse events, corresponding to shots, are handled by the fnOnClick() function. JavaScript is also used to initiate the sound effects for shots, exploding asteroids, and the asteroid collisions with the ship.

The final section is the HTML code, which is in the body element. The onkeydown attribute of the body element is used to initiate the execution of the JavaScript when a key is pressed by calling the fnStart() function. A div that covers the windshield area is used to set the crosshairs cursor and process mouse clicks for shots by calling the fnOnClick() function. Invisible audio elements are included to play the game's sound effects via JavaScript controls, while the visible game elements use the inline CSS property z-index to layer themselves appropriately.

SpaceGame.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net HTML</title>
    <style>
      .cAsteroid {
        position:absolute;
        animation: kfExpandWidth linear 2.5s 1,
        kfMoveDown linear 2.5s 1,
        kfExpandHeight linear 2.5s 1,
        kfMoveRight linear 2.5s 1;
      }
      @keyframes kfExpandWidth {
        from {width: 10%;}
        to {width: 100%;}
      }
      @keyframes kfMoveDown {
        from {top: 45%;}
        to {top: 0%;}
      }
      @keyframes kfExpandHeight {
        from {height: 10%;}
        to {height: 100%;}
      }
      @keyframes kfMoveRight {
        from {left: 45%;}
        to {left: 0%;}
      }
      .cExplosion {
        position:absolute;
		animation: kfDisappear linear .5s 1;
      }
      @keyframes kfDisappear {
        from {opacity: 100%;}
        to {opacity: 0%;}
      }
    </style>
    <script>
      var gqOutside = null;
      var giScore = 0;
      var giDamage = 0;
      var gqaAsteroids = null;
      var gqaExplosions = null;
      var giCollisionFrame = 20;
      var gbGameOver = true;
      function fnStart() {
        // Prevent multiple starts from key presses
        if (gbGameOver) {
          gbGameOver  = false;
          giScore     = 0;
          giDamage    = 0;
          gqOutside   = document.getElementById("idOutside");
          // Allocate the asteroid and explosion arrays
          gqaAsteroids  = new Array();
          gqaExplosions = new Array();
          // Remove the start message
          var qMessageElement = document.getElementById("idStartMessage");
          qMessageElement.style.display = "none";
          // Start the timed game loop
          fnGameLoop();
        }
      }
      function fnGameLoop() {
        if (!gbGameOver) {
          setTimeout(fnGameLoop, 50);
          // Increment the asteroid frame counts
          for(var i = 0; i < gqaAsteroids.length; ++i) {
            gqaAsteroids[i].miFrame += 1;
		  }
          // Place an asteroid
          if (Math.floor(Math.random() * 100) < (3 + giScore/10)) {
            fnPlaceAsteroid();
          }
          // Move the screen during a collision
          if (giCollisionFrame < 20) {
            gqOutside.style.left = Math.floor((Math.random() * 6) - 3) + "px";
            gqOutside.style.top  = Math.floor((Math.random() * 6) - 3) + "px";
            ++giCollisionFrame;
          } else {
            gqOutside.style.left = 0 + "px";
            gqOutside.style.top  = 0 + "px";
            giCollisionFrame     = 20;
          }
        } else {
          // Remove the remaining asteroids and stop their collisions
          for(var i = 0; i < gqaAsteroids.length; ++i) {
            gqOutside.removeChild(gqaAsteroids[i].mqDiv);
            clearTimeout(gqaAsteroids[i].mqTimeOut);
		  }
		  // Reset the collision shake
		  giCollisionFrame     = 20;
          var qMessageElement = document.getElementById("idStartMessage");
          qMessageElement.style.display = "block";
        }
        // Update the score and damage
        var qScore = document.getElementById("idScore");
        qScore.innerHTML = giScore;
        var qDamage = document.getElementById("idDamage");
        qDamage.innerHTML = giDamage;
      }
      function CAsteroid() {
        // The frame counter is used to determine how big the asteroid is
        this.miFrame   = 0;
        // x between 30 and 640 - 30 - 28 = 582
        this.miCenterX = Math.floor((Math.random() * 552) + 30);
        // y between 30 and 388 - 30 - 28 = 330
        this.miCenterY = Math.floor((Math.random() * 300) + 30);
        // Create the container div for the image
        this.mqDiv                = document.createElement("div");
		this.mqDiv.style.position = "absolute";
		this.mqDiv.style.left     = this.miCenterX - 28 + "px";
		this.mqDiv.style.top      = this.miCenterY - 28 + "px";
		this.mqDiv.style.width    = "56px";
		this.mqDiv.style.height   = "56px";
		this.mqDiv.style.zIndex   = "1";
		this.mqDiv.innerHTML      = "<img class='cAsteroid' src='Rock.png' />";
		gqOutside.appendChild(this.mqDiv);
		// Create the collision event function. Asteroids collide after 2.5 seconds
        this.mqTimeOut = setTimeout(fnAsteroidCollision, 2500);
      }
      function fnPlaceAsteroid() {
		var qNewAsteroid = new CAsteroid();
		// Add the asteroid at the end of the array
		gqaAsteroids.push(qNewAsteroid);
		fnAsteroidResetZ();
      }
      function fnAsteroidCollision() {
        // Make the collision sound and shake the screen
        var qCollisionAudio         = document.getElementById("idCollision");
        qCollisionAudio.currentTime = 0;
        qCollisionAudio.play();
        // Initialize the screen shake
		giCollisionFrame = 0;
		// Increase the damage
		++giDamage;
        // Remove the first asteroid in the list. This one hit the ship.
        var qRemoved = gqaAsteroids.shift();
        // Remove the asteroid image from the HTML
        gqOutside.removeChild(qRemoved.mqDiv);
        fnAsteroidResetZ();
        if (giDamage >= 3) {
          gbGameOver = true;
        }
      }
      function fnAsteroidResetZ() {
        for(var i = 0; i < gqaAsteroids.length; ++i) {
		  gqaAsteroids[i].mqDiv.style.zIndex = gqaAsteroids.length - i;
		}
      }
      function fnAsteroidShot(iIndex, dPercent) {
        // Create the explosion image
        fnCreateExplosion(dPercent, gqaAsteroids[iIndex]);
        // Stop the collision event
        clearTimeout(gqaAsteroids[iIndex].mqTimeOut);
        // Remove element first because it is accessed via the array
        gqOutside.removeChild(gqaAsteroids[iIndex].mqDiv);
        // Remove the asteroid from the array
        gqaAsteroids.splice(iIndex, 1);
        // Reset the z values
        fnAsteroidResetZ();
        ++giScore;
      }
      function fnCreateExplosion(dPercent, qAsteroid) {
		// Create the explosion image. The image is 56x56
		var iImageSize             = Math.round(dPercent*56);
		var qImageElement          = document.createElement("img");
		qImageElement.className    = "cExplosion";
		qImageElement.src          = "ExplodingRock.png";
		qImageElement.style.left   = qAsteroid.miCenterX - (iImageSize/2) + "px";
		qImageElement.style.top    = qAsteroid.miCenterY - (iImageSize/2) + "px";
		qImageElement.style.width  = iImageSize + "px";
		qImageElement.style.height = iImageSize + "px";
		qImageElement.style.zIndex = qAsteroid.mqDiv.style.zIndex;
		gqOutside.appendChild(qImageElement);
		gqaExplosions.push(qImageElement);
		// Play the explosion shot sound
        var qAsteroidShotAudio = document.getElementById("idAsteroidShot");
        qAsteroidShotAudio.currentTime = 0;
        qAsteroidShotAudio.play();
		setTimeout(fnRemoveExplosion, 500);
      }
      function fnRemoveExplosion() {
        // Get the first explosion and remove it from the outside and array
        var qExplosionImage = gqaExplosions.shift();
        gqOutside.removeChild(qExplosionImage);
      }
      function fnOnClick(e, qElement) {
        // Play the shot sound
	    var qShotAudio = document.getElementById("idShot");
	    qShotAudio.currentTime = 0;
        qShotAudio.play();

        // This is all to calculate the click coordinates relative to the div
        var dElementOffsetX = 0;
        var dElementOffsetY = 0;
        do{
          dElementOffsetX += qElement.offsetLeft - qElement.scrollLeft;
          dElementOffsetY += qElement.offsetTop - qElement.scrollTop;
          qElement = qElement.offsetParent
        } while(qElement != document.body);
        var dRelX = e.pageX - dElementOffsetX;
        var dRelY = e.pageY - dElementOffsetY;

        // Check each asteroid for a hit
        for (var i = gqaAsteroids.length - 1; i >= 0; --i) {
          // Percentage complete - there are 2500/50 = 50 frames per asteroid
          var dPC = (gqaAsteroids[i].miFrame/50);
          // The max radius is 50 pixels. The min radius is 10% or 5 pixels
          var dHitRadius = ((50*dPC) + 5*(1 - dPC));
          var dDeltaX = dRelX - gqaAsteroids[i].miCenterX;
          var dDeltaY = dRelY - gqaAsteroids[i].miCenterY;
          var dClickDistance = Math.sqrt(dDeltaX*dDeltaX + dDeltaY*dDeltaY);
          if (dHitRadius > dClickDistance) {
            fnAsteroidShot(i, dPC);
            return;
          }
        }
      }
    </script>
  </head>
  <body onkeydown="fnStart()">
    <h1 id="idStartMessage"
      style="position:absolute;left:155px;top:130px;z-index:5001;color:white;text-align: center;">
      GAME OVER<br />Press any key to begin!</h1>
    <audio id="idCollision" src="Collision.mp3"></audio>
    <audio id="idAsteroidShot" src="AsteroidShot.mp3"></audio>
    <audio id="idShot" src="Shot.mp3"></audio>
    <!-- The background stars and asteroids -->
    <div id="idOutside" style="position:absolute;left:0px;top:0px;">
      <img style="position:absolute;left:0px;top:0px;z-index:-1;" src="Space.png" />
    </div>
    <!-- The window click region -->
    <div style="position:absolute;left:0px;top:0px;z-index:5000;width:640px;height:388px;cursor:crosshair;"
      onclick="fnOnClick(event, this)"></div>
    <!-- The ship's window, console, and scores -->
    <img style="position:absolute;left:0px;top:0px;z-index:1000;" src="Windshield.png" />
    <h1 style="position:absolute;left:20px;top:395px;z-index:5000;">Score: <span id="idScore">0</span></h1>
    <h1 style="position:absolute;left:340px;top:395px;z-index:5000;">Damage: <span id="idDamage">0</span></h1>
  </body>
</html>