LESSON 21 - June 11, 2013

HTML5 GAME DEVELOPMENT

(Game Dev. Tools) LESSON # 21

Ben W. Savage

So, as tradition dictates, every 7th lesson will be a review lesson in which we'll be reinforcing the stuff we've learned throughout the previous seven lessons by making a mini-game or two. This week has been a lot of fun: we've learned how to make elements interact with the boundaries of the canvas and have applied physics operations to them like velocity and acceleration while they move across the screen. Sounds to me like it won't be very much trouble at all making game-like programs with what we've got under our belts!

So today we'll be creating a simple score-based game with simple anemy AI and simple rules. I call it Bonk! and though it's far from becoming a smash hit, it does a pretty good job of covering a number of techniques we've learned so far. Here's the code:

<!doctype html>
<title> Bonk! </title>
<canvas id="canvas" width="550" height="400">Your browser stinks </canvas>
<style>
#canvas
{
background-color: #eeeeee;
}
</style>
<script>

var theCanvas = document.getElementById("canvas");
var context = theCanvas.getContext("2d");

var playerX = 200;
var playerY = 200;
var playerVX = 4;
var playerVY = 4;

var enemyX = 0;
var enemyY = 0;
var enemyVX = 4;
var enemyVY = 4;

var playerScore = 0;
var enemyScore = 0;

var sideMessage;
var messageVisible;

var bounce = -1;

var leftSideValue = 1;
var rightSideValue = 1;
var bottomSideValue = 1;

var randomSide = 0;

window.addEventListener("keydown",onKeyDown,false);

function onKeyDown(event)
{
if (event.keyCode === 37)
{
playerVX *= -1;
}
if (event.keyCode === 39)
{
playerVX *= -1;
}
}

setInterval(drawMe,10);
setInterval(mainTimer,2000);

function mainTimer()
{
randomSide = Math.floor(Math.random() * 3000);
setInterval(drawSideValueMessage,10);
}

function drawSideValueMessage()
{
if (playerScore < 200 && enemyScore < 200)
{
context.font = "20px Verdana";
context.fillStyle = "#330000";
context.fillText("BONUS POINTS ON:" + sideMessage, 135,300);
}
if (randomSide < 1000)
{
leftSideValue = 4;
rightSideValue = 1;
bottomSideValue = 1;
sideMessage = "LEFT!";
}
else if (randomSide >= 1000 && randomSide < 2000)
{
leftSideValue = 1;
rightSideValue = 4;
bottomSideValue = 1;
sideMessage = "RIGHT!";
}
else if (randomSide >=2000 && randomSide < 3000)
{
leftSideValue = 1;
rightSideValue = 1;
bottomSideValue = 4;
sideMessage = "BOTTOM!";
}
}

function drawMe()
{
context.clearRect(0,0,canvas.width,canvas.height);

context.shadowColor = "#000000";
context.shadowOffsetX = 6;
context.shadowOffsetY = 6;
context.shadowBlur = 14;
context.fillStyle = "#ff0000";
context.fillRect(playerX,playerY,40,40);

context.fillStyle = "#55aaff";
context.fillRect(enemyX,enemyY,40,40);

context.shadowColor = "eeeeee";
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;

if (playerScore < 200 && enemyScore < 200)
{
context.font = "20px Verdana";
context.fillStyle = "#ff0000";
context.fillText("Player Score:" + playerScore, 70,20);

context.font = "20px Verdana";
context.fillStyle = "#55aaff";
context.fillText("Enemy Score:" + enemyScore, 300,20);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("Press the left and right keys to switch directions.", 90,40);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("The first to reach 200 points is the winner.", 110,60);


playerX += playerVX;
playerY += playerVY;

enemyX += enemyVX;
enemyY += enemyVY;
}

if (playerX + 40 > 550)
{
playerVX *= bounce;
playerScore += rightSideValue;

}
else if (playerX < 0)
{
playerVX *= bounce;
playerScore += leftSideValue;

}
if (playerY + 40 > 400 )
{
playerVY *= bounce;
playerScore += bottomSideValue;

}
else if (playerY < 0)
{
playerVY *= bounce;
playerScore++;
}

if (enemyX + 40 > 550)
{
enemyVX *= -1;
enemyScore += rightSideValue;
}
else if (enemyX < 0)
{
enemyVX *= -1;
enemyScore += leftSideValue;
}
if (enemyY + 40 > 400 )
{
enemyVY *= -1;
enemyScore += bottomSideValue;
}
else if (enemyY < 0)
{
enemyVY *= -1;
enemyScore += 3;
}

if (playerScore >= 200)
{
context.font = "40px Verdana";
context.fillStyle = "#ff0000";
context.fillText("PLAYER WINS!", 150,130);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("Refresh your browser to play again.", 130,40);
}
if (enemyScore >= 200)
{
context.font = "40px Verdana";
context.fillStyle = "#55aaff";
context.fillText("ENEMY WINS!", 150,130);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("Refresh your browser to play again.", 130,40);
}
}
</script>

Don't be intimidated. All of the stuff in here consists of tecnniques we've covered in previous chapters. I can imagine by now you're tired of the rectangle and grey background, but after this lesson we'll have the ability to break past these simplistic graphics and create something a bit more interesting to the eye with sprite sheets. Or if you're ambitious and can't stand these rectangles, BY ALL MEANS draw your own shapes - you know how to do that stuff by now! Once again, not one line of code should be stange to you if you've followed along from the beginning.

Go ahead and copy this code in your text editor. Understand how it works? Here are the rules: you must battle against your opponent and reach 200 before it does. You gain points by simply bouncing off the walls, but acquire bonus points by hitting a wall that is randomly chosen at intervals throughout the game. That's all there is to it! Let's analyze this code piece by piece.

Ready?


<!doctype html>
<title> Bonk! </title>
<canvas id="canvas" width="550" height="400">Your browser stinks </canvas>
<style>
#canvas
{
background-color: #eeeeee;
}
</style>

Up until the script tag there's nothing out of the ordinary, just our standard setup. I've added the name Bonk! to the title tag as you can see. I've also provided a nice message for our users with old, stinky browsers.

Following this we've added some variables.


<script>

var theCanvas = document.getElementById("canvas");
var context = theCanvas.getContext("2d");

var playerX = 200;
var playerY = 200;
var playerVX = 4;
var playerVY = 4;

var enemyX = 0;
var enemyY = 0;
var enemyVX = 4;
var enemyVY = 4;

var playerScore = 0;
var enemyScore = 0;

var sideMessage = "";

var bounce = -1;

var leftSideValue = 1;
var rightSideValue = 1;
var bottomSideValue = 1;

var randomSide = 0;

Of course we have the two setup (theCanvas and context) variables. No need to cover these again as we've seen them a number of times Following these we have both the X and Y positions for the player and enemy rectangles as well as their VX and VY values, which we know are used to determine their velocities.

Following this we have the score value for the player and the score value for the enemy. Plain and simple.

Next is a variable called sideMessage which is a string that we'll be applying to our text and will contain the values "LEFT!","RIGHT!" and "BOTTOM!".

Under that we have the variable that determines the degree of bounciness the player/enemy will have when they hit the walls and underneath this variable is the point value for hitting the left, right and bottom of the screen.

Lastly we have randomSide which will determine the randomly chosen side (which holds the bonus points) that the player will chase after.

After our variables, we have another chunk of code:


window.addEventListener("keydown",onKeyDown,false);

function onKeyDown(event)
{
if (event.keyCode === 37)
{
playerVX *= -1;
}
if (event.keyCode === 39)
{
playerVX *= -1;
}
}

This block of code adds a keyboard listener called onKeyDown. The handler below simply processes the two possible key codes (37,39) and reverses the player's direction. Again, pretty self-explanatory.

