Flash Snake Game Tutorial

[Contents]

Introduction

The "snake game" (it has several names) is one of the simplest game concepts ever, and just like Tetris it's very addictive. There are a lot of variations of this game written in Flash, and this tutorial will explain one way to create it. It's a relatively easy game to code, but many fail to make sure that when keys are pressed in rapid succession they are all registered. This is necessary if you want to have full control of the snake at all times.

I will assume that you know at least some ActionScript. If you are a beginner some techniques are probably new. This tutorial will include storing information in a two dimensional array, the concept of a queue, using a key listener, attaching movie clips at run-time and some other things like the very useful modulo operator (%).

The Game

Probably the most common version of the game is the one above. Your goal is to move the snake and eat as many "food" blocks as possible. There is only one food block at any given time. When the food is eaten, the snake grows in length. If you hit a wall or the snake itself the game is over. Other variations include several food blocks visible at the same time and the ability to move through the walls and appear from the opposite wall. Both of these variations are explained as well.

Download snakeTutorialMX.zip (Zipped Flash MX FLA, 7 Kb)
Download snakeGameWithHighscore.zip (Zipped files, 33 Kb)

Implementation

Let's jump into the implementation of the game. It is written in Flash MX. The code is located on frame 1 (the main timeline is only one frame). The line numbers in the text refers to the line numbers in the code snippets on this page, not in the .fla file.

The first thing to do is to define some variables:

01. blockSize = 8;   // the block width/height in number of pixels
02. gameHeight = 30; // the game height in number of blocks
03. gameWidth  = 45; // the game width in number of blocks
04. SNAKE_BLOCK = 1; // holds the number used to mark snake blocks in the map

The game is divided into blocks, or a grid pattern. The blockSize variable holds the size of a block in pixels. In this case the block size is 8, which means that the snake building blocks and the food should be 8x8 pixels if we want them to each cover one block (in my example the snake and food blocks are 7x7 pixels which creates a thin border around each block. Looks better I think).

The gameWidth and gameHeight variables hold the dimension of the game area. Since the game is 45x30 blocks in size, and each block is 8x8 pixels, the entire game area takes up (45*8)x(30*8) = 360x240 pixels. The SNAKE_BLOCK variable is used to mark a position on the game area as occupied by the snake. The use of SNAKE_BLOCK instead of 1 directly is purely for making the code more readable.

The next part is the keyListener object. It has an onKeyDown method defined, which is called every time the user presses a key:

01.  keyListener = new Object(); // key listener
02.  keyListener.onKeyDown = function() {
03.         var keyCode = Key.getCode(); // get key code
04. 
05.         if (keyCode > 36 && keyCode < 41) { // arrow keys pressed (37 = left, 38 = up, 39 = right, 40 = down)...
06.                 if (game.onEnterFrame != undefined) { // only allow moves if the game is running, and is not paused
07.                 		 if (keyCode-37 != turnQueue[0]) { // ...and it's different from the last key pressed
08.                    		 	     turnQueue.unshift(keyCode-37); // save the key (or rather direction) in the turnQueue
09.                		 }
10.		     }
11.         } else if (keyCode == 32) { // start the game if it's not started (32 = SPACE)
12.                 if (!gameRunning) {
13.                         startGame();
14.                 }
15.         } else if (keyCode == 80) { // pause/unpause (80 = 'P')
16.                 if (gameRunning) {
17.                         if (game.onEnterFrame) { // pause
18.                                 delete game.onEnterFrame; // remove main loop
19.                                 textMC.gotoAndStop("paused");
20.                         } else { // exit pause mode
21.                                 game.onEnterFrame = main; // start main loop
22.                                 textMC.gotoAndStop("hide");
23.                         }
24.                 }
25.         }
26. };
27. Key.addListener(keyListener);
We get the last key's code and store it in the local variable called keyCode (line 3). Then we check if the code is 37, 38, 39 or 40 (line 5). That's the key codes for the arrow keys left, up, right and down. If an arrow key has been pressed we insert the key press subtracted by 37 to the beginning of the array turnQueue. That way 0-3 represents the different turns. Before it is added to the queue we make sure the turn is not already at the beginning of the queue (line 7). It does not make sense to have several turns of the same kind after each other in the queue.

