In this tutorial I would like to show you how easy it is to create a classic "Snake" game in Flash. I will try to explain everything easily, step by step, so that you can develop the game further to your needs! The Game will be developed in AS3 and I will use the FlashDevelop IDE.
Introduction
The game won't be complex. Whenever we hit a wall, it will restart the game. After eating an apple the snake will grow, and a 'new' Apple will appear. (Actually, it will be the same apple, but I'll explain this later.)
One of the most important aspects of the game is the code's reaction to KEY_DOWN events. The snake will only then change its direction after a tick has passed, not immediately after a keypress. This means that, if the snake is going right, and you press down and left very fast, the snake will go down, not down AND left. Without this 'feature' the snake would allow us to go left while we are going right, which would mean it hit itself.
Let's Look at the Game Already!
Let's take a look at the final result we will be working towards:
Step 1: Creating the Project
In FlashDevelop, create a new Project, and inside the 'src' folder create a 'com' folder. In the 'com' folder create a new class, and call it 'Element.as'.
Set the dimensions of the project to 600x600px.
Step 2: Wait... What's an Element?
The snake is make up of blue squares, which I call elements. We will create an Element Class, which draws the element. The red apple is going to be an element too, so we will extend the code with a few more lines.
Therefore we won't create a new class for the apple. (But if you really want to, you can.)
Step 3: Writing the Element Class
The Element class creates a square. It doesn't draw it on the stage, it just creates it. The registration point of the element - the position referred to by its x- and y-coordinates - is in the top-left.
After opening the Element.as you will see something like this:
package com { /** * ... * @author Fuszenecker Zsombor */ public class Element { public function Element() { } } }
First we need this to extend the Shape class, so we can use the graphics
object to draw the square. After this, create two variables: one for the direction (if it's part of the snake), and one for the score value (if it's an apple), and then change the parameters of the constructor function:
package com { import flash.display.Shape; public class Element extends Shape { protected var _direction:String; //IF IT IS AN APPLE -> protected var _catchValue:Number; //color,alpha,width,height public function Element(_c:uint,_a:Number,_w:Number,_h:Number) { } } }
Now fill the function with some code:
package com { import flash.display.Shape; public class Element extends Shape { protected var _direction:String; //IF IT IS AN APPLE -> protected var _catchValue:Number; //color,alpha,width,height public function Element(_c:uint,_a:Number,_w:Number,_h:Number) { graphics.lineStyle(0, _c, _a); graphics.beginFill(_c, _a); graphics.drawRect(0, 0, _w, _h); graphics.endFill(); _catchValue = 0; } } }
Now, whenever we create an element, it will draw a rectangle and set the score value of the element to 0 by default. (It won't put the rectangle on stage, it just draws it within itself. Notice that we have not called the addChild()
function.)
Let's finish this class and then we can finally test how much we have done already:
package com { import flash.display.Shape; public class Element extends Shape { protected var _direction:String; //IF IT IS AN APPLE -> protected var _catchValue:Number; //color,alpha,width,height public function Element(_c:uint,_a:Number,_w:Number,_h:Number) { graphics.lineStyle(0, _c, _a); graphics.beginFill(_c, _a); graphics.drawRect(0, 0, _w, _h); graphics.endFill(); _catchValue = 0; } //ONLY USED IN CASE OF A PART OF THE SNAKE public function set direction(value:String):void { _direction = value; } public function get direction():String { return _direction; } //ONLY USED IN CASE OF AN APPLE public function set catchValue(value:Number):void { _catchValue = value; } public function get catchValue():Number { return _catchValue; } } }
We created four functions to change the directions and the value of the apple. We achieved this by using setters and getters. More about Setters/Getters in this article!
Step 4: Testing the Element Class
Open Main.as now.
Import the com.Element
class and create an Element in the init()
function:
package { import flash.display.Sprite; import flash.events.Event; import com.Element; public class Main extends Sprite { public function Main() { if(stage) addEventListener(Event.ADDED_TO_STAGE, init); else init(); } private function init(e:Event = null):void { var testElement:Element = new Element(0x00AAFF, 1, 10, 10); testElement.x = 50; testElement.y = 50; this.addChild(testElement); } } }
First we create the testElement
variable which holds our element. We create a new Element and assign that to our testElement
variable. Note the arguments we passed: first we give it a color, then the alpha, width and height. If you look in the Element class's Element function, you can see how it uses this data to draw the rectangle.
After creating the Element, we position it and put it on the stage!
Step 5: Setting Up the Variables
Look at the following code. I wrote the functions of the variables next to them (notice that we imported the necessary classes too):
package { import flash.display.Sprite; import flash.text.TextField; import flash.utils.Timer; import flash.events.TimerEvent; import flash.ui.Keyboard; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.Event; import com.Element; public class Main extends Sprite { //DO NOT GIVE THESE VARS A VALUE HERE! //Give them their values in the init() function. private var snake_vector:Vector.<Element>; //the snake's parts are held in here private var markers_vector:Vector.<Object>; //the markers are held in here private var timer:Timer; private var dead:Boolean; private var min_elements:int; //holds how many parts the snake should have at the beginning private var apple:Element; //Our apple private var space_value:Number; //space between the snake's parts private var last_button_down:uint; //the keyCode of the last button pressed private var flag:Boolean; //is it allowed to change direction? private var score:Number; private var score_tf:TextField; //the Textfield showing the score public function Main() { if(stage) addEventListener(Event.ADDED_TO_STAGE, init); else init(); } private function init(e:Event = null):void { snake_vector = new Vector.<Element>; markers_vector = new Vector.<Object>; space_value = 2; //There will be 2px space between every Element timer = new Timer(50); //Every 50th millisecond, the moveIt() function will be fired! This will set the SPEED of the snake dead = false; min_elements = 10; //We will begin with 10 elements. apple = new Element(0xFF0000, 1, 10, 10); //red, not transparent, width:10, height: 10; apple.catchValue = 0; //pretty obvious - the score of the apple last_button_down = Keyboard.RIGHT; //The first direction of the snake is set in this variable score = 0; score_tf = new TextField(); //this is the TextField which shows our score. this.addChild(score_tf); } } }
The most important variable is the snake_vector
. We will put every Element of the snake in this Vector.
Then there is the markers_vector
. We will use markers to set the direction of the snake's parts. Each object in this Vector will have a position and a type. The type will tell us whether the snake should go right, left, up, or down after 'hitting' the object. (They won't collide, only the position of the markers and the snake's parts will be checked.)
As an example, if we press DOWN, an object will be created. The x and y of this object will be the snake's head's x and y coordinates, and the type will be "Down". Whenever the position of one of the snake's Elements is the same as this object's, the snakes elements direction will be set to "Down".
Please read the comments next to the variables to understand what the other variables do!
Step 6: Writing the attachElement() Function
The attachElement()
function will take four parameters: the new snake element, the x and y coordinates, and the direction of the last part of the snake.
private function attachElement(who:Element,lastXPos:Number = 0,lastYPos:Number = 0,dirOfLast:String = "R"):void { }
Before we put the element on the stage we should position it. But for this we need the direction of the snake's last element, to know whether the new element has to be above, under, or next to this.
After checking the direction and setting the position, we can add it to the stage.
private function attachElement(who:Element,lastXPos:Number = 0,lastYPos:Number = 0,dirOfLast:String = "R"):void { if (dirOfLast == "R") { who.x = lastXPos - snake_vector[0].width - space_value; who.y = lastYPos; } else if(dirOfLast == "L") { who.x = lastXPos + snake_vector[0].width + space_value; who.y = lastYPos; } else if(dirOfLast == "U") { who.x = lastXPos; who.y = lastYPos + snake_vector[0].height + space_value; } else if(dirOfLast == "D") { who.x = lastXPos; who.y = lastYPos - snake_vector[0].height - space_value; } this.addChild(who); }
Now we can use this function in the init()
function:
for(var i:int=0;i<min_elements;++i) { snake_vector[i] = new Element(0x00AAFF,1,10,10); snake_vector[i].direction = "R"; //The starting direction of the snake if (i == 0)//first snake element { //you have to place the first element on a GRID. (now: 0,0) //[possible x positions: (snake_vector[0].width+space_value)*<UINT> ] attachElement(snake_vector[i],0,0,snake_vector[i].direction) snake_vector[0].alpha = 0.7; } else { attachElement(snake_vector[i], snake_vector[i - 1].x, snake_vector[i - 1].y, snake_vector[i - 1].direction); } }
We create the first 10 Elements, and set the direction of them to 'R' (right). If it is the first element, we call attachElement()
and we change its alpha a bit (so the "head" is a slightly lighter color).
If you wish to set the position somewhere else, then please keep the following in mind: the snake has to be placed on a grid, otherwise it would look bad and would not work. If you wish to change the x and y position you can do it the following way:
Setting the x position: (snake_vector[0].width+space_value)*[UINT]
, where you should replace [UINT] with a positive integer.
Setting the y position: (snake_vector[0].height+space_value)*[UINT]
, where you should replace [UINT] with a positive integer.
Let's change it to this:
if (i == 0)//first snake element { //you have to place the first element on a GRID. (now: 0,0) //[possible x positions: (snake_vector[0].width+space_value)*<UINT>] attachElement( snake_vector[i], (snake_vector[0].width+space_value)*20, (snake_vector[0].height+space_value)*10, snake_vector[i].direction ); snake_vector[0].alpha = 0.7; }
And the snake's first element is set onto the 20th space in the x-grid and 10th space in the y-grid.
This is what we've got so far:
package { import flash.display.Sprite; import flash.text.TextField; import flash.utils.Timer; import flash.events.TimerEvent; import flash.ui.Keyboard; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.Event; import com.Element; public class Main extends Sprite { //DO NOT GIVE THEM A VALUE HERE! Give them a value in the init() function private var snake_vector:Vector.<Element>; //the snake's parts are held in here private var markers_vector:Vector.<Object>; //the markers are held in here private var timer:Timer; private var dead:Boolean; private var min_elements:int; //holds how many parts should the snake have at the beginning private var apple:Element; //Our apple private var space_value:Number; //space between the snake parts private var last_button_down:uint; //the keyCode of the last button pressed private var flag:Boolean; //is it allowed to change direction? private var score:Number; private var score_tf:TextField; //the Textfield showing the score public function Main() { if(stage) addEventListener(Event.ADDED_TO_STAGE, init); else init(); } private function init(e:Event = null):void { snake_vector = new Vector.<Element>; markers_vector = new Vector.<Object>; space_value = 2; timer = new Timer(50); //Every 50th millisecond, the moveIt() function will be fired! dead = false; min_elements = 10; //We will begin with 10 elements. apple = new Element(0xFF0000, 1,10, 10); //red, not transparent, width:10, height: 10; apple.catchValue = 0; //pretty obvious last_button_down = Keyboard.RIGHT; //The first direction of the snake is set in this variable score = 0; score_tf = new TextField(); //this is the TextField which shows our score. this.addChild(score_tf); for(var i:int=0;i<min_elements;++i) { snake_vector[i] = new Element(0x00AAFF,1,10,10); snake_vector[i].direction = "R"; //The starting direction of the snake if (i == 0)//first snake element { //you have to place the first element on a GRID. (now: 0,0) [possible x positions: (snake_vector[0].width+space_value)*<UINT> ] attachElement(snake_vector[i], (snake_vector[0].width + space_value) * 20, (snake_vector[0].height + space_value) * 10, snake_vector[i].direction); snake_vector[0].alpha = 0.7; } else { attachElement(snake_vector[i], snake_vector[i - 1].x, snake_vector[i - 1].y, snake_vector[i - 1].direction); } } } private function attachElement(who:Element,lastXPos:Number = 0,lastYPos:Number = 0,dirOfLast:String = "R"):void { if (dirOfLast == "R") { who.x = lastXPos - snake_vector[0].width - space_value; who.y = lastYPos; } else if(dirOfLast == "L") { who.x = lastXPos + snake_vector[0].width + space_value; who.y = lastYPos; } else if(dirOfLast == "U") { who.x = lastXPos; who.y = lastYPos + snake_vector[0].height + space_value; } else if(dirOfLast == "D") { who.x = lastXPos; who.y = lastYPos - snake_vector[0].height - space_value; } this.addChild(who); } } }
Step 7: Writing the placeApple()
Function
This function does the following:
- It checks whether the apple was caught. For this we will use the
caught
parameter, and set its default value totrue
, in case we don't pass any value as parameters in the future. If it was caught, it adds 10 to the apple's score value (so the next apple is worth more). - After this the apple has to be repositioned (we don't create new apples) at a random grid position.
- If it is placed on the snake, we should place it somewhere else.
- If it is not on the stage yet, we place it there.
private function placeApple(caught:Boolean = true):void { if (caught) apple.catchValue += 10; var boundsX:int = (Math.floor(stage.stageWidth / (snake_vector[0].width + space_value)))-1; var randomX:Number = Math.floor(Math.random()*boundsX); var boundsY:int = (Math.floor(stage.stageHeight/(snake_vector[0].height + space_value)))-1; var randomY:Number = Math.floor(Math.random()*boundsY); apple.x = randomX * (apple.width + space_value); apple.y = randomY * (apple.height + space_value); for(var i:uint=0;i<snake_vector.length-1;i++) { if(snake_vector[i].x == apple.x && snake_vector[i].y == apple.y) placeApple(false); } if (!apple.stage) this.addChild(apple); }
There will be some math here, but if you think it through you should understand why it is so. Just draw it out on some paper if necessary.
-
boundsX
will hold how many elements could be drawn in one row. -
randomX
takes thisboundsX
, multiplies it with a Number between zero and one, and floors it. IfboundsX
is 12 and the random Number is 0.356, thenfloor(12*0.356)
is 4, so the apple will be placed on the 4th spot on the x-grid. -
boundsY
will hold how many elements can be drawn in one column. -
randomY
takes thisboundsY
, multiplies it with a Number between zero and one, and floors it. - Then we set the x and y position to these numbers.
In the for loop, we check whether the apple's new x and y positions are identical to any of the snake_vectors
elements. If so, we call the placeApple()
function again (recursive function), and set the parameter of it to false
. (Meaning that the apple was not caught, we just need to reposition it)
(apple.stage)
returns true if the apple is on the stage. we use the '!' operator to invert that value, so if it is NOT on the stage, we place it there.
The last thing we need to do is call the placeApple()
function at the end of the init()
function.
private function init(e:Event = null):void { /* . . . */ placeApple(false); }
Notice that we pass false
as the parameter. It's logical, because we didn't catch the apple in the init()
function yet. We will only catch it in the moveIt()
function.
Now there are only three more functions to write: the directionChanged()
, moveIt()
and the gameOver()
functions.
Step 8: Starting the moveIt()
Function
The moveIt()
function is responsible for all of the movement. This function will check the boundaries and check whether there is an object at the x and y position of the snake's head. It will also look for the apple at this position.
For all of this, we will use our timer variable.
Add two more lines in the end of the init()
function:
timer.addEventListener(TimerEvent.TIMER,moveIt); timer.start();
Look at the comments in the sourcecode, to see which block of code does what.
private function moveIt(e:TimerEvent):void { if (snake_vector[0].x == apple.x && snake_vector[0].y == apple.y) { //This code runs if the snakes heads position and the apples position are the same } if (snake_vector[0].x > stage.stageWidth-snake_vector[0].width || snake_vector[0].x < 0 || snake_vector[0].y > stage.stageHeight-snake_vector[0].height || snake_vector[0].y < 0) { //This block runs if the snakes head is out of the stage (hitting the walls) } for (var i:int = 0; i < snake_vector.length; i++) { /* START OF FOR BLOCK This whole 'for' block will run as many times, as many elements the snake has. If there are four snake parts, this whole for cycle will run four times. */ if (snake_vector[i] != snake_vector[0] && (snake_vector[0].x == snake_vector[i].x && snake_vector[0].y == snake_vector[i].y)) { //If the snakes heads position is the same as any of the snake parts, this block will run (Checking the collision with itself). } if (markers_vector.length > 0) { //if there are direction markers, this code runs } var DIRECTION:String = snake_vector[i].direction; //getting the direction of the current snake element. switch (DIRECTION) { //Sets the new position of the snakes part } /* END OF FOR BLOCK */ } }
Now we need to code the movement. For this we jump into the switch block, which will run on every snake part, because of the for loop.
First we need to check the direction of the current element.
switch (DIRECTION) { case "R" : //Here we need to set the new x position for the current part break; case "L" : //Here we need to set the new x position for the current part break; case "D" : //Here we need to set the new y position for the current part break; case "U" : //Here we need to set the new y position for the current part break; }
When the direction of the part is set to "R", for instance, we need to add something to its current X position (the space_value
plus the width of the snake part).
With this in mind, we can fill it out:
switch (DIRECTION) { case "R" : snake_vector[i].x += snake_vector[i].width + space_value; break; case "L" : snake_vector[i].x -= snake_vector[i].width + space_value; break; case "D" : snake_vector[i].y += snake_vector[i].height + space_value; break; case "U" : snake_vector[i].y -= snake_vector[i].width + space_value; break; }
After testing the code, you should see that the snake is moving, and going off the stage and never stops. (You may need to refresh the page - or just click here to load it in a new window.)
So we need to stop the snake
Step 9: Writing the gameOver()
Function
This function is going to be the shortest. We just clear the stage and restart it:
private function gameOver():void { dead = true; timer.stop(); while (this.numChildren) this.removeChildAt(0); timer.removeEventListener(TimerEvent.TIMER,moveIt); init(); }
That's it. We set the dead
variable to true, stop the movement with the timer, remove every child of the class and call the init()
function, like we just started the game.
Now, let's get back to the moveIt()
function.
Step 10: Continuing the moveIt()
Function
We will use the gameOver()
function in two places. The first is when we check if the head is out of bounds, and the second is when the snake hits itself:
private function moveIt(e:TimerEvent):void { if (snake_vector[0].x == apple.x && snake_vector[0].y == apple.y) { //This code runs if the snakes heads position and the apples position are the same } if (snake_vector[0].x > stage.stageWidth-snake_vector[0].width || snake_vector[0].x < 0 || snake_vector[0].y > stage.stageHeight-snake_vector[0].height || snake_vector[0].y < 0) { gameOver(); } for (var i:int = 0; i < snake_vector.length; i++) { /* START OF FOR BLOCK This whole 'for' block will run as many times, as many elements the snake has. If there are four snake parts, this whole for cycle will run four times. */ if (snake_vector[i] != snake_vector[0] && (snake_vector[0].x == snake_vector[i].x && snake_vector[0].y == snake_vector[i].y)) { //If the snakes heads position is the same as any of the snake parts, this block will run gameOver(); } if (markers_vector.length > 0) { //if there are direction markers, this code runs } var DIRECTION:String = snake_vector[i].direction; //getting the direction of the current snake element. switch (DIRECTION) { case "R" : snake_vector[i].x += snake_vector[i].width + space_value; break; case "L" : snake_vector[i].x -= snake_vector[i].width + space_value; break; case "D" : snake_vector[i].y += snake_vector[i].height + space_value; break; case "U" : snake_vector[i].y -= snake_vector[i].width + space_value; break; } /* END OF FOR BLOCK */ } }
This is the code we have now:
package { import flash.display.Sprite; import flash.text.TextField; import flash.utils.Timer; import flash.events.TimerEvent; import flash.ui.Keyboard; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.Event; import com.Element; public class Main extends Sprite { //DO NOT GIVE THEM A VALUE HERE! Give them a value in the init() function private var snake_vector:Vector.<Element>; //the snake's parts are held in here private var markers_vector:Vector.<Object>; //the markers are held in here private var timer:Timer; private var dead:Boolean; private var min_elements:int; //holds how many parts should the snake have at the beginning private var apple:Element; //Our apple private var space_value:Number; //space between the snake parts private var last_button_down:uint; //the keyCode of the last button pressed private var flag:Boolean; //is it allowed to change direction? private var score:Number; private var score_tf:TextField; //the Textfield showing the score public function Main() { if(stage) addEventListener(Event.ADDED_TO_STAGE, init); else init(); } private function init(e:Event = null):void { snake_vector = new Vector.<Element>; markers_vector = new Vector.<Object>; space_value = 2; timer = new Timer(50); //Every 50th millisecond, the moveIt() function will be fired! dead = false; min_elements = 10; //We will begin with 10 elements. apple = new Element(0xFF0000, 1,10, 10); //red, not transparent, width:10, height: 10; apple.catchValue = 0; //pretty obvious last_button_down = Keyboard.RIGHT; //The first direction of the snake is set in this variable score = 0; score_tf = new TextField(); //this is the TextField which shows our score. this.addChild(score_tf); for(var i:int=0;i<min_elements;++i) { snake_vector[i] = new Element(0x00AAFF,1,10,10); snake_vector[i].direction = "R"; //The starting direction of the snake if (i == 0)//first snake element { //you have to place the first element on a GRID. (now: 0,0) [possible x positions: (snake_vector[0].width+space_value)*<UINT> ] attachElement(snake_vector[i], (snake_vector[0].width + space_value) * 20, (snake_vector[0].height + space_value) * 10, snake_vector[i].direction); snake_vector[0].alpha = 0.7; } else { attachElement(snake_vector[i], snake_vector[i - 1].x, snake_vector[i - 1].y, snake_vector[i - 1].direction); } } placeApple(false); timer.addEventListener(TimerEvent.TIMER, moveIt); timer.start(); } private function attachElement(who:Element,lastXPos:Number = 0,lastYPos:Number = 0,dirOfLast:String = "R"):void { if (dirOfLast == "R") { who.x = lastXPos - snake_vector[0].width - space_value; who.y = lastYPos; } else if(dirOfLast == "L") { who.x = lastXPos + snake_vector[0].width + space_value; who.y = lastYPos; } else if(dirOfLast == "U") { who.x = lastXPos; who.y = lastYPos + snake_vector[0].height + space_value; } else if(dirOfLast == "D") { who.x = lastXPos; who.y = lastYPos - snake_vector[0].height - space_value; } this.addChild(who); } private function placeApple(caught:Boolean = true):void { if (caught) apple.catchValue += 10; var boundsX:int = (Math.floor(stage.stageWidth / (snake_vector[0].width + space_value)))-1; var randomX:Number = Math.floor(Math.random()*boundsX); var boundsY:int = (Math.floor(stage.stageHeight/(snake_vector[0].height + space_value)))-1; var randomY:Number = Math.floor(Math.random()*boundsY); apple.x = randomX * (apple.width + space_value); apple.y = randomY * (apple.height + space_value); for(var i:uint=0;i<snake_vector.length-1;i++) { if(snake_vector[i].x == apple.x && snake_vector[i].y == apple.y) placeApple(false); } if (!apple.stage) this.addChild(apple); } private function moveIt(e:TimerEvent):void { if (snake_vector[0].x == apple.x && snake_vector[0].y == apple.y) { //This code runs if the snakes heads position and the apples position are the same } if (snake_vector[0].x > stage.stageWidth-snake_vector[0].width || snake_vector[0].x < 0 || snake_vector[0].y > stage.stageHeight-snake_vector[0].height || snake_vector[0].y < 0) { gameOver(); } for (var i:int = 0; i < snake_vector.length; i++) { /* START OF FOR BLOCK This whole 'for' block will run as many times, as many elements the snake has. If there are four snake parts, this whole for cycle will run four times. */ if (snake_vector[i] != snake_vector[0] && (snake_vector[0].x == snake_vector[i].x && snake_vector[0].y == snake_vector[i].y)) { //If the snakes heads position is the same as any of the snake parts, this block will run gameOver(); } if (markers_vector.length > 0) { //if there are direction markers, this code runs } var DIRECTION:String = snake_vector[i].direction; //getting the direction of the current snake element. switch (DIRECTION) { case "R" : snake_vector[i].x += snake_vector[i].width + space_value; break; case "L" : snake_vector[i].x -= snake_vector[i].width + space_value; break; case "D" : snake_vector[i].y += snake_vector[i].height + space_value; break; case "U" : snake_vector[i].y -= snake_vector[i].width + space_value; break; } /* END OF FOR BLOCK */ } } private function gameOver():void { dead = true; timer.stop(); while (this.numChildren) this.removeChildAt(0); timer.removeEventListener(TimerEvent.TIMER,moveIt); //stage.removeEventListener(KeyboardEvent.KEY_DOWN,directionChanged); init(); } } }
Step 11: The directionChanged()
Function
We want to listen to the keyboard, so we can actually control the snake. For this we need to put some code into the init()
function and the gameOver()
function.
Put this at the end of the init()
function (setting up the listener function):
stage.addEventListener(KeyboardEvent.KEY_DOWN,directionChanged);
And this at the end of the gameOver()
function:
stage.removeEventListener(KeyboardEvent.KEY_DOWN,directionChanged);
Now create a new function:
private function directionChanged(e:KeyboardEvent):void { var m:Object = new Object(); //MARKER OBJECT //this will be added to the markers_vector, and have the properties x,y, and type //the type property will show us the direction. if it is set to right, whenever a snake's part hits it, //the direction of that snake's part will be set to right also if (e.keyCode == Keyboard.LEFT && last_button_down != e.keyCode && last_button_down != Keyboard.RIGHT) { //If we pressed the LEFT arrow, //and it was not the last key we pressed, //and the last key pressed was not the RIGHT arrow either... //Then this block of code will run } markers_vector.push(m); //we push the object into a vector, so we can acces to it later (in the moveIt() function) }
What goes into the if block?
- The direction of the head should be rewritten.
- The marker object has to be set correctly.
- The last_button variable should be set to the last button pressed.
if (e.keyCode == Keyboard.LEFT && last_button_down != e.keyCode && last_button_down != Keyboard.RIGHT && flag) { snake_vector[0].direction = "L"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"L"}; last_button_down = Keyboard.LEFT; }
Repeat this three more times, and we will have this:
private function directionChanged(e:KeyboardEvent):void { var m:Object = new Object(); //MARKER OBJECT if (e.keyCode == Keyboard.LEFT && last_button_down != e.keyCode && last_button_down != Keyboard.RIGHT) { snake_vector[0].direction = "L"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"L"}; last_button_down = Keyboard.LEFT; } else if (e.keyCode == Keyboard.RIGHT && last_button_down != e.keyCode && last_button_down != Keyboard.LEFT) { snake_vector[0].direction = "R"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"R"}; last_button_down = Keyboard.RIGHT; } else if (e.keyCode == Keyboard.UP && last_button_down != e.keyCode && last_button_down != Keyboard.DOWN) { snake_vector[0].direction = "U"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"U"}; last_button_down = Keyboard.UP; } else if (e.keyCode == Keyboard.DOWN && last_button_down != e.keyCode && last_button_down != Keyboard.UP) { snake_vector[0].direction = "D"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"D"}; last_button_down = Keyboard.DOWN; } markers_vector.push(m); }
We need one more thing to test it. In the moveIt() function we have something like this:
if (markers_vector.length > 0) { //if there are direction markers, this code runs }
Here we need another for loop, to check every snake's part against every marker on the stage, and check whether they collide. If they do, we need to set the snake's part's direction to the marker's type. If it's the last snake part which collides with the marker, we need to remove the marker from the markers_vector
, too, so the snake parts don't collide with it any more.
if (markers_vector.length > 0) { for(var j:uint=0;j < markers_vector.length;j++) { if(snake_vector[i].x == markers_vector[j].x && snake_vector[i].y == markers_vector[j].y) { //setting the direction snake_vector[i].direction = markers_vector[j].type; if(i == snake_vector.length-1) { //if its the last snake_part markers_vector.splice(j, 1); } } } }
Now if you play with it it looks okay, but there is a bug in there. Remember what i said at the beginning of the tutorial?
For instance, if the snake is going to the right and you press the down-left combo very fast, it will hit itself and restart the game.
How do we correct this? Well it's easy. We have our flag
variable, and we will use that for this. We will only be able to change the directions of the snake when this is set to true (Default is false, check the init()
function for that).
So we need to change the directionChanged()
function a little. The if blocks' heads should be changed: add a && flag
clause at the end of every 'if'.
private function directionChanged(e:KeyboardEvent):void { var m:Object = new Object(); //MARKER OBJECT if (e.keyCode == Keyboard.LEFT && last_button_down != e.keyCode && last_button_down != Keyboard.RIGHT && flag) { snake_vector[0].direction = "L"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"L"}; last_button_down = Keyboard.LEFT; flag = false; } else if (e.keyCode == Keyboard.RIGHT && last_button_down != e.keyCode && last_button_down != Keyboard.LEFT && flag) { snake_vector[0].direction = "R"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"R"}; last_button_down = Keyboard.RIGHT; flag = false; } else if (e.keyCode == Keyboard.UP && last_button_down != e.keyCode && last_button_down != Keyboard.DOWN && flag) { snake_vector[0].direction = "U"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"U"}; last_button_down = Keyboard.UP; flag = false; } else if (e.keyCode == Keyboard.DOWN && last_button_down != e.keyCode && last_button_down != Keyboard.UP && flag) { snake_vector[0].direction = "D"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"D"}; last_button_down = Keyboard.DOWN; flag = false; } markers_vector.push(m); }
If you test it now, it won't work because the flag is always false.
When do we need to set it to true then?
After a move/tick we can allow the users to change directions, we just don't want to change it twice in one tick. So put this at the very end of the moveIt()
function:
flag = true;
Now test it, and there is no bug any more.
Step 12: Finishing the Game
Now the only thing we need to do is the 'apple-check'
Remember this at the very beginning of the moveIt()
function?
if (snake_vector[0].x == apple.x && snake_vector[0].y == apple.y) { //This code runs if the snake's head's position and the apple's position are the same }
This is what we need to do in there:
- Call the placeApple() function. (We don't set the parameter to false; we leave it as it is. The default is true.)
- Show the current score
- Attach a new element to the snake's last part.
if (snake_vector[0].x == apple.x && snake_vector[0].y == apple.y) { //calling the placeApple() function placeApple(); //show the current Score score += apple.catchValue; score_tf.text = "Score:" + String(score); //Attach a new snake Element snake_vector.push(new Element(0x00AAFF,1,10,10)); snake_vector[snake_vector.length-1].direction = snake_vector[snake_vector.length-2].direction; //lastOneRichtung //attachElement(who,lastXPos,lastYPos,lastDirection) attachElement(snake_vector[snake_vector.length-1], (snake_vector[snake_vector.length-2].x), snake_vector[snake_vector.length-2].y, snake_vector[snake_vector.length-2].direction); }
Now everything should work fine. Try it out:
Here is the whole Main class again:
package { import flash.display.Sprite; import flash.text.TextField; import flash.utils.Timer; import flash.events.TimerEvent; import flash.ui.Keyboard; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.Event; import com.Element; public class Main extends Sprite { //DO NOT GIVE THEM A VALUE HERE! Give them a value in the init() function private var snake_vector:Vector.<Element>; //the snake's parts are held in here private var markers_vector:Vector.<Object>; //the markers are held in here private var timer:Timer; private var dead:Boolean; private var min_elements:int; //holds how many parts should the snake have at the beginning private var apple:Element; //Our apple private var space_value:Number; //space between the snake parts private var last_button_down:uint; //the keyCode of the last button pressed private var flag:Boolean; //is it allowed to change direction? private var score:Number; private var score_tf:TextField; //the Textfield showing the score public function Main() { if(stage) addEventListener(Event.ADDED_TO_STAGE, init); else init(); } private function init(e:Event = null):void { snake_vector = new Vector.<Element>; markers_vector = new Vector.<Object>; space_value = 2; timer = new Timer(50); //Every 50th millisecond, the moveIt() function will be fired! dead = false; min_elements = 1; apple = new Element(0xFF0000, 1,10, 10); //red, not transparent, width:10, height: 10; apple.catchValue = 0; //pretty obvious last_button_down = Keyboard.RIGHT; //The starting direction of the snake (only change it if you change the 'for cycle' too.) score = 0; score_tf = new TextField(); this.addChild(score_tf); //Create the first <min_elements> Snake parts for(var i:int=0;i<min_elements;++i) { snake_vector[i] = new Element(0x00AAFF,1,10,10); snake_vector[i].direction = "R"; //The starting direction of the snake if (i == 0) { //you have to place the first element on a GRID. (now: 0,0) [possible x positions: (snake_vector[0].width+space_value)*<UINT> ] attachElement(snake_vector[i],0,0,snake_vector[i].direction) snake_vector[0].alpha = 0.7; } else { attachElement(snake_vector[i], snake_vector[i - 1].x, snake_vector[i - 1].y, snake_vector[i - 1].direction); } } placeApple(false); timer.addEventListener(TimerEvent.TIMER,moveIt); stage.addEventListener(KeyboardEvent.KEY_DOWN,directionChanged); timer.start(); } private function attachElement(who:Element,lastXPos:Number = 0,lastYPos:Number = 0,dirOfLast:String = "R"):void { if (dirOfLast == "R") { who.x = lastXPos - snake_vector[0].width - space_value; who.y = lastYPos; } else if(dirOfLast == "L") { who.x = lastXPos + snake_vector[0].width + space_value; who.y = lastYPos; } else if(dirOfLast == "U") { who.x = lastXPos; who.y = lastYPos + snake_vector[0].height + space_value; } else if(dirOfLast == "D") { who.x = lastXPos; who.y = lastYPos - snake_vector[0].height - space_value; } this.addChild(who); } private function placeApple(caught:Boolean = true):void { if (caught) apple.catchValue += 10; var boundsX:int = (Math.floor(stage.stageWidth / (snake_vector[0].width + space_value)))-1; var randomX:Number = Math.floor(Math.random()*boundsX); var boundsY:int = (Math.floor(stage.stageHeight/(snake_vector[0].height + space_value)))-1; var randomY:Number = Math.floor(Math.random()*boundsY); apple.x = randomX * (apple.width + space_value); apple.y = randomY * (apple.height + space_value); for(var i:uint=0;i<snake_vector.length-1;i++) { if(snake_vector[i].x == apple.x && snake_vector[i].y == apple.y) placeApple(false); } if (!apple.stage) this.addChild(apple); } private function moveIt(e:TimerEvent):void { if (snake_vector[0].x == apple.x && snake_vector[0].y == apple.y) { placeApple(); //show the current Score score += apple.catchValue; score_tf.text = "Score:" + String(score); //Attach a new snake Element snake_vector.push(new Element(0x00AAFF,1,10,10)); snake_vector[snake_vector.length-1].direction = snake_vector[snake_vector.length-2].direction; //lastOneRichtung attachElement(snake_vector[snake_vector.length-1], (snake_vector[snake_vector.length-2].x), snake_vector[snake_vector.length-2].y, snake_vector[snake_vector.length-2].direction); } if (snake_vector[0].x > stage.stageWidth-snake_vector[0].width || snake_vector[0].x < 0 || snake_vector[0].y > stage.stageHeight-snake_vector[0].height || snake_vector[0].y < 0) { gameOver(); } for (var i:int = 0; i < snake_vector.length; i++) { if (markers_vector.length > 0) { for(var j:uint=0;j < markers_vector.length;j++) { if(snake_vector[i].x == markers_vector[j].x && snake_vector[i].y == markers_vector[j].y) { snake_vector[i].direction = markers_vector[j].type; if(i == snake_vector.length-1) { markers_vector.splice(j, 1); } } } } if (snake_vector[i] != snake_vector[0] && (snake_vector[0].x == snake_vector[i].x && snake_vector[0].y == snake_vector[i].y)) { gameOver(); } //Move the boy var DIRECTION:String = snake_vector[i].direction; switch (DIRECTION) { case "R" : snake_vector[i].x += snake_vector[i].width + space_value; break; case "L" : snake_vector[i].x -= snake_vector[i].width + space_value; break; case "D" : snake_vector[i].y += snake_vector[i].height + space_value; break; case "U" : snake_vector[i].y -= snake_vector[i].width + space_value; break; } } flag = true; } private function gameOver():void { dead = true; timer.stop(); while (this.numChildren) this.removeChildAt(0); timer.removeEventListener(TimerEvent.TIMER,moveIt); stage.removeEventListener(KeyboardEvent.KEY_DOWN,directionChanged); init(); } private function directionChanged(e:KeyboardEvent):void { var m:Object = new Object(); //MARKER OBJECT if (e.keyCode == Keyboard.LEFT && last_button_down != e.keyCode && last_button_down != Keyboard.RIGHT && flag) { snake_vector[0].direction = "L"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"L"}; last_button_down = Keyboard.LEFT; flag = false; } else if (e.keyCode == Keyboard.RIGHT && last_button_down != e.keyCode && last_button_down != Keyboard.LEFT && flag) { snake_vector[0].direction = "R"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"R"}; last_button_down = Keyboard.RIGHT; flag = false; } else if (e.keyCode == Keyboard.UP && last_button_down != e.keyCode && last_button_down != Keyboard.DOWN && flag) { snake_vector[0].direction = "U"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"U"}; last_button_down = Keyboard.UP; flag = false; } else if (e.keyCode == Keyboard.DOWN && last_button_down != e.keyCode && last_button_down != Keyboard.UP && flag) { snake_vector[0].direction = "D"; m = {x:snake_vector[0].x, y:snake_vector[0].y, type:"D"}; last_button_down = Keyboard.DOWN; flag = false; } markers_vector.push(m); } } }
Step 13: Summing It All Up
Congratulations! You have just created a nice game. Now you can develop it further, and create a super apple or something. For that I recommend using another function called placeSuperApple()
and a new class named SuperApple
. Whenever you catch a super apple, the snakes parts could lengthen by three elements, perhaps. This could be set with setters/getters in the SuperApple
class.
If you wish to do this, and you get stuck somewhere, just leave me a comment here.
Thank you for your time!
Comments