Following the handler we have this:


setInterval(drawMe,10);
setInterval(mainTimer,2000);

function mainTimer()
{
randomSide = Math.floor(Math.random() * 3000);
setInterval(drawSideValueMessage,10);
}

Three timers with a timer inside a timer! Whoa! What do they do?

The first timer (drawMe), is our main loop. This is what will allow our rectangles to move as well as other stuff which we'll soon see. The second of these little guys is called mainTimer and will do two things:

First it will generate a random number between 0 and 3000. This information will be used to generate the random sides which hold bonus points. Second, it will call a third timer named drawSideValueMessage which will actually write the message on every iteration of mainTimer.

This leads us to drawSideValueMessage itself:


function drawSideValueMessage()
{
if (playerScore < 200 && enemyScore < 200)
{
context.font = "20px Verdana";
context.fillStyle = "#330000";
context.fillText("BONUS POINTS ON:" + sideMessage, 135,300);
}
if (randomSide < 1000)
{
leftSideValue = 4;
rightSideValue = 1;
bottomSideValue = 1;
sideMessage = "LEFT!";
}
else if (randomSide >= 1000 && randomSide < 2000)
{
leftSideValue = 1;
rightSideValue = 4;
bottomSideValue = 1;
sideMessage = "RIGHT!";
}
else if (randomSide >=2000 && randomSide < 3000)
{
leftSideValue = 1;
rightSideValue = 1;
bottomSideValue = 4;
sideMessage = "BOTTOM!";
}
}

It looks complicated, but it's really not. Allow me to explain. First we have an if statement which checks to see whether or not the player or the enemy has reached a score of 200. If not, the basic message is setup which tells us where to move in order to get the bonus points. Now we have the text "BONUS POINTS ON" and we will proceed to add the value of sideMessage to this every time the timer is called.

Following this if statement , we have three more which associate the randomly determined variable with a side and assigns four points to that side. If the value of our random number is less than 1000, we go to our first if statement which assigns a value of 4 to the left side and applies the string "LEFT!" to our varaible sideMessage. If the random number is greater than or equal to 1000 and is less than 2000, 4 points are added to the right side and the sideMessage becomes "RIGHT!". Lastly, if our variable is between 2000 and is less than 3000, the bottom of the screen receives the 4 points and sideMessage becomes "BOTTOM!".

It's not all that complicated so far is it?

Let's go on.