As you will see later, a turn is picked each frame from the end of the turnQueue and that turn is performed. This way we can save all turns in this "buffer" if the player makes turns faster than the frame rate. Let's say the queue contains: 0, 1 and we add 3 to it: 3, 0, 1. The next frame we will turn up (1), the frame after that we turn left (0) and after that down (3). If we insert new values to the front (unshift) then we should pick them from the back (pop). We could insert them to the end (push) and then pick them from the front (shift). The idea is that the values leave the queue in the same order as they entered (unlike a line of people at the pub where some drunken bastard cuts in front of you...).

The next two parts in the onKeyDown method above make sure the game starts when the player presses <space> and pauses/resumes when the key 'P' is pressed.

A mouseListener object is defined to also start the game when the user clicks, if the game is not running. A game is not running at the very beginning or when the game over text is displayed.

01. mouseListener = new Object();
02. mouseListener.onMouseDown = function() {
03.         if (!gameRunning) { // we want to be able to start the game by clicking
04.                 startGame();
05.         }
06. };
07. Mouse.addListener(mouseListener);
The function below is the initialization of the game. This is called once to start a new game:
01. function startGame() {
02.         x = int(gameWidth/2); // x start position in the middle
03.         y = gameHeight-2;     // y start position near the bottom
04. 
05.         xVelocity = [-1, 0, 1, 0]; // x velocity when moving left, up, right, down
06.         yVelocity = [0, -1, 0, 1]; // y velocity when moving left, up, right, down
07. 
08.         map = new Array(); // create an array to store food and snake
09.         for (var n=0;n<gameWidth;n++) { // make map a 2 dimensional array
10.                 map[n] = new Array();
11.         }
12. 
13.         turnQueue = new Array(); // a queue to store key presses (so that x number of key presses during one frame are spread over x number of frames)
14. 
15.         game.createEmptyMovieClip("food", 1); // create MC to store the food
16.         game.createEmptyMovieClip("s", 2); // create MC to store the snake
17.         scoreTextField.text = "Score: 0"; // type out score info
18. 
19.         foodCounter = 0; // keeps track of the number of food movie clips
20.         snakeBlockCounter = 0; // keeps track of the snake blocks, increased on every frame
21.         currentDirection = 1; // holds the direction of movement (0 = left, 1 = up, 2 = right, 3 = down)
22.         snakeEraseCounter = -1; // increased on every frame, erases the snake tail (setting this to -3 will result in a 3 block long snake at the beginning)
23.         score = 0; // keeps track of the score
24. 
25.         placeFood("new"); // place a new food block
26. 
27.         textMC.gotoAndStop("hide"); // make sure no text is visible (like "game over ")
28.         game.onEnterFrame = main; // start the main loop
29.         gameRunning = true; // flag telling if the game is running. If true it does not necessarily mean that main is called (the game could be paused)
30. }
We define the start position (line 2, 3) of the snake, and then define how the snake moves depending on the direction (line 5, 6). The numbers -1, 0, 1, 0 in the array xVelocity are the steps to move the snake horizontally when it is moving left, up, right and down respectively. The yVelocity defines the steps vertically. You can change -1 and 1 to -2 and 2 in xVelocity and yVelocity to create an interesting result. The snake is moving two steps each frame, which results in the snake looking like a dotted line.

We create an array called map where each element contains another array (line 8-11). This is known as a two dimensional array. We also define/reset some other variables like score and set the moving direction to 1 (up).

On line 25 we create a new food movie clip and place it on a random spot. You can call this more than once if you want to have several food blocks visible at the same time.

On line 28 in startGame() we start the loop. The function main will be called once every frame while the game lasts. To halt a game (pause) we stop calling main by removing the onEnterFrame event.

The next block of code is the core of the game:

