A simple snake game in pure HTML and JavaScript, part 2
You should first read part 1 of the tutorial.
This is the second part of our quest to make a simple Snake game in the browser. In this part, we will code the game loop, rendering and controls for the game.
We left off with an empty game board in part 1, with the game state set up. Now it’s time to start implementing the game logic. You can view the result from part 1 here, right click → View Source to see the code.
The game loop
First thing we’ll do is to make the game loop. The game loop is a generic term for the code that constantly runs in the background inside games. This way, games are different from most other programs in that they require no input from the user to do things. Think about it, most computer programs do not do anything unless you click a key, move the mouse or tap a button. But games always do something, even when the user is just looking at the screen. The code that runs constantly in the background, is called the game loop.
To run the game loop; we will create a new function. We call it simply gameLoop
.
This function will run repeatedly during the duration of the game. It will handle
rendering and moving the snake around the board.
function gameLoop() {
// This function calls itself, with a timeout of 1000 milliseconds
setTimeout(gameLoop, 1000);
}
The setTimeout
call will the the function again, recursively, every second so
that any code inside will run repeatedly in the background.
We also need to start the loop by adding an initial call to gameLoop
at
the end of the startGame
function:
function startGame() {
// ... code from before
startGame();
// Start the game loop (it will call itself with timeout)
gameLoop();
}
The next step is to iterate over the entire board and update the DOM
(ie. the HTML div
elements we created in part 1) with a different
CSS class if the corresponding cell contains the snake.
We do this by looping over the Y and X axes, and checking if the board
has a snake
, if so, we set the element’s class to "snake"
. Else we
remove any class set.
function gameLoop() {
// Loop over the entire board, and update every cell
for (var y = 0; y < boardHeight; ++y) {
for (var x = 0; x < boardWidth; ++x) {
var cell = board[y][x];
if (cell.snake) {
cell.element.className = 'snake';
}
else {
cell.element.className = '';
}
}
}
// This function calls itself, with a timeout of 1000 milliseconds
setTimeout(gameLoop, 1000);
}
We also need to style the snake
class, so it looks like a snake (green).
Add the following inside the <style>
tag to do that:
#board .snake {
background-color: green;
}
Now when we refresh the page, the center element on the board should be green. It should look like the example below.
Now that the initial snake is visible, we need to make it to move around the board.
Moving the snake
To make the snake move, we need to extend the game loop.
Before we update the board, we should look at what direction the snake is heading.
Using the direction, we update the position
of the snake (the snakeX
and snakeY
global variables) accordingly. Then we also
need to update the board (the board[y][x].snake
variable). When the board is
updated, it will automatically update div
element with the snake
class, because
the code that does that will follow immediately after our check.
So what does this look like in code? We have the snakeDirection
global variable
to define the direction the snake is heading, right now it always has the value "Up"
,
but we also need to check for "Left"
, "Right"
, "Down"
. For each direction,
we change the value of either snakeX
or snakeY
by 1. Changing snakeY
will move
the snake either up or down, changing snakeX
will move the snake left or right.
Remember that snakeX = 0
and snakeY = 0
is the upper left of the board. So
to make the snake move right, we increase snakeX
. To make it go down, we increase
snakeY
. And the reverse for left and up. In code, it looks like this:
// Update position depending on which direction the snake is moving.
switch (snakeDirection) {
case 'Up': snakeY--; break;
case 'Down': snakeY++; break;
case 'Left': snakeX--; break;
case 'Right': snakeX++; break;
}
// Update the board at the new snake position
board[snakeY][snakeX].snake = 1;
After updating the snake position, we also update the board at the new position
to set the snake as present. This code should be inserted inside gameLoop
before
the double for-loops.
When you run the code, this should be the result:
As you can see, the snake moves up the board slowly, since the direction is always "Up"
,
the next step will be to make it possible to change the direction of the snake.
Handling input
Next step is handling input. Input will be done using the keyboard (sorry, mobile readers).
To capture this we need to bind the onKeyDown
event. We check what key was pressed and
update the snakeDirection
accordingly.
We create a new function to do this, called enterKey
function enterKey(event) {
// Update direction depending on key hit
}
We also need to add the onkeydown
event handler to the body
element.
Here we already have one event handler to initializes the game, so add another
one that handles the key presses:
<body onload="initGame()" onkeydown="enterKey(event)">
Inside the function, we will use the another switch statement to check what key was pressed — in this case we check for all the arrow keys — and update the direction of the snake. This will then automatically get picked up by the game loop and the we will be able to control the snake!
function enterKey(event) {
// Update direction depending on key hit
switch (event.key) {
case 'ArrowUp': snakeDirection = 'Up'; break;
case 'ArrowDown': snakeDirection = 'Down'; break;
case 'ArrowLeft': snakeDirection = 'Left'; break;
case 'ArrowRight': snakeDirection = 'Right'; break;
default: return;
}
// This prevents the arrow keys from scrolling the window
event.preventDefault();
}
The call to preventDefault
is necessary, so that pressing the arrow keys do not perform their
default action (that scrolls the page up/down/left/right).
Try the link below to try it out, note that if you collide with the walls, the game will crash and you can’t play any more. If that happens, refresh to restart the game.
Try out controlling the snake in a new tab
The walls
Let’s fix that bug with the walls that makes the game crash, and we’ll have a controllable snake game!
To check for the walls, we simply need to add an if
statement inside the game loop
after updating the snake position. This will check if the new position is outside the
board by comparing against 0
(snake can’t be on a negative position) and boardWidth
/boardHeight
.
If we detect that the snake is outside the board, we restart the game by calling startGame
,
this will reset both the position, length and direction of the snake.
// Check for walls, and restart if we collide with any
if (snakeX < 0 || snakeY < 0 || snakeX >= boardWidth || snakeY >= boardHeight) {
startGame()
}
// Update the board at the new snake position
board[snakeY][snakeX].snake = 1;
We also need to update startGame
to clear the board from the snake, so the old
snake is not visible when the game restarts. Add the following for loop inside
startGame
to do that:
// Clear the board
for (var y = 0; y < boardHeight; ++y) {
for (var x = 0; x < boardWidth; ++x) {
board[y][x].snake = 0;
}
}
Now we can play the game without it crashing. Try it out by clicking below and playing using the arrow keys:
However, the snake remains on the board as we move around. So we have one thing left on the todo list.
Making the snake disappear
To remove the snake, we need to keep track of the tail.
We can use the board to store this information. So rather than trying to keep track of where different snake segments are on the board. We store how many more iterations of the game loops the Snake should be visible in every cell.
We then simply decrease this as we iterate over the board.
We need to make a few small changes to accommodate this. First, we need
to replace the assignments board[snakeY][snakeX].snake = 1;
to assign
the snakeLength
instead. So find the two places with that piece of code and
replace with: board[snakeY][snakeX].snake = snakeLength;
Now, instead of just storing if there is a snake on the board, we store the remaining length of the snake on that cell.
The next part is changing the loop over the entire board. Instead of checking for cell.snake
,
we want to check if the value is greater than zero. If so, there is a snake on that cell,
so we still set the snake
class, but we also decrease the value of cell.snake
by 1.
The update code inside the game loop will look like this:
var cell = board[y][x];
if (cell.snake > 0) {
cell.element.className = 'snake';
cell.snake -= 1;
}
else {
cell.element.className = '';
}
Finally I also decreased the setTimeout
call to 1000 / snakeLength
. This make the speed of the game dependent on the length of the snake. Try out the result below:
Now we have an almost playable version of the game. All that remains is collecting those delicious apples (and not being able to move through yourself). We’ll write this code in the next part.
Read part 3 of the tutorial.