
Tutorial: Simple game with HTML5 Canvas
No Tears Guide to HTML5 Games
HTML5 Game Development Tutorial: Breakout
I decided to create a game by myself. The trend on the web is to recreate classic games, Pac Man, Snakes, Breakout, just name a few. For me, I decided to take a shot at Tetris. Tetris is so popular that everybody knows how to play and I don't even need to explain the rules. So let's start my little project right now. (Warning: this tutorial will not include the details like how to draw a line on html 5 canvas, you need to look it up the Internet).
Play the game
Before we start thinking about which data structure to use and writing any line of code, let me give you a basic idea of developing a game, which is shared among all the three tutorials I've read. We all know how a cartoon is made - to make any object moving, we need at least two static pictures. By switching the two pictures fast enough, the object become dynamic in human eyes. Our game is made in the same way. There will always be a method, let's call it update() for now, that calculate the current position of all the objects on our canvas and another function draw() which draws the objects on canvas. By calling these two methods frequently enough, say 20 milliseconds a time, we can make objects moving around on the canvas.
Now you've got the basic idea of how a game is created, so let's start programming. First of all, we're gonna need a canvas which displays our Tetris game. For a typical Tetris game, there should be 10 squares horizontally and 20 squares vertically. In our Tetris game, I'm going to make each square 20 pixel long, which maks our canvas 400 pixel high and 200 pixel wide. This is enough for playing the game, but in a real game we also need some area to display the next Tetriminos that's gonna fall and game statistics like levels, scores and lines. So we need to widen our canvas a little bit to 300 pixel.
The initial html file and javascript file look like this
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <title>Tetris</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <style type="text/css"> body { margin:0px; padding:0px; text-align:center; } canvas { margin-left: auto; margin-right: auto; border:1px solid black; } </style> </head> <body> <h1>Tetris</h1> <canvas id="canvas" width="300" height="400"></canvas> <script src="game.js"></script> </body> </html>
var WIDTH = 300, HEIGHT = 400, c = document.getElementById('canvas'), ctx = c.getContext('2d'); setInterval(function(){ clearCanvas(); updatePosition(); drawOnCanvas(); }, 1000/50); var clearCanvas = function(){ ctx.fillStyle = 'White'; ctx.beginPath(); ctx.rect(0, 0, WIDTH, HEIGHT); ctx.closePath(); ctx.fill(); } var drawLine = function(){ ctx.beginPath(); ctx.moveTo(200, 0); ctx.lineTo(200, 400); ctx.stroke(); } var updatePosition = function(){ } var drawOnCanvas = function(){ drawLine(); }
In the html file, we create a canvas, whose width and height are 300px and 400px respectively. We also link this html to an external javacript file, where we put all our game logic. Nothing fancy here except that you need to make sure you put your javacript link code "<script src="game.js"></script>" right before the closing tag of "body" element. Otherwise your javacript is gonna be executed before the html is loaded, resulting in an error. In the javascript file, we have three main functions, clearCanvas, updatePosition, and drawOnCanvas. They are all put in the function which is called once per 20 millisecond, implemented by the function setInterval(). This function (we will call it game loop throughout this tutorial), which we set as the first parameter of function setInterval(), acts just like the update function and draw function I've mentioned in the second paragraph. Within each call, we clear everything that's been drawn on the canvas, then update the position of all the objects that're gonna be drawn, and finally draw them on canvas. For now there is nothing in the updatePosition() function and only one function in drawOnCanvas() function, which basically just draw a black line to separate the gaming area and game statistics area.
For now, our Tetris Game looks like this.
Everything looks good. Time to create Tetriminos? Not yet. We'll start with creating the squares that form all kinds of Tetriminos. After we create the square object, implementing Tetriminos will become much easier.
var speedLevels = [20, 16, 12, 10, 8], currSpeed = speedLevels[0]; var Square = function(speed){ var self = this; self.color = "Black"; self.vPosition = 0; self.hPosition = 4; self.speed = speed; self.temp = 0; self.fall = function(){ if(self.temp == self.speed){ self.vPosition++; self.temp = 0; } self.temp++; } self.draw = function(){ console.log(self.vPosition*squareLength); ctx.fillStyle = self.color; ctx.fillRect(self.hPosition*squareLength, self.vPosition*squareLength, squareLength, squareLength); } return self; }
In the code above, we create a function to generate squares. It takes one parameter, the speed, which decides the falling speed of the square. In this Tetris game, we have five levels. As the level goes up, the falling speed increases. You might notice that the speed of a higher level is lower than the speed of a lower level. That's because the number is actually not the falling speed, it is the number of times a square stays at a certain position. The longer it stays at one position, the slower it falls. After creating the "Square" function, we can now call the "fall" function in "updatePosition" function, and "draw" function in "drawOnCanvas" function. Now you have a square falling down from the top of the game area.
When the square reaches the bottom, it continues falling down to the invisible part of the canvas. This is not what we want, we want it to stay still when it reaches the bottom. Also we want the squares coming down one at a time, meaning at any point there should be only one active square. To achieve these, I added the following code.
var gameGrid = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]; self.fall = function(){ if(self.counter >= self.speed){ if(self.checkFalling()){ self.vPosition++; } else{ gameGrid[self.vPosition][self.hPosition] = 1; self.active = false; } self.counter = 0; } self.counter++; }self.checkFalling = function(){ if(gameGrid[self.vPosition+1][self.hPosition] == 1) return false; else return true; } var drawFixedSquares = function(){ for(var i=0; i<20; i++){ for(var j=0; j<10; j++){ if(gameGrid[i][j] == 1){ ctx.fillStyle = "Black"; ctx.fillRect(j*squareLength, i*squareLength, squareLength, squareLength); } } } } var generateNextSquare = function(){ if(!currentSquare.active) currentSquare = new Square(currSpeed); } var currentSquare = new Square(currSpeed);
To keep track of which part of the game area has been occupied, I created a 20*10 array called gameGrid. The number in the array can be 0, not occupied, or 1, occupied. I added an extra line at the bottom, which represents a line of fixed squares and locates right beneath the bottom line to prevent the falling square from sinking down beneath the bottom. Remember there are two properties of a square called vPosition and hPosition. They corresponds to the indices of a square in the gameGrid. For instance, if you have a square of vPosition equal to 4, and hPosition equal to 6. Then it is in position gameGrid[4][6]. The "checkFalling" function is added to the Square function. It checks if there is an fixed square right under the current postion of the calling square. The "fall" function of Square is getting modified, so that right before a square fall down to a lower height, it checks if there is already some fixed square there. If there is, this square is marked as inactive and corresponding position in gameGrid is marked as occupied. The drawFixedSquares function is added as a global function, which draws all the fixed squares we keep track of in gameGrid. We also need a global variable, currentSquare, to hold the current active square. And the function generateNextSquare is used to generate the next square when the current square become inactive. You need to put generateNextSquare in our game loop.
setInterval(function(){ clearCanvas(); generateNextSquare(); updatePosition(); drawOnCanvas(); }, 1000/50);
Now when you run the game, you can see a square falling down from the top of the game area, and stops when it reaches the bottom. After the first square become inactive, comes the second square and then the third ...
What we're going to do next is to make the square move to right or left when we push the right or left button. You can control your Tetriminos using four keys, up, down, left, right. The left, right button moves the Tetriminos to the left or right, the down button makes the Tetriminos fall faster. And the up button rotates a Tetrimino.
var monitorKeyboard = function (){ $(document).keydown(function(evt){ if(evt.which == 39){ if(currentSquare.hPosition < 9 && !currentSquare.fixedSquareOnRight() ) currentSquare.hPosition++; } else if(evt.which == 37){ if(currentSquare.hPosition > 0 && !currentSquare.fixedSquareOnLeft() ) currentSquare.hPosition--; } else if(evt.which == 40){ if(currentSquare.vPosition < 19 && !currentSquare.fixedSquareUnder() ) currentSquare.vPosition++; } }); } self.fixedSquareUnder = function(){ if(gameGrid[self.vPosition+1][self.hPosition] == 1) return true; else return false; } self.fixedSquareOnRight = function(){ if(gameGrid[self.vPosition][self.hPosition+1] == 1) return true; else return false; } self.fixedSquareOnLeft = function(){ if(gameGrid[self.vPosition][self.hPosition-1] == 1) return true; else return false; }
To monitor user's input, I wrote a function called monitorKeyboard, which uses .keydown() to monitor a key down event. When player pushes the right key, it first checks if the square is right next to the right edge of the game area or if there is a fixed square at its right. If either of the two situation occurs, then the square cannot be moved to the right. Similar code applies to the left key and down key. You can call monitorKeyboard before the game loop. The other three function fixedSquareUnder, fixedSquareOnRight and fixedSquareOnLeft are added to the Square function. Now you can control the square by making it move to right, left, or fall faster. Experiment the new functionality by initializing the gameGrid with multiple 1's. See if the fixed squares prevent a falling square from moving to left, right or down.
At this point, if there is a line which is full of fixed squares. The game does not do anything. In a complete Tetris game, when any line is full of fixed squares, we want it to be cleared, and any lines with fixed squares on higher levels moves downward. I wrote six functions to implement this logic.
var checkLinesToClear = function(){ var fixSquareNum; var linesToClear = []; var counter = 0; for(var i=0; i<20; i++){ fixSquareNum = 0; for(var j=0; j<10; j++){ fixSquareNum += gameGrid[i][j]; } if(fixSquareNum == 10){ linesToClear[counter] = i; counter++; } } updateGameGrid(linesToClear); return counter; } var updateGameGrid = function(linesToClear){ var len = linesToClear.length; var linesToMove; if(len==0) return; for(var m=0; m<len; m++){ clearLine(linesToClear[m]); } for(var i=19; i>-1; i--){ if(!emptyLine(i)){ linesToMove = levelsToMoveDown(linesToClear, i); if(linesToMove>0) moveLineDown(i, linesToMove); } } } var emptyLine = function(currLine){ for(var j=0; j<10; j++){ if(gameGrid[currLine][j]==1){ return false; } } return true; } var levelsToMoveDown = function(linesToClear, currLine){ var len = linesToClear.length; var counter = len; for(var i=0; i<len; i++){ if(linesToClear[i]==currLine){ return 0; }else if(linesToClear[i]>currLine) { return counter; } else{ counter--; } } return counter; } var clearLine = function(lineNumber){ for(var i=0; i<10; i++){ gameGrid[lineNumber][i] = 0; } } var moveLineDown = function(lineNumber, lines){ for(var i=0; i<10; i++){ gameGrid[lineNumber+lines][i] = gameGrid[lineNumber][i]; } clearLine(lineNumber); }
The checkLinesToClear function checks if there is any line that is full of fixed squares. And store the line number (0~19) of these lines into an array. It then calls function updateGameGrid, which clears the lines full of squares and moves all the lines on higher levels down. The game checks lines to clear each time a square becomes inactive, so checkLinesToClear is added to the fall function as shown below.
self.fall = function(){ if(self.counter >= self.speed){ if(!self.fixedSquareUnder()){ self.vPosition++; } else{ gameGrid[self.vPosition][self.hPosition] = 1; self.active = false; checkLinesToClear(); } self.counter = 0; } self.counter++; }
The other functions are just helpers. emptyLine takes the line number (0~19) as argument and returns true if there are some fixed squares within that line, and false otherwise. levelsToMoveDown returns the number of levels a line need to move down to after some fully loaded lines are cleared. The number of levels to move down might be different for different lines. clearLine clear a fully loaded line, i.e. setting the corresponding positions in gameGrid to 0s. moveLineDown moves a line down by given levels. Now you can experiment the new functionality by initializing the gameGrid with lines that just need one square to be cleared. Below is how I initialize the gameGrid.

When the falling square reaches the bottom, the bottom line is cleared, and concurrently all the lines with fixed squares move down one level.
Now we have a fully functional Tetris game (the square version). In the next tutorial, I'm going to show you how to create Tetriminos and implement the control over them based on what we've completed.