Monday, July 25, 2011

HTML5 Games 101 - An introductory tutorial to HTML5 Games

In my last blog, I had posted the game of Snake that I developed as my first attempt in HTML5 programming and all I can say is... it was fun!

In this tutorial, I will show you how to use the basic features of HTML5 and get the simple game of Snake up and ready within a couple of hours, even if you are a beginner. All you require is some basic logic and any experience with programming is a huge plus. However, I must also admit here that this is more of a JavaScript work than HTML5, as it just uses the canvas and a couple more elements.

Okay, so lets begin!



First things first - Create a HTML file. Open your favorite editor, copy the snippet below and save it with a name as per your liking. I'll stick with "index.html". There is no different file extension for a HTML5 document. It simply uses one subtle and welcoming change in the "DOCTYPE" declaration (welcoming as its easy to remember compared to that from HTML4) and the browser will understand that it is HTML5.

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
<!DOCTYPE HTML>
<html>
<head>
</head>
<body>
    <div id="wrapper">
        <h1>Snake</h1>
        <div id="score">Score: 0 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Level: 1</div>
        <canvas width="300" height="400" id="canvas">
        </canvas>
        <div id="control">Controls: W = Up; A = Left; S = Down; D = Right</div>
    </div>
</body>
</html>
-----------------------------------------------------------------------------------------------------------------------
Lets now look at what we have written here:

<!Doctype HTML> - As I mentioned above, a very simple and neat way to tell your HTML5 enabled browser that it will be accessing a HTML5 document. Then there is a regular <head> tag followed by the <body> tag

In the body of the page, we first define a div named "wrapper" which essentially will include everything that we are about to display. Note that this is not required in our example. Then we create a div to display the game score and the level. As you'll find out later, we will also use this to inform the player if the game is over.

Then comes the main component, "Canvas", the most essential HTML5 component for that can be used for rendering graphs, game graphics, or other visual images on the fly. HTML5 defines a set of functions for drawing various shapes, creating paths, shades and gradients with help of JavaScript to display them within the Canvas. For the purpose of this tutorial, I have defined canvas with width as 300, height as 400 and id = "canvas".

Lastly, we define another div to help user with the controls or any other message that we may want to display to user related to controls during gameplay or afterwards.

Now the fun part, the JavaScript for the game.
We'll start with defining all the global variables that we'll require during our game. The script goes into the <head> tag of the html file.

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
<script>

        //global constants
        var context;                // Variable to hold the entire game context
  
        //specifying the width and height of our game area, we'll use the complete canvas in this case
        var width = 300;
        var height = 400;
        
        //specify the initial game configuration
        var snakeLength = 3;        // initial length of snake is set to 3
        var level = 1;              // start from level 1
        var sqSize = 10;            // step size is 10px. Also size of single body unit        
        /* *************************** /
         * specify the initial snake alignment and direction of movement
         * Snake is starts horizontal moving towards its right
         * *************************** */
        var bodyX = new Array(150, 150-sqSize, 150-2*sqSize);   //array to manage X coordinates for snake body
        var bodyY = new Array(200, 200, 200);                   //array to manage Y coordinates for snake body
        var vX = new Array(1, 1, 1);    //array to manage horizontal velocity for snake body
        var vY = new Array(0, 0, 0);    //array to manage vertical velocity for snake body
        //variables used to put rats on the canvas
        var rX;
        var rY;
        
        var score = 0;                // keeping the score
        var scoreDiv;                // to hold the context of div used to display score and level

        var eaten = true;              // to check if new rat needs to be placed
        var gameOver = false;    // to check if the game is over and enable control to restart the game        
        var controlsDiv;             // to hold the context of div used to display game controls
</script>        
-----------------------------------------------------------------------------------------------------------------------    
Comments accompanying the variables are self explanatory, for those having any confusion hold on for few minutes till you reach the code where they are used and it should get clear to you. Still have any doubt, ping it to me.

Another neat feature of HTML5, in case you haven't notices till now is <script> tag, which no longer requires to be specified with type attribute.

