Recreating "Snake" using HTML5 Canvas and KineticJS
Join the DZone community and get the full member experience.
Join For Free
concept of the game
initially a small sized snake appears on the screen which keeps running for the rest of the game like endless runner. player can change the direction only. speed of the snake increases with time. length of the snake also increases after eating randomly appearing food. increasing length and speed of the snake adds difficulty to the game over time.
we can use storyboard technique to graphically represent our idea.
according to wikipedia: "storyboards are graphic organizers in the form of illustrations or images displayed in sequence for the purpose of pre-visualizing a motion picture, animation, motion graphic or interactive media sequence."
here is our storyboard:
for your convenience i am adding the description of each storyboard screen:
screen 1: snake (square) waiting for a key-press to start moving. circle is shown as food item.
screen 2: after eating (hitting) food item the snake’s length increases and food item appears at another random location.
screen 3: snake can re-enter the playing area from the opposite edge after leaving it from an edge.
screen 4: snake dies after biting itself
here, uml statechart diagram may also help to understand different "states" of the snake during a game.
according to wikipedia: "a state diagram is a type of diagram used in computer science and related fields to describe the behavior of systems. state diagrams require that the system described is composed of a finite number of states; sometimes, this is indeed the case, while at other times this is a reasonable abstraction. many forms of state diagrams exist, which differ slightly and have different semantics."
here is our statechart diagram:
in the diagram, edges represent the actions and the ovals represent the states a snake is in after the specific action. game starts with an idle snake. you can move snake in all four directions using arrow keys. while moving in any direction when a key is pressed, the snake goes to "deciding" state to determine which key was pressed and then again goes to the respective direction. snake eats food when encountered while moving. there is also a "dead" state if snake hits itself while moving.
you may also want to add more state diagrams for prominent objects to clarify. there are other uml diagrams which may help you describe your game project. these diagrams are not only helpful for yourself but also helps if you are working in a team making team communication easy and unambiguous.
structure of the game
the play area in virtually divided into a 200?200-pixel grid having 10?10-pixel cells. the initgrid() function prepares the grid. as you can guess from the code that the snake’s width and height is also 10?10 pixels. so as a programming trick, i used the height and width of the snake to represent the dimensions of a single cell.
function initgrid() { //*****initialize grid ... cell = {"col":col*snakeheight, "row":row*snakewidth}; ... }
you are right if you are thinking about the usage of this virtual grid. in fact, during the initialization of the game structure, this grid helps us to identify the places (cells…to be precise) where we can put the snake and food randomly. a random number generator function randomcell(cells) gives us a random number which we use as an index to the grid array and get the coordinates stored against that specific index. here is the random number generator function…
function randomcell(cells) { return math.floor((math.random()*cells)); }
math.random and math.floor are both javascript functions.
the following code shows the usage of grid and random number generator function…
var initsnakeposition = randomcell(grid.length - 1); //pass number of cells var snakepart = new kinetic.rect({ name: 'snake', x: grid[initsnakeposition].col, y: grid[initsnakeposition].row, width: snakewidth, height: snakeheight, fill: 'black', });
kinetic.rect constructor constructs the initial single rectangle to represent the snake. later, when the snake would grow after eating the food, we would be adding more rectangles. each rectangle is assigned a number to represent its actual position in the snake array. as there is only one rectangle at the moment, we assign it position 1.
snakepart.position = snakeparts;
snakeparts is a counter which keeps counting the number of parts in the snake array. you might be wondering that we have not created any array of snakepart objects but we are talking about array? in fact if you keep the value of name: property same for all the snakepart objects, kineticjs would return all those objects as an array if you ask like this…
var snakepartsarray = stage.get('.snake');
you will see the usage of this feature in action later in the code.
snakepart.position shows how you can add custom properties to kinetic.rect object dynamically, or to any other object.
why we need the position when kineticjs can return indexed array? please don’t bother yourself with this question at the moment, you will find the answer if you keep reading.
two more identifications are required to make the job more easy to manage the snake actions and movements, snake head and tail. there is only one snake-part (rectangle) to begin with therefore both head and tail pointers point to the same rectangle.
var snaketail; var snakehead; ... snakehead = snakepart; snaketail = snakepart;
we are done with setting up the snake. to construct the food which is a simple circle of radius 5 see the following code…
var randomfoodcell = randomcell(grid.length - 1); var food = new kinetic.circle({ id: 'food', x:grid[randomfoodcell].col+5, y:grid[randomfoodcell].row+5, radius: 5, fill: 'black' });
kinetic.circle constructs the food for our snake game. here adding +5 to x and y coordinates to place the circle exactly in the centre of a 10?10 cell provided that the radius of the circle is 5.
after we are done with the creation of basic shapes for our game and their positions on the game area/grid we need to add those shapes to a kinetic.layer and then add that layer to the kinetic.stage.
// add the shapes (sanke and food) to the layer var layer = new kinetic.layer(); layer.add(snakepart); layer.add(food); // add the layer to the stage stage.add(layer);
the ‘stage’ object used in the cod above has already been created in the beginning using the following code snippet…
//stage var stagewidth = 200; var stageheight = 200; var stage = new kinetic.stage({ container: 'container', width: stagewidth, height: stageheight });
container property of stage needs to know the id of the div where we want to show our html5 canvas.
initial screen of the game looks like this once our structure is complete…
after setting up the environment/structure let’s deal with the user stories / use cases one by one.
the main game loop executing after a set interval
var gameinterval = self.setinterval(function(){gameloop()},70);
setinterval is a javascript function which makes a function called asynchronously that is passed as an argument, after the set intervals. in our case gameloop() is the function which drives the whole game. have a look at it…
function gameloop() { if(checkgamestatus()) move(where); else { clearinterval(gameinterval);//stop calling gameloop() alert('game over!'); } }
well, the behaviour of gameloop() is pretty obvious. it moves the snake according to the arrow key pressed. and if snake hits himself then display a game over message to the player and also stop the asynchronous calls by calling a javascript clearinterval() method.
to capture the arrow keys i have used the jquery’s keydown event handler to respond to the keys pressed. it sets a variable ‘where’ that is eventually used by gameloop() to pass the code to actual move() function.
$( document ).ready(function() { $(document).keydown(function(e) { switch(e.keycode) { // user pressed "up" arrow case up_arrow: where = up_arrow; break; // user pressed "down" arrow case down_arrow: where = down_arrow; break; // user pressed "right" arrow case right_arrow: where = right_arrow; break; // user pressed "left" arrow case left_arrow: where = left_arrow; break; } }); });
as you might have guessed already that move() function is actual brain of this game and kinetic.animation handles the actual movement of the objects. all you have to do is to set new locations for your desired objects and then call start() method. to prevent the animation from running infinitely call stop() method immediately after start(). try removing the stop() method and see what happens yourself.
function move(direction) { //super hint: only move the tail var foodhit = false; switch(direction) { case down_arrow: foodhit = snakeeatsfood(direction); var anim2 = new kinetic.animation(function(frame) { if(foodhit) { snakehead.sety(snakehead.gety()+10); growsnake(direction); if(snakehead.gety() == stageheight) snakehead.sety(0); relocatefood(); } else { snaketail.sety(snakehead.gety()+10); snaketail.setx(snakehead.getx()); if(snaketail.gety() == stageheight) snaketail.sety(0); reposition(); } }, layer); anim2.start(); anim2.stop(); break; case up_arrow: ... ...
move the snake
snake is divided into small 10?10-pixels squares. the square on the front is head and the back most square is tail. the technique to move the snake is simple. we pick the tail and put it before the head except when there is only one snake part. the position number assigned to each part of the snake is out of sequence now. head and tail pointers are pointing towards wrong parts. we need to reposition the pointers and position numbers. it is done by calling reposition(). it works as shown in the diagram below…
grow the snake when it eats food
snake eats food when snake’s head is on the food. once this condition is met, food is relocated to some other cell of the grid. the decision is made inside snakeeatsfood() function. the algorithm used is commonly known as bounding box collision detection for 2d objects.
to grow the snake, head moves one step ahead by leaving an empty cell behind. a new rectangle is created at that empty cell to give the impression of the growth of the snake.
//grow snake length after eating food function growsnake(direction) { switch(direction) { case down_arrow: var x, y; x = snakehead.getx(); y = snakehead.gety()-10; resetpositions(createsnakepart(x,y)); break; ... ...
resetpositions() is almost identical to reposition(), see the detils under "move the snake" heading.
assign the food a new location
once the snake eats the food, the food is assigned a new location on the grid. relocatefood() performs this function. it prepares a new grid skipping all the positions occupied by the snake. after creating a new grid array, random number generator function generates a number which is used as an index to the grid and eventually we get the coordinates where we can place the food without overlapping the snake.
re-enter the snake from the opposite edge when it meets and passes an edge
this is really simple. we let snake finish its move, then we check if the head is out of boundary. if it is, we assign it new coordinates to make it appear from the opposite end. the code given below works when snake is moving down, for example…
if(snakehead.gety() == stageheight) snakehead.sety(0);
end the game when snake hits himself or it occupies the whole ground
after each move, checkgamestatus() is called by gameloop() to check if snake has hit himself or not. logic is fairly simple. the same bounding box collision detection method for 2d objects is used here. if coordinates of head matches the coordinates of any other part of the snake, the snake is dead – game end!
live demo
download in package
today we prepared another one good tutorial using kineticjs and html5. i hope you enjoyed our lesson. good luck and welcome back.
Published at DZone with permission of Andrey Prikaznov, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments