Tasty orange squares
We’ve improved our game to the point where we can add more features. On the whole, our game right now is looking pretty boring, the snake just wonders around in never-ending and seemingly infinite limbo where nothing interesting ever happens. So let’s change that! We want to add food items which randomly appear on the map and – when bumped into by the snake – make the snake longer. Let’s break this problem into parts and work on every part individually.
Let’s start by adding some code to the drawGame
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function drawGame() { // first we need to clear everything currently on canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // now, let's draw the snake ctx.fillStyle = COLOR_SNAKE; // set drawing color snake.forEach(function(cell) { // draw out each individual cell var cell_x = cell[0] * CELL_SIZE; var cell_y = cell[1] * CELL_SIZE; ctx.fillRect(cell_x, cell_y, CELL_SIZE, CELL_SIZE); }); ctx.fillStyle = COLOR_FOOD; // set drawing color food.forEach(function(cell) { var cell_x = cell[0] * CELL_SIZE; var cell_y = cell[1] * CELL_SIZE; ctx.fillRect(cell_x, cell_y, CELL_SIZE, CELL_SIZE); }); } |
We are doing the exact same thing for food as we did for snake cells. As you can see, the code is nearly identical. When you see code like this you should always consider simplifying it by writing functions you can reuse. Let’s do just that and add a brand new function immediately above the drawGame
function. We’ll call it drawCells
:
1 2 3 4 5 6 7 8 9 |
function drawCells(cells, color) { ctx.fillStyle = color; // set drawing color cells.forEach(function(cell) { // draw out each individual cell var cell_x = cell[0] * CELL_SIZE; var cell_y = cell[1] * CELL_SIZE; ctx.fillRect(cell_x, cell_y, CELL_SIZE, CELL_SIZE); }); } |
Now we will change the drawGame
function correspondingly:
1 2 3 4 5 6 7 |
function drawGame() { // first we need to clear everything currently on canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // now, let's draw everything drawCells(snake, COLOR_SNAKE); drawCells(food, COLOR_FOOD); } |
As a general rule of thumb, try to always avoid redundancy in code. You don’t need to do obsess and lose sleep over it, just keep it in mind as a good general practice. When you spot repeating code, the light-bulbs in your head should start flickering. Cleaning code like this up makes things easier for your future self or someone else who might be working on your code.
The code to draw food is now in place but we still need to figure out a way to create food items to begin with. Our food
variable is initialized to an empty list, now let’s find a way to fill it up!
Let’s create a new function, we’ll name it makeFood
. Add this code immediately above the updateGame
function:
1 2 |
function makeFood() { } |
Since we want to place food items on screen at random, we will also need a random number generator. For ease of use, let’s make it a function. Add this code immediately above the makeFood
function:
1 2 3 |
function getRandomNum(a, b) { return Math.floor(Math.random() * b + a); } |
Math.random() generates a random number in range [0, 1). Our function multiplies this number by b and adds a, and then applies Math.floor to the result. This gives us a random number in range [a, b]. Now we can conveniently use this function when generating random coordinates.
Let’s also add a call to makeFood
at the end of our updateGame
routine. Add the two lines to the bottom:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
function updateGame() { // move the snake var head = snake[snake.length - 1]; // the last element is the head var next = [head[0], head[1]]; // our next head if (direction.length > 0) { heading = direction.shift(); } switch (heading) { case UP: // move the snake along the y axis up next[1]--; if (next[1] < 0) next[1] = GRID_H - 1; break; case DOWN: // move the snake along the y axis down next[1]++; if (next[1] > GRID_H - 1) next[1] = 0; break; case LEFT: // move the snake along the x axis left next[0]--; if (next[0] < 0) next[0] = GRID_W - 1; break; case RIGHT: // move the snake along the x axis right next[0]++; if (next[0] > GRID_W - 1) next[0] = 0; break; } // push the new head into the snake snake.push(next); // cut the tail of the snake snake.shift(); // generate food makeFood(); } |
Now that we’ve got everything set up, let’s play around with the makeFood
function. The problem goes as follows. While the game plays out, we want to generate food with random x and y coordinates. At the same time, we don’t want the food to pop up on top of the snake, so let’s make our code exclude all the cells that are currently occupied by the snake. To achieve this, we will loop through all the cells contained in the snake
list and see if any of them matches our random coordinates. If we find a match, we’ll simply abort the food generation and exit the function, and let it have another go on the following update – since this function will be continuously called, this won’t pose much of a problem and we can always give it a try on the next go. We also want to limit the number of food items, let’s go with 5 for now.
With that in mind, let’s add some code to the makeFood
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function makeFood() { // attempt to add food to the play field // if the generated coordinates contain the snake, abort if (food.length >= 5) return; // disallow more than 5 food items // first create some random coordinates var x = getRandomNum(0, GRID_W - 1); var y = getRandomNum(0, GRID_H - 1); // check every cell of the snake for these coordinates // and abort if necessary for (var i = 0; i < snake.length; i++) { var cell = snake[i]; if (cell[0] === x && cell[1] === y) return; } // if we reached this point, we can add food to the list food.push([x, y]); } |
But we may be forgetting something. What if food spawns on top of another food? This may mess up our game logic further down the line, so let’s try and fix this. Learning from our previous encounters with redundant code, let’s add another function immediately above the makeFood
function and call it checkCells
:
1 2 3 4 5 6 7 8 |
function checkCells(x, y, cells) { for (var i = 0; i < cells.length; i++) { var cell = cells[i]; if (cell[0] === x && cell[1] === y) return i; } return -1; } |
Here, we look for matches between x and y coordinates and any elements within some arbitrary list of cells
that is given to the function. We then return an index of the item it found on hit, or -1 on miss when we find no items. This will come in useful later.
Let’s now edit the makeFood
function like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function makeFood() { // attempt to add food to the play field // if the generated coordinates contain the snake, abort if (food.length >= 5) return; // disallow more than 5 food items // first create some random coordinates var x = getRandomNum(0, GRID_W - 1); var y = getRandomNum(0, GRID_H - 1); // check every cell of the snake for these coordinates // and abort if necessary if (checkCells(x, y, snake) >= 0) return; // now check every food cell for these coordinates // and abort if necessary - we don't want doubled food if (checkCells(x, y, food) >= 0) return; // if we reached this point, we can add food to the list food.push([x, y]); } |
Now we need to deal with the act of the snake actually eating food items on screen. If you run the game right now, you will observe that food items do in fact show up one by one, but the snake simply ignores them and goes right through them. We will need to alter the updateGame
function so it takes food into account. What we will do is simply check, on each update, whether or not the new head of the snake is touching food. If so, we will dispose of this food and extend the snake by ignoring the final operation of cutting its tail. Let’s code these ideas into updateGame
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
function updateGame() { // move the snake var head = snake[snake.length - 1]; // the last element is the head var next = [head[0], head[1]]; // our next head if (direction.length > 0) { heading = direction.shift(); } switch (heading) { case UP: // move the snake along the y axis up next[1]--; if (next[1] < 0) next[1] = GRID_H - 1; break; case DOWN: // move the snake along the y axis down next[1]++; if (next[1] > GRID_H - 1) next[1] = 0; break; case LEFT: // move the snake along the x axis left next[0]--; if (next[0] < 0) next[0] = GRID_W - 1; break; case RIGHT: // move the snake along the x axis right next[0]++; if (next[0] > GRID_W - 1) next[0] = 0; break; } // push the new head into the snake snake.push(next); // check if we encountered food var ate = checkCells(next[0], next[1], food); // if we've eaten, remove the food item we just ate if (ate >= 0) food.splice(ate, 1); // cut the tail of the snake if (ate === -1) // skip if we ate this turn snake.shift(); // generate food makeFood(); } |
The way we coded our checkCells
function now turned out to be very handy. We use a single variable ate
to give us information on both whether or not we ate a food item or not, and at which index the item is if we did. We then use Array.splice() to remove the item from the list. To grow the snake, we simply skip the next part where we remove the tail of the snake from the list in case we’ve eaten. Tink of it like this: whenever ate
is -1, our snake is roaming around in empty space, so we keep removing the tail to make the snake go. But when we encounter food, ate
goes to some number greater than -1, in which case we can make the snake grow by not cutting the tail on that single update frame.
We can now test the game and marvel at the snake’s newly gained ability to eat! It should look and play something like this:
Om nom nom nom
As you can observe, our snake grows as the player eats food. However, food seems to reappear immediately after it is eaten. Also we can observe that at the start of the game the food is generated 5 times in a sequence of frames.
Let’s add some code into our resetGame
function so that we generate some food items the game begins:
1 2 3 4 5 6 7 8 9 |
function resetGame() { // setup game variables snake = [[1, 1], [2, 1], [3, 1], [4, 1], [5, 1]]; food = []; for (var i = 0; i < 5; i++) makeFood(); heading = RIGHT; direction = []; } |
That fixes the problem of food popping up in sequence at the start of the game. When we now run the game, we’re most likely (that is – unless generation fails) to begin the game with 5 food items randomly placed on screen.
To give the game a more slow paced feeling, we will change the code a bit so that food isn’t generated continuously. We need to tell the computer to only create more food items once in a blue moon. Let’s make a small change to the updateGame
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
function updateGame() { // move the snake var head = snake[snake.length - 1]; // the last element is the head var next = [head[0], head[1]]; // our next head if (direction.length > 0) { heading = direction.shift(); } switch (heading) { case UP: // move the snake along the y axis up next[1]--; if (next[1] < 0) next[1] = GRID_H - 1; break; case DOWN: // move the snake along the y axis down next[1]++; if (next[1] > GRID_H - 1) next[1] = 0; break; case LEFT: // move the snake along the x axis left next[0]--; if (next[0] < 0) next[0] = GRID_W - 1; break; case RIGHT: // move the snake along the x axis right next[0]++; if (next[0] > GRID_W - 1) next[0] = 0; break; } // push the new head into the snake snake.push(next); // check if we encountered food var ate = checkCells(next[0], next[1], food); // if we've eaten, remove the food item we just ate if (ate >= 0) food.splice(ate, 1); // cut the tail of the snake if (ate === -1) // skip if we DIDN'T eat this turn snake.shift(); // generate food if (getRandomNum(0, 12) === 0) makeFood(); } |
We now only generate more food whenever a randomly generated number in range [0, 12] equals zero. Or in human terms, on every update to our game, we roll a dice that has 13 sides (13 because we include the zero) and see if it rolls 0, and only then create more food.
Our game should now feel more natural to play as food takes some small amount of time to reappear after it is eaten:
Now that we have what appears to almost be a fully playable game, we need to deal with one more nuisance – collision detection. We will code this up in the following section.
Great Tutorial !
Hello! Cool post, amazing!!!
Thanks!