Now lets have a look a look at couple of drawing APIs for HTML5
Below we define 3 functions:
1. To draw the canvas boundary in order to mark our playing area
2. To draw the points (squares) for the snake body
3. Draw our snake

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
/* *************************** /
* Initially it was meant to mark the canvas boundary
* but this take the background color of the page (black for my blog) so now am filling canvas white
* ************************** */
function drawCanvasBoundary()
{
context.fillStyle="#FFF";                 //set canvas color to be white
context.fillRect(0,0,width,height);    //draws a rectangle of canvas size filled with white color. This serves as our background
context.fill();

/*Draw black boundary if working in white background*/
context.strokeStyle="#000";
context.strokeRect(0,0,width,height)
}
/* *************************** /
* Draws each body part of the snake
* x, y = provides the body position
* ************************** */
function drawPoint(x,y)
{
// First draw a square for size "sqSize" filled with black
context.fillStyle = "#000";
context.fillRect(x,y,sqSize, sqSize);
context.fill();
// Then draw the square boundary in white
context.strokeStyle="#FFFFFF";
context.strokeRect(x,y,sqSize, sqSize);
}
/* *************************** /
* Draws snake by calling the helper drawPoint function
* ************************** */
function drawSnake()
{
for(var i=0; i < snakeLength; i++) 
        drawPoint(bodyX[i],bodyY[i]); 
 }
-----------------------------------------------------------------------------------------------------------------------
Here we essentially use 2 functions from the HTML5 APIs - strokeRect and fillRect and customize them using the corresponding style attributes viz. strokeStyle and fillStyle. But what are these functions for? Well as the name suggest, strokeRect is to draw a rectangle and fillRect first draws the rectangle and then fills it with the color specified in its attributes. Does this remind you of anything familiar?? MSPaint??
So over here we have defined that we need a white background for our playarea and snake has to be displayed in black color squares with white borders to give the impression of the digital displays of our older mobiles.

Now that we have written the drawing functions, let us display our canvas and snake.

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
/* *************************** /
* Initialize the game variables and the game context
* and then sends the game to the main game loop
* *************************** */
function init()
{
// Get game context
context = document.getElementById("canvas").getContext("2d");
//draws the canvas
drawCanvasBoundary();
//draws snake
drawSnake();
}
window.addEventListener("load", init, true);      // insert the listener for onload event to call our init function
-----------------------------------------------------------------------------------------------------------------------
The init function first gets the "game context", 2D in this case (Canvas supports 3D as well!). This context is required to tell our browser, where all the commands needs to be executed. Then we make function calls for the 2 draw functions we previously defined. Lastly, we tell the browser to execute our init function whenever the page gets loaded with the help of addEventListener command. Now reload your page and you should see the following output:


Till now we are just displaying a static output. Now its time to add interactivity and move our snake. Lets define the functions to move snake and the controllers with their behaviors.

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
        /* *************************** /
         * If the rat was eaten, it calculates new rat coordinates,
         * check for collision with snake body and places new rat on canvas
         * ************************** */
        function moveSnake()
        {
            for(var i=0; i < snakeLength; i++)
            {
                bodyX[i] += (vX[i]*sqSize);
                bodyY[i] += (vY[i]*sqSize);
            }
            for(var i=snakeLength-1; i>0; i--)
            {
                vX[i] = vX[i-1];
                vY[i] = vY[i-1];
            }
            // eatRat();  -- Lets hide it for time being, we shall address this later
        }

        /* *************************** /
         * Handles keyboard events to control snake
         * It only acknowledges the arrow keys and ignores the remaining
         * Calculate the new valid direction for snake head
         * for instance - if snake is moving right, it cannot move left even if left arrow is pressed
         * ************************** */
        function keydown(e)
        {
            if(e.keyCode == 65 && vX[0] != 1)       //left arrow - Changed to 'a'
            {
                vX[0] = -1;
                vY[0] = 0;
            }
            else if (e.keyCode == 87 && vY[0] != 1) //up arrow - changed to 'w'
            {
                vY[0] = -1;
                vX[0] = 0;
            }
            else if (e.keyCode == 68 && vX[0] != -1) //right arrow - changed to 'd'
            {
                vX[0] = 1;
                vY[0] = 0;
            }
            else if (e.keyCode == 83 && vY[0] != -1) //down arrow - changed to 's'
            {
                vY[0] = 1;
                vX[0] = 0;
            }
            else if (e.keyCode == 13 && gameOver == true)
            {
                gameOver = false;
                // restart();  // Lets hide it, we shall address at last what to do when game gets over.
            }
        }
       

        /* *************************** /
         * The update and draw game loop
         * ************************** */
        function gameProcess()
        {
            // Sets the interval for next refresh. Game level defines the rate of refresh and thereby increase speed with level
            intervalId = setTimeout(gameProcess, 1000/(6*level));   
            
            clear();
            drawCanvasBoundary();
            
            moveSnake();
            
            drawSnake();
        }

