Build a game using the DOM – Part 4

March 11th, 2012

Previously on Build a game using the DOM, in part 1 we made a block move around with the arrow keys, in part 2 we made our block shoot lasers and in part 3 we added enemies that we could shoot. We could stop here and call the game “Invincible laser shooting block” but I think that would get boring after a while. So, we’ll set it up so that if our block hits an enemy block, we reset the game back to the start positions. And then we’ll add a score and lives so the game isn’t infinite.

The first thing we’re going to do is add the ability for our game to check if our block is in contact with one of the enemies. We’ll do this by altering the moveEnemies function since we already have loop running that we can take advantage of.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function moveEnemies() {
   if (enemies.length < enemyTotal) {
       var enemy = new Enemy();
           enemies.push([enemy, enemyPos.y]);
           playArea.appendChild(enemies[enemies.length - 1][0]);
           enemies[enemies.length - 1][0].classList.add('enemy');
           enemies[enemies.length - 1][0].style.top = enemies[enemies.length - 1][1] + 'px';
           enemies[enemies.length - 1][0].style.left = Math.floor(Math.random() * 500) + 'px';
   }
   for (var i = 0; i < enemies.length; i++) {
       enemies[i][1] += enemySpeed;
       enemies[i][0].style.top = enemies[i][1] + 'px';
       
       // new stuff
       var ex = parseInt(enemies[i][0].style.left),
             ey = parseInt(enemies[i][0].style.top),
             ew = ex + parseInt(enemies[i][0].offsetWidth),
             eh = ey + parseInt(enemies[i][0].offsetHeight),
             sx = parseInt(ship.style.left),
             sy = parseInt(ship.style.top),
             sw = sx + parseInt(ship.offsetWidth),
             sh = sy + parseInt(ship.offsetHeight);

       if (parseInt(enemies[i][0].style.top) > (playArea.bottomBoundary + enemies[i][0].style.height)) {
           enemies[i][1] = enemyPos.y;
           enemies[i][0].style.top = enemies[i][1] + 'px';

       // new stuff
       } else if (sx >= ex && sx <= ew && sy >= ey && sy <= eh) {
           checkLives();
       } else if (sw <= ew && sw >= ex && sy >= ey && sy <= eh) {
           checkLives();
       } else if (sh >= ey && sy <= eh && sx >= ex && sx <= ew) {
           checkLives();
       } else if (sh >= ey && sh <= eh && sw <= ew && sw >= ex) {
           checkLives();
       }
   }
}

I’ve marked the new content with the ultra descriptive new stuff comment. Inside the for loop, we add a bunch of new variables that we use to get the position of the current enemy in the loop and the position of the player. The variable ex = the enemy’s x position and ew = the x position and the width of the enemy. Doing this with the y position and height as well enables us to figure out if the player overlaps any part of the enemy. We don’t have to declare variables for these values, we could just plug in the longer code in the next part, but this really simplifies it and makes it easier to read.

Next we add some else if statements to our existing if statement. Here is where we check if our player has overlapped the enemy block and if it has, then we run a function called checkLives. Why are there for if statements to check this? The way I think about it is we need to check for each corner of the player’s block and to do this we need an if statement for each corner. If you look at the code, we’re really checking if, as in the first one, the player’s x position is greater than the enemy’s x position but less than the enemy’s x position plus it width and if it’s y position is great than the enemy’s y position but less than it’s y position and it’s height. If this is true, then we know that the top left corner of our player’s block is inside the enemy and it’s a hit. Now we need to write the checkLives function.

First we need to add something to our variables at the top, you can add this up the other createElement variables to keep things organized:

1
gameOverText = document.createElement('h1')

This is for the text we’ll display when the player runs out of lives, which we also need to create a variable for. And finally we need to make a variable to check if the game is over:

1
2
lives = 3,
gameOver = false

Add that at the end of the variables at the top of the code. Now, on to the checkLives function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function checkLives() {
   if (lives > 1) {
       resetGame();
   } else {
       playArea.removeChild(ship);
       for (var i = 0; i < enemies.length; i++) {
           playArea.removeChild(enemies[i][0]);
       }
       if (lasers.length > 0) {
           for (var i = 0; i < lasers.length; i++) {
               playArea.removeChild(lasers[i][0]);
           }
       }
       gameOver = true;
       playArea.appendChild(gameOverText);
       gameOverText.classList.add('game-over');
       gameOverText.innerHTML = 'Game Over';
   }
   lives -= 1;
}

The purpose of the checkLives function is to see if the player has more than 0 lives. If they do, we run a function called resetGame, which we’ll write next, and subtract 1 life. If they don’t, then we remove everything from the play area, add the gameOverText which we set the innerHTML to equal ‘Game Over’ and set gameOver to true. Pretty simple.

Before we write the resetGame function, we need to add some variables and then we need to add a couple of lives of code where we set up the game. We have an object called shipPos that we use to position our player and it looks like this:

1
2
3
4
shipPos = {
   x: 0,
   y: 0
}

For the resetGame function to work, we need to get the starting position of the player and save it because at the beginning, we’re setting the start position dynamically based on the width and height of the play area. Update shipPos to this:

