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.
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.
Dowload the file which will give
you the needed movie clips for the game creation. Right click the link and
select Save Target As.
***Your game will need
to contain layers for the code, score text(scoreTextField),
the text movie clip(textMC), the game container(gameMC), and a background.
Let's
jump into the implementation of the game. 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.
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).
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.
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).
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.
T h e E n d.