/* *************************** /
* Clears the canvas to empty for redrawing
* not an ideal way but then this is just basic
* ************************** */
function clear()
{
context.clearRect(0,0,width,height);
}
-----------------------------------------------------------------------------------------------------------------------
Okay, so we defined a lot of things here... lets take it one by one
clear() - As the comments say, this function simply clears the entire canvas which is then redrawn.
keydown(e) - Function to handle the key strokes for guiding our snake through the game, currently it only listens to "W,A,S,D" for directions and "Enter" to restart the game after it gets over. Note: I had to change controls from arrow keys to WASD and include a restart key from what I learnt from last blog post - 1. arrow keys were moving the page up n down; and 2. to restart the game, the user had to reload the entire page.
moveSnake() - unlike any other object, different parts of Snake's body can move in directions different than its head which forces us to keep direction coordinates for each and every body part in the Vx and Vy arrays. The motion is however constrained by that of preceding body part. So the direction vector gets simply copied downwards.
gameProcess() or the "Game Loop", every game has this which basically performs - (update-draw), (update-draw),.... repeatedly. The function sets the time for next refresh, clean the canvas, calls moveSnake to calculate the updated coordinates (the update function of game) and finally draws snake (the draw function).

Now, lets refresh the page again... what happened? there is something wrong here...
we forgot to initialize the game loop in our init function, so lets do it now.

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
//setTimeout calls the game loop i.e. gameProcess function after the specified time
intervalId = setTimeout(gameProcess, 1000/6);
//get handle to the div containing our score and level details
scoreDiv = document.getElementById("score");
//get handle to the div containing our score and level details
controlDiv = document.getElementById("control");
//specify the function to handle the keypress
window.onkeydown = keydown;

-----------------------------------------------------------------------------------------------------------------------
Add these 4 commands in the init functions and reload the page. Yippiee!!! our snake is now moving.
So where is the RAT?? lets start putting the rats in the game and make our snake eat them.

Code Snippet
-----------------------------------------------------------------------------------------------------------------------

        /* *************************** /
         * If the rat was eaten, calculates new rat coordinates,
         * check for collision with snake body and place it on canvas
         * ************************** */
        function placeRat()
        {
            if(eaten)
            {
                rX = Math.floor(width*Math.random()/sqSize)*sqSize;
                rY = Math.floor(height*Math.random()/sqSize)*sqSize;
                if(checkFoodCollision(rX,rY))
                    placeRat();
                else
                    eaten = false;
            }
            drawPoint(rX, rY);
     };
        /* *************************** /
         * Iterates through all body parts and compares their x & y coordinates
         * with those of new Rat location sent as the parameter(x & y)
         * ************************** */
        function checkFoodCollision(x, y)
        {
            for (var i = 0; i < snakeLength; i++)
                if(x == bodyX[i] && y == bodyY[i])
                {
                    return true;
                }
            return false;
        }

        /* *************************** /
         * Checks whether the head has reached the rat
         * in case its true, sets flag for calculation of new Rat location
         * and calculates and add a body part at the tail increasing the snakeLength
         * Thereafter, it increments score and check if level needs to be incremented
         * ************************** */
        function eatRat()
        {
            if(bodyX[0] == rX && bodyY[0] == rY)
            {
                eaten = true;
                // calculate the new X & Y location for tail
                var newX = bodyX[snakeLength-1]-vX[snakeLength-1]*sqSize;
                var newY = bodyY[snakeLength-1]-vY[snakeLength-1]*sqSize;
                
                //Add the new tail part in respective arrays
                bodyX.push(newX);
                bodyY.push(newY);
                
                //Initial velocity of the new part will be same as that of old tail
                //so just copy from last part
                vX.push(vX[snakeLength-1]);
                vY.push(vY[snakeLength-1]);

                snakeLength++;      // increment the snakelength
                
                score += 10;        // increment score
                
                // check and increment level
                if((score%100) == 0)
                    level++;
                
                // update score on webpage
                scoreDiv.innerHTML = "Score: " +score+"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Level: "+level;
            }
    }        