1
2
3
4
5
6
shipPos = {
   x: 0,
   y: 0,
   startX: 0,
   startY: 0
}

And then after where we set shipPos.x and shipPos.y, we need to add a couple of lines:

1
2
3
4
shipPos.x = (playArea.offsetWidth / 2) - (ship.offsetWidth / 2);
shipPos.y = playArea.offsetHeight - (ship.offsetHeight * 2);
shipPos.startX = shipPos.x; // new lines
shipPos.startY = shipPos.y; // new lines

Now that we have those set, we can reset the player to their starting position when they lose a life.

1
2
3
4
5
6
7
8
9
10
11
12
13
function resetGame() {
   shipPos.x = shipPos.startX;
   shipPos.y = shipPos.startY;
   ship.style.left = shipPos.x + 'px';
   ship.style.top = shipPos.y + 'px';
   enemyPos.x = 150;
   for (var i = 0; i < enemyTotal; i++) {
       enemies[i][1] = enemyPos.y;
       enemies[i][0].style.top = enemies[i][1] + 'px';
       enemies[i][0].style.left = enemyPos.x + 'px';
       enemyPos.x += 150;
   }
}

With resetGame, we reposition the player back to the starting position and then we reposition all the enemies back to their starting positions. And lastly for the lives part of this tutorial, we have a change to make to our loop function:

1
2
3
4
5
6
7
8
function loop() {
   if (gameOver === false) { // new stuff
       moveShip();
       moveEnemies();
       moveLasers();
   }
   requestAnimFrame(loop);
}

Now we only run the functions that move our elements around if gameOver is false, so when the player runs out of lives, we don’t run those functions that wouldn’t do anything. You’ll notice one other difference from part 3, instead of setTimeout, I’ve using requestAnimFrame to run the loop. This is a function created by Paul Irish to use the requestAnimationFrame if the browser can or else use setTimeout. I’ve just created a seperate JS file so I can just link to it when I need it for a project. Just make sure you link to it before the game code.

1
2
3
4
5
6
7
8
9
10
window.requestAnimFrame = (function(){
 return  window.requestAnimationFrame       ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame    ||
         window.oRequestAnimationFrame      ||
         window.msRequestAnimationFrame     ||
         function(/* function */ callback, /* DOMElement */ element){
           window.setTimeout(callback, 1000 / 60);
         };
})();

Next up is keeping track of the score and it’s probably a pretty good idea if the player knows how many lives they have left, so we’ll add that too. First thing we’ll do is add some CSS, including some styling for our game over text, which you might have noticed we gave the class of game-over to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.score-div {
   position:absolute;
   top:10px;
   left:10px;
}
.score-label, .lives-label {
   font-family:Arial, Helvetica, sans-serif;
   color:#fff;
   font-weight:bold;
   margin:0;
   font-size:20px;
}
.lives-div {
   position:absolute;
   top:40px;
   left:10px;
}
.game-over {
   font-family: Arial, Helvetica, sans-serif;
   margin-top:240px;
   text-align:center;
   color:#fff;
}

This will position our score and lives divs in the upper left corner and it will center the game over text when that’s on screen. Now the JavaScipt:

1
2
3
4
scoreDiv = document.createElement('div'),
scoreLabel = document.createElement('p'),
livesDiv = document.createElement('div'),
livesLabel = document.createElement('p')

Add these lines up with the other createElement variables and then add these at the end of the variables:

1
2
3
scoreText = 'Score: ',
score = 0,
livesText = 'Lives: '

Next, after the section where we set up the game, add this block of code:

1
2
3
4
5
6
7
8
document.body.appendChild(scoreDiv);
scoreDiv.classList.add('score-div');
scoreDiv.appendChild(scoreLabel);
scoreLabel.classList.add('score-label');
document.body.appendChild(livesDiv);
livesDiv.classList.add('lives-div');
livesDiv.appendChild(livesLabel);
livesLabel.classList.add('lives-label');

This code has the same stuff going on as when we’re creating the player and enemy blocks. We add the scoreDiv and the livesDiv to the body and give them the classes score-div and lives-div respectively. Then we add the labels which will hold our text inside the divs. Next, inside the checkHit function, inside the if statement where we remove the laser and enemy if it’s been hit, add this line:

1
score += 100;

Now, for every enemy that’s killed, the score will go up by 100 which is all well and good but we also need to update the score text so the player can see it.

1
2
3
4
function updateScore() {
   scoreLabel.innerHTML = scoreText + score;
   livesLabel.innerHTML = livesText + lives;
}

Really simple function here, we just update the text inside the scoreLabal and the livesLabal p tags. Of course, if we still have to call the function and we’ll do that in the loop function. Updated it to look like this:

1
2
3
4
5
6
7
8
9
function loop() {
   if (gameOver === false) { // new stuff
       moveShip();
       moveEnemies();
       moveLasers();
   }
   updateScore();
   requestAnimFrame(loop);
}

Now if you run the game, you’ll have score, lives and a game over screen. A fair amount of code, but none of it was too complicated, especially when compared to the previous steps. You can check out the demo here. I’ve marked all the new code in case you want to go through it.

Next time, we’ll finally add graphics, a start screen and a start over button on the game over screen.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>