01. function main() { // called on every frame if the game is running and it's not paused
02.         if (turnQueue.length > 0) { // if we have a turn to perform...
03.                 var dir = turnQueue.pop(); // ...pick the next turn in the queue...
04.                 if (dir % 2 != currentDirection % 2) { // not a 180 degree turn (annoying to be able to turn into the snake with one key press)
05.                         currentDirection = dir; // change current direction to the new value
06.                 }
07.         }
08. 
09.         x += xVelocity[currentDirection]; // move the snake position in x
10.         y += yVelocity[currentDirection]; // move the snake position in y
11. 
12.         if (map[x][y] != SNAKE_BLOCK && x > -1 && x < gameWidth && y > -1 && y < gameHeight) { // make sure we are not hitting the snake or leaving the game area
13.                 game.s.attachMovie("snakeMC", snakeBlockCounter, snakeBlockCounter, {_x: x*blockSize, _y: y*blockSize}); // attach a snake block movie clip
14.                 snakeBlockCounter++; // increase the snake counter
15. 
16.                 if (typeof(map[x][y]) == "movieclip") { // if it's a not a vacant block then there is a food block on the position
17.                         score += 10; // add points to score
18.                         scoreTextField.text = "Score: " + score; // type out score info
19.                         snakeEraseCounter -= 5; // make the snake not remove the tail for five loops
20.                         placeFood(map[x][y]); // place the food movie clip which is referenced in the map map[x][y]
21.                 }
22. 
23.                 map[x][y] = SNAKE_BLOCK; // set current position to occupied
24. 
25.                 var tailMC = game.s[snakeEraseCounter]; // get "last" MC according to snakeEraseCounter (may not exist)
26.                 if (tailMC) { // if the snake block exists
27.                         delete map[tailMC._x/blockSize][tailMC._y/blockSize]; // delete the value in the array m
28.                         tailMC.removeMovieClip(); // delete the MC
29.                 }
30.                 snakeEraseCounter++; // increase erase snake counter
31.         } else { // GAME OVER if it is on a snake block or outside of the map
32.                 gameOver();
33.         }
34. }
The code above is probably the most complex part. The first thing we do each frame is to check if there are any turns saved in the queue (line 2). If there is, we pick the last one (line 3). But we only change direction if the turn is not a 180 degree turn. That is, we don't want to be able to move left and then if right is pressed move to the right directly and thus collide with the snake tail. So, if the current direction is left (0) we want to be able to turn up (1) or down (3). If we are moving up (1) we want to be able to turn left (0) or right (2). As you can see, the rule is that if the current direction and the new direction are both even or both odd numbers then that is a 180 degree turn.

The % operator calculates the remainder of a number divided by another number. So, an even number % 2 will return 0 and an odd number % 2 will return 1. If dir % 2 is different than currentDirection % 2 then one of them is even and the other is odd, and that turn is allowed.

Then we move the snake to a new position (line 9-10). To get to the new position we add a number which is -1, 0 or 1. Which to add depends on the snake direction.

Once we have moved to a new position, we make sure that the new position does not contain a snake block and that it's not outside the game area (line 12). If this test fails we call gameOver(). If it's true, we attach a new snake block to the "s" movie clip in the "game" movie clip (line 13-14). We move the attached movie clip to the right position directly by passing an object with the right _x and _y values to the attachMovie method.

A variation of the rules is to make the snake move to the opposite side when it is about to leave the game area. We can achieve this by adding the following code between line 10 and 12:

if (x < 0) { // Move through walls
        x += gameWidth;
} else if (x >= gameWidth) {
        x -= gameWidth;
}
if (y < 0) {
        y += gameHeight;
} else if (y >= gameHeight) {
        y -= gameHeight;
}

If we use this code then we only need to check for snake blocks in the if-statement on line 12 since the only way to lose the game is to hit the snake.

After we have attached the new snake block we check to see if there is a food block reference saved in the map array at the new position (line16). If there is, we add 10 to the score (line 17) and print the score in the text field instance called scoreTextField (line 18). We then decrease the snakeEraseCounter by 5, which will have the result that the snake tail is not erased the following 5 frames, which will make is grow 5 blocks in length. The last thing we do if a food block is eaten is to move it to a new position (line 20).

You could add these 3 lines after line 20 to spawn a new food block say every 300 points:

if (score % 300 == 0) {
        placeFood("new"); // place a new food block
}

After we have checked for food on the new position, we must mark the new position as now occupied by the snake (line 23).