-----------------------------------------------------------------------------------------------------------------------
Again lets look at each function one-by-one:
placeRat() - This function uses the math library to generate a random position (x,y) coordinate on the canvas. It then checks if Snake's body is not hiding the new Rat position, if it does - it calls for generating fresh set of coordinates otherwise, it sets eaten Rat parameter to be false and draws the new Rat.
checkFoodCollision() - Function called by placeRat() to check if the new rat position is not hidden under snake's body
eatRat() - Checks if Snake's head is at the location of Rat. If yes, it raise the rat is eaten flag to tell our program to generate new rat position. Then it increments snake length and add new body part at the end. Finally it increments score and determine if the level needs to be increased as well, and then display both of them.

Also unhide the call to "eatRat()" from moveSnake() function and
Add, the call to placeRat() to gameProcess() function
Our game of snake is almost ready, but for one last bit of work. Currently if you try to move the snake past the boundary, it will go and disappear from the skin. Also try folding snake on itself, it will do because we haven't worked on the collision/termination conditions. Lets do it now!

Code Snippet
-----------------------------------------------------------------------------------------------------------------------


        /* *************************** /
         * Checks snake colliding with the boundary walls
         * Snake can collide with itself only if its length is 5
         * else if condition checks for snake length and calls for self collision check
         * ************************** */
        function checkCollision()
        {
            if(bodyX[0] >= width || bodyX[0] < 0 || bodyY[0] < 0 || bodyY[0] >= height)
            {
                scoreDiv.innerHTML = "Score: " +score+"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Level: "+level+"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>Game Over</b>";
                controlDiv.innerHTML = "Press \"Enter\" to restart"
                gameOver = true;
                clearTimeout(intervalId);
            }
            else if(snakeLength > 4)
                {
                    if(checkSelfCollision(bodyX[0],bodyY[0]))
                    {
                        scoreDiv.innerHTML = "Score: " +score+"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Level: "+level+"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>Game Over</b>";
                        controlDiv.innerHTML = "Press \"Enter\" to restart";
                        gameOver = true;
                        clearTimeout(intervalId);
                    }
                }
        }
        
        /* *************************** /
         * Iterates through all body parts starting from 5
         * and compares their x & y coordinates with that of head sent as the parameter(x & y)
         * ************************** */
        function checkSelfCollision(x, y)
        {
            for (var i = 4; i < snakeLength; i++)
                if(x == bodyX[i] && y == bodyY[i])
                {
                    return true;
                }
            return false;
        }        
-----------------------------------------------------------------------------------------------------------------------

We need to check for 2 different types of collision, one is snake colliding with the boundary walls and the other being, self collision.
checkSelfCollision() - This function iterates over the body parts starting from 5 (why?) and checks whether the snake's head has not collided with the body part or not.
checkCollision() - Checks if the snake has collided with the boundary, if not calls for checking selfCollision

Now, just add a call to checkCollision before we drawSnake() in the gameProcess() function, so that the final gameProcess function looks like this

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
function gameProcess()
{
   intervalId = setTimeout(gameProcess, 1000/(6*level));
   clear();
   drawCanvasBoundary();
   placeRat();
   moveSnake();
   checkCollision();
   drawSnake();
}
-----------------------------------------------------------------------------------------------------------------------
Lastly, lets enable the control to restart the game when it gets over.
Unhide the call to restart() function from keydown() function (look at the end where we handle case for keycode 13 i.e. "Enter Key") and then add then define the restart function as below - 

Code Snippet
-----------------------------------------------------------------------------------------------------------------------
/* *************************** /
* Restart the game
* ************************** */
function restart()
{
  bodyX = new Array(150, 150-sqSize, 150-2*sqSize);
  bodyY = new Array(200, 200, 200);

  vX = new Array(1, 1, 1);
  vY = new Array(0, 0, 0);

  snakeLength = 3;

  score = 0;
  level = 1;

 eaten = true;

  scoreDiv.innerHTML = "Score: " +score+"     Level: "+level;
  controlDiv.innerHTML = "Controls: W = Up; A = Left; S = Down; D = Right";

  intervalId = setTimeout(gameProcess, 1000/6);

}
-----------------------------------------------------------------------------------------------------------------------
And we are done!!! Complete source code can be obtained from here
Enjoy the game and do write your suggestions/comments


Snake

Score: 0      Level: 1


Controls: W = Up; A = Left; S = Down; D = Right
HTML5 Games 101 - by Aniruddha Loya
Post a Comment