function drawMe()
{
context.clearRect(0,0,canvas.width,canvas.height);

context.shadowColor = "#000000";
context.shadowOffsetX = 6;
context.shadowOffsetY = 6;
context.shadowBlur = 14;
context.fillStyle = "#ff0000";
context.fillRect(playerX,playerY,40,40);

context.fillStyle = "#55aaff";
context.fillRect(enemyX,enemyY,40,40);

context.shadowColor = "eeeeee";
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
</div>
<p>

We then move on to our main loop which we named drawMe(). Here we're clearing the canvas with a clearRect at every iteration. This helps us create the illusion of animation
by erasing the previous "frame". Try commenting out this section a moment with two forward slashes like so:
</p>

//context.clearRect(0,0,canvas.width,canvas.height);

See what I mean about needing to erase the previous frames? What a mess!

Following our clearRect we establish a drop shadow for each of our rectangles. We then proceed to create a red and blue rectangle and assign our X and Y variables to them. Nothing too mind-blowing here. We end this seciton with a bit of a trick by assigning our drop shadow a value of "#eeeeee" which is the same color value as our canvas rendering it much less visible and annoying. Kinda hacky, but there's no need to do everything by the book just now.

Which leads us to this:


if (playerScore < 200 && enemyScore < 200)
{
context.font = "20px Verdana";
context.fillStyle = "#ff0000";
context.fillText("Player Score:" + playerScore, 70,20);

context.font = "20px Verdana";
context.fillStyle = "#55aaff";
context.fillText("Enemy Score:" + enemyScore, 300,20);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("Press the left and right keys to switch directions.", 90,40);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("The first to reach 200 points is the winner.", 110,60);


playerX += playerVX;
playerY += playerVY;

enemyX += enemyVX;
enemyY += enemyVY;
}

As you can see, this next chunk of code is wrapped in an if statement which basically says, "if no one has reached 200 points yet, do this stuff." All that the code inside this if statement does is create all the gameplay text and add the VX and VY values to both the enemy and the player, creating the movement each timer tick. We've seen this a couple times already. Go check out the lesson on velocity if you've missed it.

Note that the playerScore and enemyScore values are attached to the "Player Score:" and "Enemy Score:" strings in the first two fillText satements.

We follow this with something which might look terrifying at first, but is actually quite simple:


if (playerX + 40 > 550)
{
playerVX *= bounce;
playerScore += rightSideValue;

}
else if (playerX < 0)
{
playerVX *= bounce;
playerScore += leftSideValue;

}
if (playerY + 40 > 400 )
{
playerVY *= bounce;
playerScore += bottomSideValue;

}
else if (playerY < 0)
{
playerVY *= bounce;
playerScore++;
}

if (enemyX + 40 > 550)
{
enemyVX *= bounce;
enemyScore += rightSideValue;
}
else if (enemyX < 0)
{
enemyVX *= bounce;
enemyScore += leftSideValue;
}
if (enemyY + 40 > 400 )
{
enemyVY *= bounce;
enemyScore += bottomSideValue;
}
else if (enemyY < 0)
{
enemyVY *= bounce;
enemyScore += 3;
}

Egads, what a bunch of code! Another option would have been to use switch statements, but I prefer a bunch of ifs/if elses. De gustibus! Switch to switch if you like!

Basically all that's happening here is the following:

The first four ifs control the player and the second four ifs control the enemy. Each of the player's 4 if statements refers to what happens when the player touches the top,bottom,left and right walls of the canvas. The VX and VY are multiplied by a variable called bounce which we saw equals -1 and causes the player to bounce back in the opposite direction. We also add the value for hitting a particular side to the player's score. All of this is repeated on the enemy's side as well. Note that if the enemy touches the top of the screen, the enemy's score is increased by 3. I've found this balances out the gameplay and gives the player more of a challenge.

And this leads us to the very last part of our code:


if (playerScore >= 200)
{
context.font = "40px Verdana";
context.fillStyle = "#ff0000";
context.fillText("PLAYER WINS!", 150,130);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("Refresh your browser to play again.", 130,40);
}
if (enemyScore >= 200)
{
context.font = "40px Verdana";
context.fillStyle = "#55aaff";
context.fillText("ENEMY WINS!", 150,130);

context.font = "15px Verdana";
context.fillStyle = "#000000";
context.fillText("Refresh your browser to play again.", 130,40);
}
}
</script>

All we have here is the text which pops up when one of the two characters wins. The first if deals with the character and the second if deals with the enemy. Below each ....WINS! text we have the instructions for how to play again. It's a lazy way to reset the game, but it's fine for now. Later we'll deal with UI, intro screens and the like.

So that's it! We've made a simple game! Wasn't that hard, was it? And our games are becoming more and more complex! That means we're well on our way to doing some interesting stuff. Now our games at this point are much less interesting than they COULD BE because we haven't yet talked about collision detection (aka, what happens when a something collides with something else) and collision detection is a HUGE part of game development! We'll be seeing that in the next set of 7 lessons, so very soon in other words! Tune in next time for further exploration into the world of HTML5!

So stay tuned and I hope you enjoyed! As always, thanks for following along! Code didn't work for you? Hate me? Are you my illegitimate child? Send input anytime to me on Twitter: @benwhi.

Until next time!

-Ben
@benwhi
Onward to Lesson Twenty Two!
Back to Lesson Twenty!
Back to Index