The last part is to remove the tail with one block each frame (unless it's supposed to grow because of eaten food). We pick the "last" tail movie clip (line 25). It's not always the last, because lets say the last snake block has number 50, and a food has just been eaten. Then snakeEraseCounter will be perhaps be 50 - 5 = 45 and no block is removed. But if the block did exist (line 26) we remove it from the map (line 27) and then finally remove the movie clip itself (line 28). The snakeEraseCounter is always increased by 1 (line 30).

Since we want to add a snake block to the front of the snake and remove the one at the back, why attach one to the front and then remove the one at the back instead of just moving the tail block to the front? Sure, you could do that, but when I did the under 1Kb snake game I came to the conclusion that this way required the least amount of coding, and if you move the tail block to the front you still have to attach movie clips to make the snake grow. With this solution you get the growing of the snake automatically, by just offsetting the snakeEraseCounter. This solution also solves the problem when a food block is eaten while the snake is growing. Let's say we eat a food block and the snake is supposed to grow 5 in length for every food block. After 2 frames, when the snake has grown 2 blocks out of 5, we eat another food block. That should result in the snake growing not the next 5 frames but rather the next 8 frames.

The gameOver() function below just displays the "game over" text and stops the main loop:

01. function gameOver() {
02.         textMC.gotoAndStop("gameOver"); // show "game over" text
03.         delete game.onEnterFrame; // quit looping main function
04.         gameRunning = false; // the game is no longer running
05. }
gameOver() is only called in main() when we hit the snake or leave the game area.

The last function places a new food block on a random location if "new" is passed to it, or moves an existing food block if a movie clip reference is passed to it:

01. function placeFood(foodMC) {
02.         do {
03.                 var xFood = random(gameWidth);
04.                 var yFood = random(gameHeight);
05.         } while (map[xFood][yFood]); // keep picking a spot until it's a vacant spot (we don't want to place the food on a position occupied by the snake)
06. 
07.         if (foodMC == "new") { // create a new food movie clip
08.                 foodMC = game.food.attachMovie("foodMC", foodCounter, foodCounter);
09.                 foodCounter++;
10.         }
11. 
12.         foodMC._x = xFood*blockSize; // place the food
13.         foodMC._y = yFood*blockSize; // place the food
14. 
15.         map[xFood][yFood] = foodMC; // save a reference to this food movie clip in the map
16. }
The first thing to do is to find a free spot to place the food on. This is done using a do...while loop (line 2-5). We simply pick a random x coordinate and a random y coordinate within the game area and then continue to do this as long as the picked spot contains a food block or the snake itself. This way we will end up with a coordinate which is not occupied where we can place the food.

If "new" was passed as an argument to the function, we attach a new food movie clip in the movie clip called "food".

Then we move the food movie clip (either the one we created or the one which reference was passed) to the random location which was picked. Then we save a reference to this food movie clip in the map array.

Adding walls to the game area

You may want to add walls to the game area. In that case, you can use this function:

function placeWall(x, y, type) {
        wallMC = game.wall.attachMovie(type, wallCounter, wallCounter);
        wallCounter++;
        
        wallMC._x = x*blockSize;
        wallMC._y = y*blockSize;
        
        map[x][y] = 1;
}

Just paste the code above into the .fla. The function arguments are: a block column, a block row and a block type. The type is a string, and it will attach the movie clip from the library with the linkage name identical to the type.

You also need to create a new movie clip container for the walls and a couple of wall blocks. In the startGame function, add these lines:

game.createEmptyMovieClip("wall", 3); // create MC to store the walls
wallCounter = 0;
placeWall(10, 10, "wallMC");
placeWall(11, 10, "wallMC");
placeWall(12, 10, "wallMC");

Here I've just added three wall blocks. Make sure you have a movie clip in the library with a linkage id "wallMC" (right click on the symbol and select properties). Or you can just use "snakeMC".

Adding a different graphic for the snake head

There's one very simple addition you can make to have the snake's "head" look different. In the snakeMC, add a new frame, so that it consists of 2 frames. On frame 1 you place the graphics you want to use for the head. On frame 2, you place the graphics used for the rest of the snake. Also, place a stop() action on frame 2, otherwise the whole snake will blink.

That's it. As you probably have noticed, the example game at the top of this page has a highscore table. I decided not to include it in this tutorial for the reason that it requires a server side script written in asp, php or similar. There are a lot of free guestbook/highscore (basically the same thing when coding) scripts available elsewhere and information on how to use them with Flash.

T h e  E n d.