Thank you HackerNews for the front-page love!
Last month I made my first game: html5-lightcycles. It is a Tron inspired lightcycle game, a humble recreation of the 1980s arcade classic. I wrote a blog post about the experience, you can check it out here. The whole truth behind the lightcycle game includes the tidbit that when I first started playing around with the creating that game, snake was my original intention.
For those that don’t know it, snake is one of the oldest video games, with origins in the 1970s. Nokia put it on cell phones in 1998, and it became a staple of cell phone gaming. The concept is simple: you control a snake that grows when it consumes food, and dies when it hits a wall or itself. Some variants include obstacles, increased speed, and other mechanisms to increase difficulty.
Moving a box across the canvas was easy, and turning off the screen reset gave me lightcycles for free. Getting the snake to turn corners was not as easy as lightcycles, so I abandoned snake and moved on, eventually creating several proof of concept game-like creations. I thought about snake again, and it hit me: the snake is an array, and shift()ing and push()ing coordinates would let it move properly, and when it got food, it could just skip the
shift(). The rest just came together.
If you are visiting this page in an HTML5 capable browser, and are not on a mobile device, then this game should work for you.
You can also view/play the game (in 640x480 mode) directly on jsfiddle.
The Source Code
For those that are interested, here is a quick overview of the code for this game.
First, we need a basic page setup with a canvas tag.
Nothing special here, just a standard page, canvas element, and some basic instructions and description of game play.
Again, very simple. You could really skip all of this and just drop a canvas element on a page, but some basic styling and semantic markup goes a long way.
To make the code more bearable, I have organized the file into the following groups:
- game (object)
- snake (object)
- food (object)
- some helpers/service methods
- an event listener
- the game loop
Well, before all of that, we do have to grab the canvas context like so:
Now that we have that out of the way, lets take a walk thru something more fun.
game object contains controls for score, game state, and some display.
Most of the property names should be self explanatory:
game.scorekeeps the current score of the game, initially 0
game.fpsstores the frames per second (fps) the game is running at. This increases with difficulty.
game.overstarts as true. It just sounds right when checking for game state.
game.messageholds any messages to display to the user, for now there is only the “game over” message.
game.start() method is pretty straightforward: it sets the game state via
game.over = false and clears the
game.score, resets the fps, and then runs an
init() method on the
snake object. We’ll see what that does in a second, but we can assume it initializes the snake “game piece”. The same goes for
game.stop() method sets the game state to over and displays a friendly message explaining how to start a new game.
The next three methods draw things for the game: a box, a score, and a message. The box method is used to draw both food and snake parts, as a snake is just a line of boxes. The
game.drawBox() method takes an x and y coordinate, a size, and a color. With these parameters, it can go to the x,y coordinates specified, and draw a box
size high and wide, and fill it with the
drawScore() is a bit different, as it displays the score almost full hight of the canvas, with a color very close to the background color. This allows the score to be shown without having to sacrifice game board real-estate for the sole purpose of the score.
game.drawMessage() method also is nothing special, it checks if a
game.message exists, and puts in on the canvas if so.
The last method in the
game object is
resetCanvas(). This clears the canvas in between draws to allow the score and messages to be displayed properly, and wiped when needed.
game object sets up a lot of the underpinning for our game, but the real challenge for me, and the coolest part of this game in my opinion, is the
While it’s no Mario, our snake is our hero.
This object is significantly larger than the last one, and has a lot more coolness, also. The first thing the class does is initiate the
snake.size property, and we notice it is dynamic:
size: canvas.width / 40.
This game was designed to run on either a
640x480 canvas. This means our
snake.size is directly proportional to our canvas, and at our predetermined canvas sizes, we’d end up with the following section sizes:
320 / 40 = 8
640 / 40 = 16
Note that here size refers to each section of the snake, so on a
320x240 canvas, our snake will be made up of a bunch of 8x8 pixel squares. Next we initialize the x and y coordinates for the snake, and set the default starting
direction. Lastly, and most importantly, is the
Unlike many game pieces, our snake does not just move, it crawls. As its head moves forward, its body continues to follow the same path, and turning a corner can take several “moves”. To accomplish this, our snake is make up of an array of coordinates, and we will
push() coordinates off and on to the array to update it’s position.
Creating a new snake has a few parts, so there is an
init() method to take care of getting it constructed. This method starts by ensuring the
sections array is cleared and the starting
direction is reset. It then places the snake in the middle of the game board, and again avoids hard-coded positions in favor of allowing for multiple canvas sizes.
We need to remember that when
game.drawBox() creates each section, it begins from the x,y coordinates, then creates a box based on the height and width with the x,y in the center. This means that a box will have a center that is 1/2 the width and 1/2 the height. To abide by these conditions, our snake is positioned accordingly.
init() method builds our starting snake:
This creates 5 array elements, each containing an x,y coordinate that is offset by the
snake.size. Note that we are beginning the array with the tail, and ending it with the head. This will make
shift() remove the tail, and we can use
push() to advance the head position. The
snake.y coordinates always refer to the head position.
Next we have the
snake.move() method. This checks the
snake.direction, which is updated later in the script, and then manipulates the head of the snake accordingly. Upward movement requires, the y coordinate to decrease, down increases, left decreases the x coordinate, and inversely, right increases it. Notice the unit added or removed is
snake.size, since the snake sections sizes are larger than 1px we need to move the head by the size of the sections.
After updating the head position,
snake.move() then invokes
snake.checkCollision(). We’ll cover this method in a minute, just know it determines if the head as collided with the outside of the game area, or with an existing section of the snake. Either of these results in the game being stopped.
move() method continues on to the
snake.checkGrowth() method, which will allow the snake to grow if it is on the same position as a piece of food. Like
snake.checkCollision(), we’ll cover the internals of this function in more detail shortly.
The last thing
move() does is
push() the new
snake.y coordinates onto the
Our next method,
snake.draw(), iterates through the
snake.sections array and then splits the values into arrays with the x and y coordinates, and passes them along to the next method in the class:
Here we circle back to the game.drawBox() function, and pass the x, y, size, and color of our snake. In turn, each section is drawn, and our array can be represented as a series of boxes on our canvas.
Moving on through the
snake object, we see the
isCollision() functions. The first,
checkCollision(), is really just a helper that calls
isCollision() on our snake’s coordinates, and stops the game if there are any collisions found.
Taking a look at
snake.isCollision(), we find that determining if our snake if out of bounds or being cannibalistic is not very difficult:
The first four tests determine if the head of our snake has moved to a position that is outside the boundaries of our canvas. Again we are reminded to check for half of the size of the snake when looking for boundaries. If all those checks pass, we then see if the snake has attempted to eat itself by seeing if the coordinates exist in the
The last method in our
snake class is there to see if our snake needs to grow.
I could have probably named this method better, and future versions may see this refactored out into some more granular functions for readability and maintainability. For now, we’ll dissect it as it is. The method starts off by determining if the head position of the snake is the same as the position of the food, the proverbial mushroom for our hero snake. If the head is on a food piece the function increases the
food.set, and if the score is divisible by 5 it increases the the game speed by 1 FPS. If the snake is not on a food piece, then it
shifts() off the tail section. Notice if the snake eats food, the
shift() never happens. This is how our snake grows.
Our food object is short and sweet.
Like the snake object,
food must have a
color. The real magic in this object is in the
food.set is called, the first thing that happens is the size of the food is set to match the size of the snake. Next, we set this x and y coordinates for randomized food placement. Because our snake really moves around in a grid that is determined by the width of each section, not every pixel on the canvas is accessible by the center of the snake’s head. To place food in a position where can be consumed, some care must be given.
We can determine that
Math.ceil(Math.random() * 10 will return an integer between 1 and 10, and we can multiply that by
snake.size and the ratio it has in relation to the overall canvas. To offset for the center of the snake head, we’ll need to subtract the width,
snake.size / 2. This looks complex, but it really just finds an x,y placement that is inside the canvas and accessible to our snake.
The last method in our
food class is
draw(). Predictably, this simply calls the
game.drawBox() method, and passes along all the food parameters.
To make this game work, we need to be able to utilize a few helpers.
We cannot allow our snake to move the opposite direction it is currently moving, and having a quick place to lookup the inverse of a direction is helpful, so we have a
inverseDirection object where this can be accomplished easily.
We want our users to be able to use multiple sets of keys to control our snake, so we have a
keys object literal that defines our four directions with arrays of char codes for the respective keys. We’ll be accepting WASD, arrow keys, and the vim movement keys HJKL.
The last of our helpers adds a
getKey() method to the Object prototype so we can see if a key code is present in our
keys object, and if so return the key of the matched value. So really all it does it return a direction based on a key’s char code. This makes our life a lot easier when we need to see if a key represents a direction.
We’re on our way to a complete game. All that is left is a way to listen for new input, and the infamous game loop.
Now when a key is pressed, our event listener will fire, and these few lines of code with execute. Here we utilize our
getKey() method to see if the key pressed was a movement key, and if so we also assert that it is not the
inverseDirection of our current
snake.direction. If these conditions are met, the
snake.direction is updated and our snake will be off on a new bearing. If the key was not a direction key, the listener checks to see if it was a spacebar or enter key, and if it is the
game.start() method is called. If the key is none of these, the key-press is ignored.
That is pretty simple, and our helpers made the listener much easier to read and follow along with.
Here were are, the final stage. We’ve done all the heavy lifting of creating our game environment, our player, objectives, scoring, difficulty increases and even the drawing of our assets. Now we need to pull it all together and make it run.
For our game loop, I am using the
requestAnimationFrame method, which requires setting the appropriate, browser specific function. For a detailed look at
requestAnimationFrame, I recommend consulting The Book of Mozilla.
requestAnimationFrame is set, it can be called to update our canvas and will keep our refresh rate at or below 60 FPS. The
requestAnimationFrame accepts a callback that is invoked no more than 60 times per second, and in our case we’ll use it in a recursive
loop(), we see the first thing that happens a check for the games state. If the game is active (not over) then a series of instructions is executed. Let’s review what each of these do:
game.resetCanvas()- Clears the canvas of all drawings
game.drawScore()- Draws the
game.scoreinto the background of our game board
snake.move()- Calculates the new head position of our snake
food.draw()- Places a piece of food on the game board
snake.draw()- Draws our snake by iterating through the
game.drawMessage()- Displays game messages on screen, used for GAME OVER message
By executing these functions multiple times a second, our snake is able to move, and our game becomes alive. To do this, we’ll create an infinite loop.
No human can control our snake advancing 60 times per second, so to slow down animation we can throttle the
setTimeout() method. Remember we initialize
game.fps to 8, so
setTimeout() will allow
requestAnimationFrame to execute 8 times per second, and it will increase along with
game.fps to make the snake move faster over time. There are other methods available for controlling game speed, but I like the way this give the snake that vintage gaming feel, with a bit of a jerking motion when as it slithers to its next meal.
The script in completed with one final line of code that sets our
loop() function in motion: