Build a Classic Snake Game in AS3
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
caughtparameter, 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.
boundsXwill hold how many elements could be drawn in one row.randomXtakes thisboundsX, multiplies it with a Number between zero and one, and floors it. IfboundsXis 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.boundsYwill hold how many elements can be drawn in one column.randomYtakes 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!
View full post on Activetuts+

With the advent of tools like Adobe Edge and libraries like EaselJS, more resources are becoming available for developers looking to create interactive HTML5 content. Many of these tools are being geared specifically for Flash developers to make the transition from ActionScript to HTML5 canvas a smooth one. This article will overview oCanvas, an HTML5 library that developers might not only find invaluable but also very easy to use.
HTML5 Canvas Background
Before we dive into exploring oCanvas, let’s quickly set the scene for how HTML5 canvas works. If you want a more thorough explanation on how to use HTML5 canvas, check out this tutorial.
If you know ActionScript, you already know a lot of JavaScript, which is where the real power resides when working with canvas. We use the HTML5 drawing API to create our content along with some good ol’ JavaScript to make things interactive and dynamic. But when we combine the two, the approach behind how we go about putting together our code is a bit different than what we are used to with ActionScript.
In short, to use the native Canvas API, we draw pixels onto the drawing context of the canvas. But the key thing to remember is that we are working with the entire canvas, not just a single shape or image that we have drawn. Every time we want to alter something we have drawn we have to redraw the entire canvas. If we want to animate something we have to redraw the canvas over and over in our JavaScript to make it appear that things are moving.
This notion is very similar to traditional animation, where animators had to draw each and every pose in their sequence and have the camera move through them very quickly to simulate movement. But if you’re used to tree-like structures such as the DOM, or the display list in Actionscript, this notion can be hard to get your head around. This rinse and repeat approach to programming is much different than working with objects for most developers.
Introducing oCanvas
Luckily for those of us who are so accustomed to working with objects, oCanvas brings that familiar approach to HTML5 canvas. oCanvas is a JavaScript library developed by Johannes Koggdal with the intention of making it easier to develop with HTML5 canvas. It allows you to work directly with objects, modify their properties and hook up events to them all while handling the nitty-gritty behind the scenes stuff for you. As put best by Johannes on his blog:
Get the Library
To start using oCanvas we need to include a copy of the library on our HTML page. We can either reference the CDN-hosted file, or host a local copy ourselves. Jump over to the oCanvas website and you can either download a copy of the library or grab the reference to the CDN-hosted version. The current version is 2.0 and was released only a few weeks ago, which addressed many of the bugs that were in the initial release. On the site there is a minified production version, which is good to use when you’re ready to deploy your project. There is also a development version, which is uncompressed but is better for debugging. I like to directly link to the hosted version for faster loading and caching by the browser.
Initial Code Setup
After you make a reference to oCanvas, next we need to setup a canvas element in the body of our HTML and create a reference to it for use in our Javascript.
As always, if you place your script above the canvas element, you need to wrap it in a function so you know the DOM is ready. There’s a couple ways to go here. You can either create your own function and then call it in your body element when it loads, like this:
function Main() { // your oCanvas code } <body onload="Main();">Or you can wrap your code within oCanvas’s built-in
domReady()method. This is the equivalent to jQuery’s$(document).ready(). In oCanvas we use this:oCanvas.domReady(function () { //Your Code Here });Note: You could use jQuery’s
$(document).ready()method if you wanted to.Initialize an Instance of oCanvas
This piece of code is absolutely necessary and is the first thing you must write when using oCanvas.
var canvas = oCanvas.create({ canvas: "#canvas", background: "#0cc", fps: 60 });In this code we store a reference to the canvas element within our document and get access to the core instance, which will enable you to start creating objects. The
create()method takes an object as its argument which controls how oCanvas will work. There are numerous properties to pass into thecreate()method, but the only mandatory one is the canvas property: a CSS selector that must point to a canvas element within the DOM.The other properties passed in the above code are the background and fps properties. The background property allows you to apply a background to the canvas, which can be CSS color values, gradients and images. If it’s omitted, the canvas will be transparent. The fps property sets the number of frames per second any animation will run at. The default is 30 fps.
Note: While we overview many of the features in oCanvas, I recommend checking out the library’s documentation to get a better understanding of each section.
Display Objects
There are numerous types of display objects you can create with oCanvas. You can create shapes such as rectangles, ellipses, polygons and lines along with images, text and even sprite sheets. To create a new display object we use oCanvas’s Display Module, and specify what kind of display object we want to create as well as some basic properties – like so:
var box = canvas.display.rectangle({ x: 50, y: 150, width: 50, height: 50, fill: "#000" });Then to add it to the display we call a familiar method for you Flash developers…
Good Ol’ addChild()
Yes an oldie but a goodie, which makes adding objects to oCanvas a familiar process. So to add our box to the canvas, we would write:
Just like in ActionScript,
addChild()adds the specified object as a child of the caller. And in turn the child’s x and y will be relative to its parent. So in this case we are making box a child of the canvas, which we could simplify like this:The
add()method also adds the object to the canvas — which is really the same thing as canvas.addChild(box). ButaddChild()is most useful for adding an object as a child to an already created display object, like:var square = canvas.display.rectangle({ x: 0, y: 0, width: 10, height: 10, fill: "#990000"}); box.addChild(square);Let’s take a look at some of the different types of display objects you can create in oCanvas.
Shapes
You already saw a square, but we can use the
rectangledisplay object to create a lot of things. Here’s a rectangle with a blue stroke:var rectangle = canvas.display.rectangle({ x: 500, y: 100, width: 100, height: 200, fill: "#000", stroke:"outside 2px blue" });The
fillproperty can take any valid CSS color, along with CSS gradients and even image patterns.To create an ellipse we would write:
var ellipse = canvas.display.ellipse({ x: 100, y: 100, radius_x: 20, radius_y: 30, fill: "rgba(255, 0, 0, 0.5)" });If you want a full circle, just replace the
radius_xandradius_yproperties with a singleradiusproperty.Creating any kind of regular polygon is just as easy — all you have to do is specify the number of sides and the radius you want your shape to have. To create a triangle:
var triangle = canvas.display.polygon({ x: 320, y: 145, sides: 3, radius: 50, fill: "#406618" });How about a pentagon?
var pentagon = canvas.display.polygon({ x: 200, y: 50, sides: 5, rotation: 270, radius: 40, fill: "#790000" });To accomplish that with the HTML5 canvas API, you would have to draw a bunch of paths and try figure out what x and y positions to join them at. I attempted to draw an octagon for comparison’s sake but as you can see below I gave up quite easily. Not quite sure what this is supposed to be.
var canvas = $("#canvas"); var ctx = canvas.get(0).getContext("2d"); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(100,50); ctx.lineTo(50, 100); ctx.lineTo(0, 90); ctx.closePath(); ctx.fill();Images
Creating display objects with images doesn’t get any simpler than in oCanvas. Just specify an x and y position and the path to the image file:
var tree = canvas.display.image({ x: 100, y: 350, image: "tree.png" });A nice feature of the image display object is the
tileproperty, which lets you easily create a grid of the same image instead of drawing it over and over.Text
oCanvas contains a text display object and handles font styling just like CSS does.
var text = canvas.display.text({ x: 70, y: 300, align: "center", font: "bold 18px sans-serif", text: "oCanvas Rocks", fill: "purple" });You can use many of the other text properties you’re familiar with from CSS. Check out the documentation on text for more.
Properties and Methods
All display objects inherit a common group of properties and methods. Some of the most common display object properties are:
x, y, width, height, rotation, scalingX, scalingY, opacity, shadow(uses CSS-box-shadow syntax) andzIndex. You can check out this link for a full list of the base properties and methods. Let’s take a look at some other noteworthy ones.Origin
This method is a big time-saver because it let’s you easily set the origin inside the object. In other words, it allows you to set the registration point of the object. If you ever tried to perform a rotation from the center with the HTML5 Canvas API, you know how big of a headache it can be. You have to do a slew of actions of saving the drawing state, translating the canvas, performing your rotation and then restoring the drawing state. With the
originproperty you can easily define an object’s origin:var obj = canvas.display.image({ x: 270, y: 270, origin: { x: "center", y: "center" } });This would draw the image from its center; if we were to rotate the object, it would rotate from its center, too. Besides “center” you could also pass in “left” or “right” for the x and “top” or “bottom” for the y positions. In addition to using the predefined keywords, you could also supply positive or negative numbers as values of where to draw the object. The default origin for all display objects is defined at its top left.
You can also use the
setOrigin()method at any time to define an object’s origin:obj.setOrigin("left", "bottom")ID
A display object’s id, which is really a read-only property, corresponds to where the object exists in the draw list — which you can think of as the display list. I find it to be very useful because it can serve as a unique identifier in certain situations when you might be seeking out a specific object in your code. Consider a basic snippet like this:
getId(box.id) function getId(id) { if (id==9) console.log("CORRECT ! " + id) else console.log("WRONG! " + id) }Composition
The composition property is the equivalent of
globalCompositeOperationwithin the native Canvas API. If you’re not familiar with it, it basically determines how pixels are rendered when drawn onto already existing pixels on the canvas. I encourage you to read up on the different compositing operations you can set, but with oCanvas you can simply set the operation you want by passing it as a string:var shape = canvas.display.rectangle({ x: 270, y: 270, width: 180, height: 80, fill: "#ff6900", composition:"destination-atop" });There are many different operations you can pass in but I think one of the neat things you can do with the composition property is create masks between different display objects. Check out the file named
masks.htmlin the download package. If you ever relied on creating layer masks in your Flash applications, you’ll enjoy this one.Methods of Note
Since we mentioned rotating objects earlier, you can quickly rotate an object with the
rotate()androtateTo()methods:You can also simply set the rotation property:
There’s also the
move()andmoveTo()methods which, like their names suggest, allows you to move an object by a specified amount of pixels for the former and toa specified x and y position for the latter.The same idea works for the
scale()andscaleTo()methods():We mentioned
addChild()before; let’s not forget aboutremoveChild()andremoveChildAt(). And like theadd()method, we can do the opposite withremove().Another really useful method is
clone(), which allows you to duplicate a display object and all of its properties.var box = canvas.display.rectangle({ x: 50, y: 150, width: 50, height: 50, fill: "#000"}); var box2 = box.clone(x:200)Events
A big plus to oCanvas is that you can add events to specific objects. The oCanvas contains many methods and properties for easily handling mouse, keyboard and even touch events all with one simple method.
Bind()
If you’re familiar with jQuery, you probably already know where I’m going with this.
canvas.bind("click tap", function () { canvas.background.set("#efefef"); });All this does is change the background color of the canvas, but notice how we pass in “click tap” — easily allowing us to add support for both mouse and touch devices.
Besides click events, you can also listen for other mouse events:
mousedown, mouseup, mousemove, mouseenter, mouseleaveanddblclick.A simple rollover effect might look like this:
box.bind("mouseenter", function () { canvas.background.set("#333"); }).bind("mouseleave", function () { canvas.background.set("#000"); });This is an example of chaining functions — which (not to sound like a broken record) is another jQuery feature leveraged in oCanvas.
But instead of altering the canvas when a mouse event occurs, what about altering an actual display object? This is still HTML5 Canvas after all, so we must remember to call an important method to tell the canvas to update itself.
canvas.redraw()
The
redraw()method (which is actually part of the Draw Module, not the Events Module) redraws the canvas with all the display objects that have been added. So if want to perform an action on a particular object and have the rest of the draw list stay intact, we must add this one simple line of code to our functions:square.bind("click tap", function () { square.x+=50; canvas.redraw(); });Unbind()
What good is an event listener if we can’t remove it?
rectangle.bind("click tap", function onClick () { this.fill="#FF9933"; canvas.redraw(); rectangle.unbind("click tap", onClick) });How About a Quick Drag and Drop?
We don’t need the
bind()method for this one. We just write:That’s probably the quickest and easiest drag and drop code you’ll ever write.
Note on events: When you’re working with events, it’s natural to want to get as much information as possible about the event. Luckily, we can still do so when working with oCanvas. For example if we take the click handler a few lines up and log the event to the console we can see all the properties we have from the event.
rectangle.bind("click tap", function onClick (e) { this.fill="#FF9933"; canvas.redraw(); rectangle.unbind("click tap", onClick); console.log(e); });Keyboard and Touch Events
Besides mouse events, oCanvas has entire modules dedicated to keyboard and touch events with their own unique methods and properties. These events are also handled with the
bind()method. The events system in oCanvas is a very broad topic, so I encourage taking a look at the events section in the documentation and experimenting.Timeline
With the Timeline Module we can set up our main loop for our application. If you were creating a game, this would essentially be your game loop. I like to think of it as the equivalent of an
ENTER_FRAMEin Flash.It’s simple to set up — we just call the
setLoopfunction and chain thestart()method to it:canvas.setLoop(function () { triangle.rotation+=5; }).start();If we wanted to tie the
setLoop()function to an event – say to a mouse click – we could do something like this:canvas.setLoop(function () { triangle.rotation+=5; }) button.bind("click tap", function () { canvas.timeline.start() });And we could stop the timeline by simply calling:
Animation
Using
setLoop()is the way to go for animations that will occur over a long period of time and to handle constant updates you have to make throughout your application. But oCanvas has built in methods to handle more simpler and basic animations that are commonly needed. These methods are also practically taken verbatim from jQuery.Animate()
The
animate()method works just like it does in jQuery. If you’re not familiar with this side of jQuery, think if it like a tweening engine like TweenMax or Tweener for Flash. You can animate any property that can be set by a numeric value:circle.animate( { y: circle.y - 300, scalingX:.5, scalingY: .5 }, "short", "ease-in", function () { circle.fill = "#45931e"; canvas.redraw(); } );Here we animate the circle’s y position and overall size, apply some easing, and when it is finished we run a callback function that changes its fill color. But don’t forget to call
redraw().fadeIn(),fadeOut(), andfadeTo()To fade an object in and out we could simply call:
To fade the opacity to a specific value, we’d use
fadeTo():You can also define the duration, easing and provide a callback function for these methods in the same way you would with the
animate()method.Scenes
oCanvas contains a very useful Scenes Module that allows you to easily separate your application into different states. Game developers might appreciate this one because it’s a simple approach to breaking down your game into different sections. Even old school Flash animators might liken the Scenes Module to the Scenes panel, which allows you to literally create different scenes within a Flash project.
To create a scene in oCanvas we call the
create()method to return ascenesobject:var intro = canvas.scenes.create("intro", function () { // add display objects here });Within the
create()method we pass in two arguments: the name of the scene as a string, and a function where we add the display object we want to add to that scene.var introText = canvas.display.text({ x: canvas.width / 2, y: canvas.height/2, align: "center", font: "bold 36px sans-serif", text: "Introduction", fill: "#133035" }); var intro = canvas.scenes.create("intro", function () { this.add(introText); });Now we have to load our scene and those objects will be added to the display:
canvas.scenes.load("intro");Notice we pass in the name we gave the scene when we created it.
And of course we can unload a scene at any time:
canvas.scenes.unload("intro");Imagine how much of a time-saver this could be if you used scenes and event handlers together.
oCanvas vs. EaselJS
The only real downside to oCanvas is that it hasn’t gained as much traction in the development community as you might guess – or at least it seems that way for now. Part of that reason for this, I think, is because of the popularity of EaselJS. There seems to be alot more awareness and resources out there for EaselJS than there are for oCanvas — which is hard to believe since the latter was first released in March 2011, but for some reason it has flown under the radar.
I’ve been using both libraries for quite some time now, and I can honestly say I am big fan of both. EaselJS definitely feels more like you are using ActionScript and if you’re a Flash developer will be easy to pick up. And as we’ve seen, oCanvas could pass for jQuery’s long lost brother in many ways. So if you’re a pure ActionScripter, you might just naturally gravitate towards EaselJS— especially since Easel was written specifically to appeal to Flash developers.
However, I’ve been using Actionscript much longer than jQuery and I personally find oCanvas simpler to use and less verbose to write. And even though EaselJS is pretty easy to use itself, the simple syntax in oCanvas just makes it such a welcome tool.
But besides the simpler syntax, oCanvas and EaselJS in many ways are are pretty much interchangeable. Both libraries can accomplish more or less the same tasks and there’s very little difference in performance, if any. However I do notice the Ticker function in EaselJS runs a little more smoothly than oCanvas’
setLoopfunction (though that could just be a browser-based difference).EaselJS does have much more of an extensive API — especially when it comes to drawing and effects. And if you take into account TweenJS and SoundJS, Easel is definitely a more complete tool — especially if you’re used to using an application like Flash that offers fine-tune control over your projects. But if you’re new to the whole HTML5 game, you’re likely to hit the ground running with oCanvas much faster. When I was first introduced to oCanvas, I found it so much fun to play with. Everything is already there for you — all the necessary methods and events to start creating, manipulating and animating objects right away.
Wrapping Up
Whichever library you prefer, oCanvas and EaselJS are only the beginning of what I think will be an influx of tools and resources to allow developers to easily create browser-based applications. The features of oCanvas detailed in this article barely scratch the surface of what could be created using this very powerful library.
By no means though is oCanvas (or any library for that matter) a reason not to learn and use the native HTML5 Canvas API. But if you find yourself in a situation where all your former Flash clients are now looking for you to create HTML5 apps (like mine were) and you don’t have time to learn something like the unfriendly transformation matrix in the native Canvas API — oCanvas can definitely ease the learning curve.
In this tutorial I’ll show you how to create a menu like Apple’s Dock using AS3 classes. We will create a single AS file that will perform all the magic, extending it to add new features.
March of 2010
Final Result Preview
First, let’s take a look at what we’ll be creating. Roll your mouse over the icons to see how they move and scale.
Step 1: Create a New ActionScript File
Begin by creating a new ActionScript file and saving it as “DockItem.as”. I’m saving mine at c:/macmenu/org/effects/DockItem.as.
Note that our document root (where the .fla lives) will be c:/macmenu; the folder /org/effects will form the package for the DockItem class.
Step 2: Create a New FLA
Create a new ActionScript 3.0 Flash File and open it, so that we have both DockItem.as and this .fla file opened. Save this .fla in the root folder (the DockItem.as is at c:/macmenu/org/effects, so our site root is c:/macmenu) the /org/effects is the package of DockItem Object and we save the .fla as c:/macmenu/macmenu.fla.
Step 3: Import Icons
Now we import or draw some icons to the .fla. I’ve imported some icons I have here from an Illustrator file, but you can of course draw your own and apply a gradient to them.
Step 4: Begin Converting Icons to Symbols
Select any icon and click Modify > Convert To Symbol.
In the box that opens, give it a name (I named this symbol “Star”) and pay attention to the registration point; it needs to be bottom center. For the class use the same name (remember that you can’t use spaces) and for the Base class, use org.effects.DockItem (the class that we’ll create). Also, make sure your Type is set to Movie Clip.
Then, align all the objects to the bottom: select all, click Window > Align, make sure the button "To stage" is unselected (otherwise it will align at the botton of the stage), then click the top-right button in this panel to align all the objects.
Step 5: Convert All Icons to Symbols
We can have as many buttons as we want, so let’s convert all our icons to symbols. Remember to give them a name and a Class, set all their registration points to bottom center and set the Base class to org.effects.DockItem.
See below for how our library and the icons should look; note the space between them, it’s important for creating a good effect.
Step 6: Start Coding the DockItem Class
If we test the movie now it will throw an error saying that an ActionScript file must have at least one external and visible definition; that’s because all our menu items are extending the DockItem class, which we haven’t yet written. Let’s write it now…
Start creating the package by extending the Sprite class (we will extend Sprite since we don’t have a timeline animation.)
package org.effects{ import flash.display.Sprite; public class DockItem extends Sprite{ } }At this point we have our DockItem extending the Sprite class, so if you test it now it will work, but you’ll see no effects.
(Confused? Not used to coding with classes? Check out this Quick Tip on using a document class for an introduction.)
Step 7: Import Necessary Classes
Now we will import all the necessary classes. A custom class is being used here, the TweenLite class, which you can download from GreenSock.com. When you’ve downloaded TweenLite, extract it to your /macmenu/ folder (so you will have a folder /macmenu/com/greensock/).
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; //http://www.greensock.com/tweenlite import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ } }I’ve imported the Sprite class because it’s what we are extending; if you have animations on the timeline, extend the MovieClip class. We will use the Event class when the custom object is added to stage and we’ll use the MouseEvent when checking the distance of each icon from the mouse.
Step 8: Declare Necessary Variables
During this step we’ll declare the necessary variables:
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; } }Note that I used the _initPosition as private: it just sets the initial x-position of the icon. The distance of the mouse will always be measured from this point, because the actual x-position of the item will always be changing.
maxXDistance is the maximum x-distance over which the mouse will affect the icon, maxYDistance is the maximum y-distance over which mouse will affect the icon and maxScale is the maximum scale that will be added to the icon (for example, if you set it to 2, the maximum scale the object can reach is 3.)
I’ve used public variables for the last three so we can change them at runtime.
Step 9: Coding the Constructor Function
The constructor function must have the same name as the class (and therefore the same name as the file), hence DockItem():
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } } }Why do we have some parameters here? This allows us to use different combinations of distances and scales: we can have a short distance with a very big scale or a long distance with a small scale. Also, we can determine the y distance within which the mouse will affect the icon.
As we are extending the Sprite class we can add children or even code a custom class for each icon extending the DockItem class, so if we extend it we can use the super() function to pass the new parameters to the superclass. We can then use the DockItem class anytime and anywhere.
In this step we set the maxXDistance variable, maxYDistance variable and the maxScale variable to the values passed as parameters. Also, we check if the object is on the stage – if not, we add an Event to check when it is. We also add another event listener to detect when the icon is removed from the stage. We’ll add a MOUSE_MOVE event to the stage to get the distance, so it’s important to know whether it’s on the stage.
Step 10: The Init() Function
This is the function that will be run once the icon is created and added to the stage. In the init() function we just add an MouseEvent.MOUSE_MOVE listener to the stage, set the _initPosition variable to the x value of the object, and listen for the mouse leaving the area of stage.
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } private function init(e:Event=null):void{ _initPosition=x; stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.addEventListener(Event.MOUSE_LEAVE,mouseLeave); } } }Step 11: The Mouse Functions
When the mouse moves over the stage, this function (triggered by the MOUSE_MOVE event we added a listener for in the last step) will check the mouse position of the parent object and measure the distance from the object to the mouse parent position.
We use parent.mouseX because that gets us the x-position of the mouse relative to whichever object contains the icon, rather than relative to the registration point of the icon.
We also tween the icons back to their starting positions if the mouse leaves the stage in the mouseLeave() handler.
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } private function init(e:Event=null):void{ _initPosition=x; stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.addEventListener(Event.MOUSE_LEAVE,mouseLeave); } private function mouseMove(e:MouseEvent):void{ var yDistance:Number=Math.abs(parent.mouseY-y); if(yDistance>maxYDistance){ if(_initPosition==x) return; else{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); return; } } //get the difference between the parent mouse x position and the initial position of the object var xDistance:Number=parent.mouseX-_initPosition; //check if the distance of the mouse from the object is more than max distance, it can't be bigger... xDistance = xDistance > maxXDistance ? maxXDistance : xDistance; //check if the distance is lower than the negative of the max distance, it can't be lower... xDistance = xDistance < -maxXDistance ? -maxXDistance : xDistance; //create a variable for the position, assuming that the x position must be the initial position plus the distance of the mouse, but it can't be more than the max distance. var posX=_initPosition-xDistance; //we get the scale proportion here, it goes from 0 to maxScale variable var scale:Number=(maxXDistance-Math.abs(xDistance))/maxXDistance; //the minimum scale is 1, the original size, and the max scale will be maxScale variable + 1 scale=1+(maxScale*scale); //here we use a Tween to set the new position according to the mouse position TweenLite.to(this,.3,{x:posX,scaleX:scale,scaleY:scale}); } private function mouseLeave(e:Event):void{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); } } }First, we check the y distance (vertical distance between the icon and the mouse); if it’s further away than the range we set with the maxYDistanceVariable, then we check whether the icon is back in its original position, and, if not, we tween it there. The return keyword breaks out of the function, so none of the rest of the code will be run in this case.
If the mouse is close to the icon vertically, we use some maths to figure out a new scale and position for the icon based on its horizontal distance from the mouse, then tween it to those values.
Step 12: The End() Function
If we remove the object from the stage, we need to remove the mouseMove and mouseLeave listeners; if not we can get errors every time the mouse is moved. This function is the handler for the REMOVED_FROM_STAGE listener we added earlier, so will be triggered when the object is removed.
package org.effects{ import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class DockItem extends Sprite{ private var _initPosition:Number; public var maxXDistance:Number; public var maxYDistance:Number; public var maxScale:Number; public function DockItem($maxXDistance:Number=60,$maxYDistance:Number=30,$maxScale:Number=2):void{ maxXDistance=$maxXDistance; maxYDistance=$maxYDistance; maxScale=$maxScale; if(stage) init(); else addEventListener(Event.ADDED_TO_STAGE,init); addEventListener(Event.REMOVED_FROM_STAGE,end); } private function init(e:Event=null):void{ _initPosition=x; stage.addEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.addEventListener(Event.MOUSE_LEAVE,mouseLeave); } private function mouseMove(e:MouseEvent):void{ var yDistance:Number=Math.abs(parent.mouseY-y); if(yDistance>maxYDistance){ if(_initPosition==x) return; else{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); return; } } //get the difference between the parent mouse x position and the initial position of the object var xDistance:Number=parent.mouseX-_initPosition; //check if the distance of the mouse from the object is more than max distance, it can't be bigger... xDistance = xDistance > maxXDistance ? maxXDistance : xDistance; //check if the distance is lower than the negative of the max distance, it can't be lower... xDistance = xDistance < -maxXDistance ? -maxXDistance : xDistance; //create a variable for the position, assuming that the x position must be the initial position plus the distance of the mouse, but it can't be more than the max distance. var posX=_initPosition-xDistance; //we get the scale proportion here, it goes from 0 to maxScale variable var scale:Number=(maxXDistance-Math.abs(xDistance))/maxXDistance; //the minimum scale is 1, the original size, and the max scale will be maxScale variable + 1 scale=1+(maxScale*scale); //here we use a Tween to set the new position according to the mouse position TweenLite.to(this,.3,{x:posX,scaleX:scale,scaleY:scale}); } private function mouseLeave(e:Event):void{ TweenLite.to(this,.3,{x:_initPosition,scaleX:1,scaleY:1}); } private function end(e:Event=null):void{ stage.removeEventListener(MouseEvent.MOUSE_MOVE,mouseMove); stage.removeEventListener(Event.MOUSE_LEAVE,mouseLeave); } } }All we do in this function is remove the event listener from the stage.
Step 13: Test It!
At this point we can already test it; it will work since each object is linked with the Base class DockItem. However, we don’t have a bounding box for clicking (if we set our object’s buttonMode property to true, we’ll see that we can click it only when it’s over the actual graphic.)
Step 14: Start Turning Icons Into Buttons
So far we can see the effect working, so now let’s turn each item into a button. We’ll create a new ActionScript file and this one will extend the DockItem – let’s name it DockButton. Its package will be the same as DockItem (org.effects), so we’ll save itb in the same folder as DockItem.as (example: c:/macmenu/org/effects/DockButton.as)
Step 15: Change the Base Class
Now we change the base class of each object in the library. We are currently using org.effects.DockItem as Base class, let’s now use org.effects.DockButton.
If we test it now, there will be an error. This is because DockButton.as is still empty, so let’s code it.
Step 16: Start Coding DockButton.as
OK, now we’ll extend the DockItem class, because we want to use everything that we have in DockItem and add some more tricks (allowing it to act as a button), but we don’t want to add the new features to DockItem directly. This way, if we want to use the DockItem as anything other than a Button later on, we can, but if we want to use it as a Button we can use the DockButton.
package org.effects{ public class DockButton extends DockItem{ } }If we test our project now, it will work, but it will work exactly as the DockItem as we haven’t yet added anything new.
Step 17: Import Classes for DockButton
Let’s import some things we will need to extend the DockItem. As we are extending the DockItem we don’t need to import the classes that are already there, since we wont use them directly in DockButton.
package org.effects{ import flash.geom.Rectangle; public class DockButton extends DockItem{ } }I’ve imported the Rectangle class, but why? It’s because we will use the bounding box of our object to create a fake background, to allow the button to be clickable even if the mouse isn’t precisely over a colored area. Let’s create a background graphic with alpha 0 (transparent), so we will have a square to click.
Step 18: Constructor for DockButton
Since we need to create a bounding box for DockButton, we will get its own bounds, that’s why we imported the flash.geom.Rectangle class
package org.effects{ import flash.geom.Rectangle; public class DockButton extends DockItem{ public function DockButton():void{ buttonMode=true; mouseChildren=false; var bounds:Rectangle=getBounds(this); this.graphics.beginFill(0,0); this.graphics.drawRect(bounds.x,bounds.y,bounds.width,bounds.height); this.graphics.endFill(); } } }What we have done? We created a constructor which first sets the object’s buttonMode to true, so our DockButton will be treated as a Button. Then we set mouseChildren to false, so mouse events will come from the DockButton object, not any other object inside it. Next we get the bounds of the object using getBounds() and draw a transparent rectangle using the graphics object. (The graphics property comes with the Sprite class, and we extended Sprite to make our DockItem object. Now we’ve extended our DockItem to make our DockButton object, DockButton has everything from the Sprite class and the DockItem class.)
Step 19: Check Everything and Test It
OK, let’s perform a check:
If it’s all OK, test the movie…
(At this point, if you want to put the folder org/effects in your classpath you can, so you won’t need to copy this folder to each project you create and use the DockItem or DockButton.)
Step 20: Change the Color on Mouse Over
Why not change the color of the button when the mouse passes over it? In this section I will teach how. For this we will use the TweenLite engine again to give some tint to the object. However, we are already using TweenLite in the DockItem object and we are extending this object at DockButton. We want to extend DockButton to change the color, but we can’t use TweenLite anymore in the same object since the new TweenLite object will overwrite the other one (even with the property overwrite:false in TweenLite it will reduce the performance a lot if we use it directly in the same object). All is not lost; we have an icon inside each object of the library and we can apply the tint to that.
To do this, let’s create another ActionScript File, but now save this one at the same folder as the .fla with the name “OverButton.as” (example: c:/macmenu/OverButton.as.)
Step 21: Coding the OverButton Object
First we create the package and import the necessary classes; since we saved the OverButton.as file in the same folder of the .fla file the package will be top level, so there’s no need to write “package org.effects”:
package{ import org.effects.DockButton; import flash.display.DisplayObject; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class OverButton extends DockButton{ } }OK, so we’re extending DockButton this time and we’ve imported the DisplayObject class because we will treat the icon as a DisplayObject. We’ve also imported MouseEvent which we’ll use to check when the mouse is over the icon and when it’s out. We also have TweenLite to create some tween effects with the color.
Step 22: OverButton Constructor
package{ import org.effects.DockButton; import flash.display.DisplayObject; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class OverButton extends DockButton{ private var _object:DisplayObject; public function OverButton():void{ _object=this.getChildAt(0) as DisplayObject; this.addEventListener(MouseEvent.MOUSE_OVER, mouseOver); this.addEventListener(MouseEvent.MOUSE_OUT, mouseOut); TweenPlugin.activate([TintPlugin]); } } }Why have we created a private var _object as DisplayObject? Our actual icon is stored in this variable (that’s what line 13 does) and is treated as a DisplayObject; we will use the color effect on our icon, not in the whole object.
We add the event listeners of the mouse to check when the mouse is over and when the mouse is out.
Step 23: Coding Mouse Functions
Since we have created the listeners for mouse over and mouse out, we will now create their functions:
package{ import org.effects.DockButton; import flash.display.DisplayObject; import flash.events.MouseEvent; import com.greensock.TweenLite; import com.greensock.plugins.TweenPlugin; import com.greensock.plugins.TintPlugin; public class OverButton extends DockButton{ private var _object:DisplayObject; public function OverButton():void{ _object=this.getChildAt(0) as DisplayObject; this.addEventListener(MouseEvent.MOUSE_OVER, mouseOver); this.addEventListener(MouseEvent.MOUSE_OUT, mouseOut); TweenPlugin.activate([TintPlugin]); } private function mouseOver(e:MouseEvent):void{ new TweenLite(_object,.5,{tint:0x990099}); } private function mouseOut(e:MouseEvent):void{ new TweenLite(_object,.5,{tint:null}); } } }Note that we are using the TweenLite on _object now, not on “this” any more. That’s because the OverButton extends the DockButton which extends the DockItem where there is already a TweenLite being used. Also, in DockButton we have a fake alpha 0 background that doesn’t need to be painted.
For the tint property of TweenLite I used a color code of 0×990099, which is a medium purple; if you use null as the value the tint will be removed softly.
Step 24: Change the Base Classes
At this point if you test the movie, you won’t see any color change, because we need to change the base class of each object in the library again. Open the Library once more in the .fla (Window > Library). Right-click each object and change its base class to OverButton (not org.effects.OverButton, because the class file is not in the /org/effects folder).
OK, now you can test it!
Conclusion
In this tutorial I’ve explained about extending objects. The actual dock effect is pure math – it’s distance calculations, scale settings – but it’s important we see in the code that we cant use the “x” property as position reference, because the “x” property is changed every time. I hope now you all have a better understanding of the “extends” keyword, and can appreciate how the calculations are done here. Thanks for reading
In this tutorial series (part free, part Premium) we’ll create a high-performance 2D shoot-em-up using the new hardware-accelerated
Stage3Drendering engine. We will be taking advantage of several hardcore optimization techniques to achieve great 2D sprite rendering performance. In this part, we’ll build a high-performance demo that draws hundreds of moving sprites on-screen at once.Final Result Preview
Let’s take a look at the final result we will be working towards: a high-performance 2D sprite demo that uses Stage3D with optimizations that include a spritesheet and object pooling.
Introduction: Flash 11 Stage3D
If you’re hoping to take your Flash games to the next level and are looking for loads of eye-candy and amazing framerate, Stage3D is going to be your new best friend.
The incredible speed of the new Flash 11 hardware accelerated Stage3D API is just begging to be used for 2D games. Instead of using old-fashioned Flash sprites on the DisplayList or last-gen blitting techniques as popularized by engines such as FlashPunk and Flixel, the new breed of 2D games uses the power of your video card’s GPU to blaze through rendering tasks at up to 1000x the speed of anything Flash 10 could manage.
Although it has 3D in its name, this new API is also great for 2D games. We can render simple geometry in the form of 2D squares (called quads) and draw them on a flat plane. This will enable us to render tons of sprites on screen at a silky-smooth 60fps.
We’ll make a side-scrolling shooter inspired by retro arcade titles such as R-Type or Gradius in ActionScript using Flash 11′s Stage3D API. It isn’t half as hard as some people say it is, and you won’t need to learn assembly language AGAL opcodes.
In this 6-part tutorial series, we are going to program a simple 2D shoot-’em-up that delivers mind-blowing rendering performance. We are going to build it using pure AS3, compiled in FlashDevelop (read more about it here). FlashDevelop is great because it is 100% freeware – no need to buy any expensive tools to get the best AS3 IDE around.
Step 1: Create a New Project
If you don’t already have it, be sure to download and install FlashDevelop. Once you’re all set up (and you’ve allowed it to install the latest version of the Flex compiler automatically), fire it up and start a new “AS3 Project.”
FlashDevelop will create a blank template project for you. We’re going to fill in the blanks, piece-by-piece, until we have created a decent game.
Step 2: Target Flash 11
Go into the project menu and change a few options:
Step 3: Imports
Now that our blank project is set up, let’s dive in and do some coding. To begin with, we will need to import all the Stage3D functionality required. Add the following to the very top of your
Main.asfile.Step 4: Initialize Stage3D
The next step is to wait for our game to appear on the Flash stage. Doing things this way allows for the future use of a preloader. For simplicity, we will be doing most of our game in a single little class that inherits from the Flash Sprite class as follows.
public class Main extends Sprite { private var _entities : EntityManager; private var _spriteStage : LiteSpriteStage; private var _gui : GameGUI; private var _width : Number = 600; private var _height : Number = 400; public var context3D : Context3D; // constructor function for our game public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } // called once Flash is ready private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); stage.quality = StageQuality.LOW; stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.addEventListener(Event.RESIZE, onResizeEvent); trace("Init Stage3D..."); _gui = new GameGUI("Simple Stage3D Sprite Demo v1"); addChild(_gui); stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); trace("Stage3D requested..."); }After setting some stage-specific properties, we request a Stage3D context. This can take a while (a fraction of a second) as your video card is configured for hardware rendering, so we need to wait for the
onContext3DCreateevent.We also want to detect any errors that may occur, especially since Stage3D content does not run if the HTML embed code that loads your SWF doesn’t include the parameter
"wmode=direct". These errors can also happen if the user is running an old version of Flash or if they don’t have a video card capable of handling pixel shader 2.0.Step 5: Handle Any Events
Add the following functions that detect any events that might be triggered as specified above. In the case of errors due to running old Flash plugins, in future versions of this game we might want to output a message and remind the user to upgrade, but for now this error is simply ignored.
For users with old video cards (or drivers) that don’t support shader model 2.0, the good news is that Flash 11 is smart enough to provide a software renderer. It doesn’t run very fast but at least everyone will be able to play your game. Those with decent gaming rigs will get fantastic framerate like you’ve never seen in a Flash game before.
// this is called when the 3d card has been set up // and is ready for rendering using stage3d private function onContext3DCreate(e:Event):void { trace("Stage3D context created! Init sprite engine..."); context3D = stage.stage3Ds[0].context3D; initSpriteEngine(); } // this can be called when using an old version of Flash // or if the html does not include wmode=direct private function errorHandler(e:ErrorEvent):void { trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); } protected function onResizeEvent(event:Event) : void { trace("resize event..."); // Set correct dimensions if we resize _width = stage.stageWidth; _height = stage.stageHeight; // Resize Stage3D to continue to fit screen var view:Rectangle = new Rectangle(0, 0, _width, _height); if ( _spriteStage != null ) { _spriteStage.position = view; } if(_entities != null) { _entities.setPosition(view); } }The event handling code above detects when Stage3D is ready for hardware rendering and sets the variable
context3Dfor future use. Errors are ignored for now. The resize event simply updates the size of the stage and batch rendering system dimensions.Step 6: Init the Sprite Engine
Once the
context3Dhas been received, we are ready to start the game running. Continuing withMain.as, add the following.private function initSpriteEngine():void { // init a gpu sprite system var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); _spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); _spriteStage.configureBackBuffer(_width,_height); // create a single rendering batch // which will draw all sprites in one pass var view:Rectangle = new Rectangle(0,0,_width,_height) _entities = new EntityManager(stageRect); _entities.createBatch(context3D); _spriteStage.addBatch(_entities._batch); // add the first entity right now _entities.addEntity(); // tell the gui where to grab statistics from _gui.statsTarget = _entities; // start the render loop stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); }This function creates a sprite rendering engine (to be implemented below) on the stage, ready to use the full size of your flash file. We then add the entity manager and batched geometry system (which we will discuss below). We are now able to give a reference to the entity manager to our stats GUI class so that it can display some numbers on screen regarding how many sprites have been created or reused. Lastly, we start listening for the
ENTER_FRAMEevent, which will begin firing at a rate of up to 60 times per second.Step 7: Start the Render Loop
Now that everything has been initialized, we are ready to play! The following function will be executed every single frame. For the purposes of this first tech demo, we are going to add one new sprite on stage each frame. Because we are going to implement an object pool (which you can read more about in this tutorial) instead of inifinitely creating new objects until we run out of RAM, we are going to be able to reuse old entities that have moved off screen.
After spawning another sprite, we clear the stage3D area of the screen (setting it to pure black). Next we update all the entities that are being controlled by our entity manager. This will move them a little more each frame. Once all sprites have been updated, we tell the batched geometry system to gather them all up into one large vertex buffer and bast them on screen in a single draw call, for efficiency. Finally, we tell the context3D to update the screen with our final render.
// this function draws the scene every frame private function onEnterFrame(e:Event):void { try { // keep adding more sprites - FOREVER! // this is a test of the entity manager's // object reuse "pool" _entities.addEntity(); // erase the previous frame context3D.clear(0, 0, 0, 1); // move/animate all entities _entities.update(getTimer()); // draw all entities _spriteStage.render(); // update the screen context3D.present(); } catch (e:Error) { // this can happen if the computer goes to sleep and // then re-awakens, requiring reinitialization of stage3D // (the onContext3DCreate will fire again) } } } // end class } // end packageThat’s it for the inits! As simple as it sounds, we have now created a template project that is ready to blast out an insane number of sprites. We are not going to use any vector art. We aren’t going to put any old-fashioned Flash sprites on the stage apart from the Stage3D window and a couple of GUI overlays. All the work of rendering our in-game graphics is going to be handled by Stage3D, so that we can enjoy improved performance.
Going Deeper: Why Is Stage3D So Fast?
Two reasons:
That said, many Stage3D engines seem to get bogged down by a few hundred sprites. This is because they have been programmed without regard to the overhead that each draw command adds. When Stage3D first came out, some of the first 2D engines would draw each and every sprite individually in one giant (slow and inefficient) loop. Since this article is all about extreme optimization for a next-gen 2D game with fabulous framerate, we are going to implement an extremely efficient rendering system that buffers all geometry into one big batch so we can draw everything in only one or two commands.
How to Be Hardcore: Optimize!
Hardcore gamedevs love optimizations. In order to blast the most sprites on screen with the fewest number of state changes (such as switching textures, selecting a new vertex buffer, or having to update the transform once for each and every sprite on screen), we are going to take advantage of the following three performance optimizations:
These three hardcore gamedev tricks are the key to getting awesome FPS in your game. Let’s implement them now. Before we do, we need to create some of the tiny classes that these techniques will make use of.
Step 8: The Stats Display
If we’re going to be doing tons of optimizations and using Stage3D in an attempt to achieve blazingly fast rendering performance, we need a way to keep track of the statistics. A few little benchmarks can go a long way to prove that what we’re doing is having a positive effect on the framerate. Before we go farther, create a new class called
GameGUI.asand implement a super-simple FPS and stats display as follows.Step 9: The Entity Class
We are about to implement an entity manager class that will be the “object pool” as described above. We first need to create a simplistic class for each individual entity in our game. This class will be used for all in-game objects, from spaceships to bullets.
Create a new file called
Entity.asand add a few getters and setters now. For this first tech demo, this class is merely an empty placeholder without much functionality, but in later tutorials this is where we will be implementing much of the gameplay.Step 10: Make a Spritesheet
An important optimization technique we are going to use is the use of a spritesheet – sometimes referred to as a Texture Atlas. Instead of uploading dozens or hundreds of individual images to video RAM for use during rendering, we are going to make a single image that holds all the sprites in our game. This way, we can use a single texture to draw tons of different kinds of enemies or terrain.
Using a spritesheet is a considered a best practice by veteran gamedevs who need to ensure their games run as fast as possible. The reason it speeds things up so much is much the same as the reason why we are going to use geometry batching: instead of having to tell the video card over and over to use a particular texture to draw a particular sprite, we can simply tell it to always use the same texture for all draw calls.
This cuts down on “state changes” which are extremely costly in terms of time. We no longer need to say “video card, start using texture 24… now draw sprite 14″ and so on. We just say “draw everything using this one texture” in a single pass. This can increase performance by an order of magnitude.
For our example game we will be using a collection of legal-to-use freeware images by the talented DanC, which you can get here. Remember that if you use these images you should credit them in your game as follows: “Art Collection Title” art by Daniel Cook (Lostgarden.com).
Using Photoshop (or GIMP, or whatever image editor you prefer), cut and paste the sprites your game will need into a single PNG file that has a transparent background. Place each sprite on an evenly-spaced grid with a couple pixels of blank space between each. This small buffer is required to avoid any “bleeding” of edge pixels from adjacent sprites that can occur due to bilinear texture filtering that happens on the GPU. If each sprite is touching the next, your in-game sprites may have unwanted edges where they should be completely transparent.
For optimization reasons, GPUs work best with images (called textures) that are square and whose dimensions are equal to a power of two and evenly divisible by eight. Why? Because of the way that the pixel data is accessed, these magic numbers happen to align in VRAM in just the right way to be fastest to access, because the data is often read in chunks.
Therefore, ensure that your spritesheet is either 64×64, 128×128, 256×256, 512×512 or 1024×1024. As you might expect, the smaller the better – not just in terms of performance but because a smaller texture will naturally keep your game’s final SWF smaller.
Here is the spritesheet that we will be using for our example. “Tyrian” art by Daniel Cook (Lostgarden.com).
Right-click to download
Step 11: The Entity Manager
The first optimization technique we’re going to take advantage of to achieve blazing performance is the use of “object pools”. Instead of constantly allocating more ram for objects like bullets or enemies, we’re going to make a reuse pool that recycles unused sprites over and over again.
This technique ensures that RAM use stays very low and GC (garbage collection) hiccups rarely occur. The result is that framerate will be higher and your game will run smoothly no matter how long you play.
Create a new class in your project called
EntityManager.asand implement a simple recycle-on-demand mechanism as follows.Step 12: Set Boundaries
Our entity manager is going to recycle entities when they move off the left edge of the screen. The function below is called during inits or when the resize event is fired. We add a few extra pixels to the edges so that sprites don’t suddenly pop in or out of existence.
public function setPosition(view:Rectangle):void { // allow moving fully offscreen before looping around maxX = view.width + 32; minX = view.x - 32; maxY = view.height; minY = view.y; }Step 13: Set Up the Sprites
The entity manager runs this function once at startup. It creates a new geometry batch using the spritesheet image that was embedded in our code above. It sends the
on the one texture. This spritesheet will be used by the batch geometry renderer.
bitmapDatato the spritesheet class constructor, which will be used to generate a texture that has all the available sprite images on it in a grid. We tell our spritesheet that we’re going to use 64 different sprites (8 byIf we wanted, we could use more than one spritesheet, by initializing additional images and batches as required. In the future, this might be where you create a second batch for all terrain tiles that go underneath your spaceship sprites. You could even implement a third batch which is layered on top of everything for fancy particle effects and eye candy. For now, this simple tech demo only needs a single spritesheet texture and geometry batch.
public function createBatch(context3D:Context3D) : LiteSpriteBatch { var sourceBitmap:Bitmap = new SourceImage(); // create a spritesheet with 8x8 (64) sprites on it _spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, 8, 8); // Create new render batch _batch = new LiteSpriteBatch(context3D, _spriteSheet); return _batch; }Step 14: The Object Pool
This is where the entity manager increases performance. This one optimization (an object reuse pool) will allow us to only create new entities on demand (when there aren’t any inactive ones that can be reused). Note how we reuse any sprites that are currently marked as inactive, unless they are all currently being used, in which case we spawn a new one. This way, our object pool only every holds as many sprites as are even visible at the same time. After the first few seconds that our game has been running, the entity pool will remain constant – rarely will a new entity need to be created once there are enough to handle what’s going on on-screen.
Continue adding to
EntityManager.asas follows:// search the entity pool for unused entities and reuse one // if they are all in use, create a brand new one public function respawn(sprID:uint=0):Entity { var currentEntityCount:int = _entityPool.length; var anEntity:Entity; var i:int = 0; // search for an inactive entity for (i = 0; i < currentEntityCount; i++ ) { anEntity = _entityPool[i]; if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) { //trace('Reusing Entity #' + i); anEntity.active = true; anEntity.sprite.visible = true; numReused++; return anEntity; } } // none were found so we need to make a new one //trace('Need to create a new Entity #' + i); var sprite:LiteSprite; sprite = _batch.createChild(sprID); anEntity = new Entity(sprite); _entityPool.push(anEntity); numCreated++; return anEntity; } // for this test, create random entities that move // from right to left with random speeds and scales public function addEntity():void { var anEntity:Entity; var randomSpriteID:uint = Math.floor(Math.random() * 64); // try to reuse an inactive entity (or create a new one) anEntity = respawn(randomSpriteID); // give it a new position and velocity anEntity.sprite.position.x = maxX; anEntity.sprite.position.y = Math.random() * maxY; anEntity.speedX = (-1 * Math.random() * 10) - 2; anEntity.speedY = (Math.random() * 5) - 2.5; anEntity.sprite.scaleX = 0.5 + Math.random() * 1.5; anEntity.sprite.scaleY = anEntity.sprite.scaleX; anEntity.sprite.rotation = 15 - Math.random() * 30; }The functions above are run whenever a new sprite needs to be added on screen. The entity manager scans the entity pool for one that is currently not in use and returns it when possible. If the list is fully of active entities, a brand new one needs to be created.
Step 15: Simulate!
The final function that is the responsibility of our entity manager is the one that gets called every frame. It is used to do any simulation, AI, collision detection, physics or animation as required. For the current simplistic tech demo, it simply loops through the list of active entities in the pool and updates their positions based on velocity. Each entity is moved according to their current velocity. Just for fun, they are set to spin a little each frame as well.
Any entity that goes past the left side of the screen is “killed” and is marked as inactive and invisible, ready to be reused in the functions above. If an entity touches the other three screen edges, the velocity is reversed so it will “bounce” off that edge. Continue adding to
EntityManager.asas follows:// called every frame: used to update the simulation // this is where you would perform AI, physics, etc. public function update(currentTime:Number) : void { var anEntity:Entity; for(var i:int=0; i<_entityPool.length;i++) { anEntity = _entityPool[i]; if (anEntity.active) { anEntity.sprite.position.x += anEntity.speedX; anEntity.sprite.position.y += anEntity.speedY; anEntity.sprite.rotation += 0.1; if (anEntity.sprite.position.x > maxX) { anEntity.speedX *= -1; anEntity.sprite.position.x = maxX; } else if (anEntity.sprite.position.x < minX) { // if we go past the left edge, become inactive // so the sprite can be respawned anEntity.die(); } if (anEntity.sprite.position.y > maxY) { anEntity.speedY *= -1; anEntity.sprite.position.y = maxY; } else if (anEntity.sprite.position.y < minY) { anEntity.speedY *= -1; anEntity.sprite.position.y = minY; } } } } } // end class } // end packageStep 16: The Sprite Class
The final step to get everything up and running is to implement the four classes that make up our “rendering engine” system. Because the word Sprite is already in use in Flash, the next few classes will use the term
LiteSprite, which is not just a catchy name but implies the lightweight and simplistic nature of this engine.To begin, we will create the simple 2D sprite class that our entity class above refers to. There will be many sprites in our game, each of which is collected into a large batch of polygons and rendered in a single pass.
Create a new file in your project called
LiteSprite.asand implement some getters and setters as follows. We could probably get away with simply using public variables, but in future versions changing some of these values will require running some code first, so this technique will prove invaluable.Each sprite can now keep track of where it is on screen, as well as how big it is, how transparent, and what angle it is facing. The spriteID property is a number used during rendering to look up which UV (texture) coordinate needs to be used as the source rectangle for the pixels of the spritesheet image it uses.
Step 17: The Spritesheet Class
We now need to implement a mechanism to process the spritesheet image that we embedded above and use portions of it on all our rendered geometry. Create a new file in your project called
LiteSpriteSheet.asand begin by importing the functionality required, defining a few class variables and a constructor function.The class constructor above is given a
BitmapDatafor our spritesheet as well as the number of sprites that are on it (in this demo, 64).Step 18: Chop It Up
Because we are using a single texture to store all of the sprite images, we need to divide the image into several parts (one for each sprite on it) when rendering. We do this by assigning different coordinates for each vertex (corner) of each quad mesh used to draw a sprite.
These coordinates are called UVs, and each goes from 0 to 1 and represents where on the texture stage3D should start sampling pixels when rendering. The UV coordinates and pixel rectangles are stored in an array for later using during rendering so that we don't have to calculate them every frame. We also store the size and shape of each sprite (which in this demo are all identical) so that when we rotate a sprite we know its radius (which is used to keep the pivot in the very centre of the sprite).
// generate a list of uv coordinates for a grid of sprites // on the spritesheet texture for later reference by ID number // sprite ID numbers go from left to right then down public function createUVs(numSpritesW:int, numSpritesH:int) : void { trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+ ' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.'); var destRect : Rectangle; for (var y:int = 0; y < numSpritesH; y++) { for (var x:int = 0; x < numSpritesW; x++) { _uvCoords.push( // bl, tl, tr, br x / numSpritesW, (y+1) / numSpritesH, x / numSpritesW, y / numSpritesH, (x+1) / numSpritesW, y / numSpritesH, (x + 1) / numSpritesW, (y + 1) / numSpritesH); destRect = new Rectangle(); destRect.left = 0; destRect.top = 0; destRect.right = _spriteSheet.width / numSpritesW; destRect.bottom = _spriteSheet.height / numSpritesH; _rects.push(destRect); } } } public function removeSprite(spriteId:uint) : void { if ( spriteId < _uvCoords.length ) { _uvCoords = _uvCoords.splice(spriteId * 8, 8); _rects.splice(spriteId, 1); } } public function get numSprites() : uint { return _rects.length; } public function getRect(spriteId:uint) : Rectangle { return _rects[spriteId]; } public function getUVCoords(spriteId:uint) : Vector.<Number> { var startIdx:uint = spriteId * 8; return _uvCoords.slice(startIdx, startIdx + 8); }Step 19: Generate Mipmaps
Now we need to process this image during the init. We are going to upload it for use as a texture by your GPU. As we do so, we are going to create smaller copies that are called "mipmaps". Mip-mapping is used by 3d hardware to further speed up rendering by using smaller versions of the same texture whenever it is seen from far away (scaled down) or, in true 3D games, when it is being viewed at an oblique angle. This avoids any "moiree" effects (flickers) than can happen if mipmapping is not used. Each mipmap is half the width and height as the previous.
Continuing with
LiteSpriteSheet.as, let's implement the routine we need that will generate mipmaps and upload them all to the GPU on your video card.public function uploadTexture(context3D:Context3D) : void { if ( _texture == null ) { _texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false); } _texture.uploadFromBitmapData(_spriteSheet); // generate mipmaps var currentWidth:int = _spriteSheet.width >> 1; var currentHeight:int = _spriteSheet.height >> 1; var level:int = 1; var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0); var transform:Matrix = new Matrix(.5, 0, 0, .5); while ( currentWidth >= 1 || currentHeight >= 1 ) { canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0); canvas.draw(_spriteSheet, transform, null, null, null, true); _texture.uploadFromBitmapData(canvas, level++); transform.scale(0.5, 0.5); currentWidth = currentWidth >> 1; currentHeight = currentHeight >> 1; } } } // end class } // end packageStep 20: Batched Geometry
The final hardcore optimization we are going to implement is a batched geometry rendering system. This "batched geometry" technique is often used in particle systems. We are going to use it for everything. This way, we can tell your GPU to draw everything in one go instead of naively sending hundreds of draw commands (one for each sprite on screen).
In order to minimize the number of draw calls and rendering everything in one go, we will be batching all game sprites into a long list of (x,y) coordinates. Essentially, the geometry batch is treated by your video hardware as a single 3D mesh. Then, once per frame, we will upload the entire buffer to Stage3D in a single function call. Doing things this way is far faster than uploading the individual coordinates of each sprite separately.
Create a new file in your project called
LiteSpriteBatch.asand begin by including all the imports for functionality it will need, the class variables it will use, and the constructor as follows:Step 21: Batch Parent and Children
Continue by implementing getters and setters and functionality for handling the addition of any new sprites to the batch. The parent refers to the sprite stage object used by our game engine, while the children are all the sprites in this one rendering batch. When we add a child sprite, we add more data to the list of verteces (which supplies the locations on screen of that particular sprite) as well as the UV coordinates (the location on the spritesheet texture that this particular sprite is stored at). When a child sprite is added or removed from the batch, we set a boolean variable to tell our batch system that the buffers need to be re-uploaded now that they have changed.
public function get parent() : LiteSpriteStage { return _parent; } public function set parent(parentStage:LiteSpriteStage) : void { _parent = parentStage; } public function get numChildren() : uint { return _children.length; } // Constructs a new child sprite and attaches it to the batch public function createChild(spriteId:uint) : LiteSprite { var sprite : LiteSprite = new LiteSprite(); addChild(sprite, spriteId); return sprite; } public function addChild(sprite:LiteSprite, spriteId:uint) : void { sprite._parent = this; sprite._spriteId = spriteId; // Add to list of children sprite._childId = _children.length; _children.push(sprite); // Add vertex data required to draw child var childVertexFirstIndex:uint = (sprite._childId * 12) / 3; _verteces.push(0, 0, 1, 0, 0,1, 0, 0,1, 0, 0,1); // placeholders _indeces.push(childVertexFirstIndex, childVertexFirstIndex+1, childVertexFirstIndex+2, childVertexFirstIndex, childVertexFirstIndex+2, childVertexFirstIndex+3); var childUVCoords:Vector.<Number> = _sprites.getUVCoords(spriteId); _uvs.push( childUVCoords[0], childUVCoords[1], childUVCoords[2], childUVCoords[3], childUVCoords[4], childUVCoords[5], childUVCoords[6], childUVCoords[7]); _updateVBOs = true; } public function removeChild(child:LiteSprite) : void { var childId:uint = child._childId; if ( (child._parent == this) && childId < _children.length ) { child._parent = null; _children.splice(childId, 1); // Update child id (index into array of children) for remaining children var idx:uint; for ( idx = childId; idx < _children.length; idx++ ) { _children[idx]._childId = idx; } // Realign vertex data with updated list of children var vertexIdx:uint = childId * 12; var indexIdx:uint= childId * 6; _verteces.splice(vertexIdx, 12); _indeces.splice(indexIdx, 6); _uvs.splice(vertexIdx, 8); _updateVBOs = true; } }Step 22: Set Up the Shader
A shader is a set of commands that is uploaded directly to your video card for extremely fast rendering. In Flash 11 Stage3D, you write them in a kind of assembly language called AGAL. This shader needs only be created once, at startup. You don't need to understand assembly language opcodes for this tutorial. Instead, simply implement the creation of a vertex program (which calculates the locations of your sprites on screen) and a fragment program (which calculates the color of each pixel) as follows.
protected function setupShaders() : void { var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); vertexShaderAssembler.assemble( Context3DProgramType.VERTEX, "dp4 op.x, va0, vc0 \n"+ // transform from stream 0 to output clipspace "dp4 op.y, va0, vc1 \n"+ // do the same for the y coordinate "mov op.z, vc2.z \n"+ // we don't need to change the z coordinate "mov op.w, vc3.w \n"+ // unused, but we need to output all data "mov v0, va1.xy \n"+ // copy UV coords from stream 1 to fragment program "mov v0.z, va0.z \n" // copy alpha from stream 0 to fragment program ); var fragmentShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler(); fragmentShaderAssembler.assemble( Context3DProgramType.FRAGMENT, "tex ft0, v0, fs0 <2d,clamp,linear,mipnearest> \n"+ // sample the texture "mul ft0, ft0, v0.zzzz\n" + // multiply by the alpha transparency "mov oc, ft0 \n" // output the final pixel color ); _shader = _context3D.createProgram(); _shader.upload( vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode ); } protected function updateTexture() : void { _sprites.uploadTexture(_context3D); }Step 23: Move the Sprites Around
Just before being rendered, each sprite's vertex coordinates on screen will have most likely changed as the sprite moves around or rotates. The following function calculates where each vertex (corner of the geometry) needs to be. Because each quad (the square that makes up one sprite) has four vertices each, and each vertex needs an x, y and z coordinate, there are twelve values to update. As a little optimization, if the sprite is not visible we simply write zeroes into our vertex buffer to avoid doing unnecessary calculations.
protected function updateChildVertexData(sprite:LiteSprite) : void { var childVertexIdx:uint = sprite._childId * 12; if ( sprite.visible ) { var x:Number = sprite.position.x; var y:Number = sprite.position.y; var rect:Rectangle = sprite.rect; var sinT:Number = Math.sin(sprite.rotation); var cosT:Number = Math.cos(sprite.rotation); var alpha:Number = sprite.alpha; var scaledWidth:Number = rect.width * sprite.scaleX; var scaledHeight:Number = rect.height * sprite.scaleY; var centerX:Number = scaledWidth * 0.5; var centerY:Number = scaledHeight * 0.5; _verteces[childVertexIdx] = x - (cosT * centerX) - (sinT * (scaledHeight - centerY)); _verteces[childVertexIdx+1] = y - (sinT * centerX) + (cosT * (scaledHeight - centerY)); _verteces[childVertexIdx+2] = alpha; _verteces[childVertexIdx+3] = x - (cosT * centerX) + (sinT * centerY); _verteces[childVertexIdx+4] = y - (sinT * centerX) - (cosT * centerY); _verteces[childVertexIdx+5] = alpha; _verteces[childVertexIdx+6] = x + (cosT * (scaledWidth - centerX)) + (sinT * centerY); _verteces[childVertexIdx+7] = y + (sinT * (scaledWidth - centerX)) - (cosT * centerY); _verteces[childVertexIdx+8] = alpha; _verteces[childVertexIdx+9] = x + (cosT * (scaledWidth - centerX)) - (sinT * (scaledHeight - centerY)); _verteces[childVertexIdx+10] = y + (sinT * (scaledWidth - centerX)) + (cosT * (scaledHeight - centerY)); _verteces[childVertexIdx+11] = alpha; } else { for (var i:uint = 0; i < 12; i++ ) { _verteces[childVertexIdx+i] = 0; } } }Step 24: Draw the Geometry
Finally, continue adding to the
LiteSpriteBatch.asclass by implementing the drawing function. This is where we tell stage3D to render all the sprites in a single pass. First, we loop through all known children (the individual sprites) and update the verterx positions based on where they are on screen. We then tell stage3D which shader and texture to use, as well as set the blend factors for rendering.What is a blend factor? It defines whether or not we should use transparency, and how to deal with transparent pixels on our texture. You could change the options in the
setBlendFactorscall to use additive blanding, for example, which looks great for particle effects like explosions, since pixels will increase the brightness on screen as they overlap. In the case of regular sprites, all we want is to draw them at the exact color as stored in our spritesheet texture and to allow transparent regions.The final step in our draw function is to update the UV and index buffers if the batch has changed size, and to always upload the vertex data because our sprites are exected to be constantly moving. We tell stage3D which buffers to use and finally render the entire giant list of geometry as if it were a single 3D mesh, so that it gets drawn using a single, fast,
drawTrianglescall.public function draw() : void { var nChildren:uint = _children.length; if ( nChildren == 0 ) return; // Update vertex data with current position of children for ( var i:uint = 0; i < nChildren; i++ ) { updateChildVertexData(_children[i]); } _context3D.setProgram(_shader); _context3D.setBlendFactors(Context3DBlendFactor.ONE, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA); _context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, _parent.modelViewMatrix, true); _context3D.setTextureAt(0, _sprites._texture); if ( _updateVBOs ) { _vertexBuffer = _context3D.createVertexBuffer(_verteces.length/3, 3); _indexBuffer = _context3D.createIndexBuffer(_indeces.length); _uvBuffer = _context3D.createVertexBuffer(_uvs.length/2, 2); _indexBuffer.uploadFromVector(_indeces, 0, _indeces.length); // indices won't change _uvBuffer.uploadFromVector(_uvs, 0, _uvs.length / 2); // child UVs won't change _updateVBOs = false; } // we want to upload the vertex data every frame _vertexBuffer.uploadFromVector(_verteces, 0, _verteces.length / 3); _context3D.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); _context3D.setVertexBufferAt(1, _uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); _context3D.drawTriangles(_indexBuffer, 0, nChildren * 2); } } // end class } // end packageStep 25: The Sprite Stage Class
The final class required by our fancy (and speedy) hardware-accelerated sprite rendering engine is the sprite stage class. This stage, much like the traditional Flash stage, holds a list of all the batches that are used for your game. In this first demo, our stage will only be using a single batch of sprites, which itself only uses a single spritesheet.
Create one last file in your project called
LiteSpriteStage.asand begin by creating the class as follows:Step 26: The Camera Matrix
In order to know exactly where on screen each sprite needs to go, we will track the location and size of the rendering window. During our game's initializations (or if it changes) we create a model view matrix which is used by Stage3D to transform the internal 3D coordinates of our geometry batches to the proper on-screen locations.
public function get position() : Rectangle { return _rect; } public function set position(rect:Rectangle) : void { _rect = rect; _stage3D.x = rect.x; _stage3D.y = rect.y; configureBackBuffer(rect.width, rect.height); _modelViewMatrix = new Matrix3D(); _modelViewMatrix.appendTranslation(-rect.width/2, -rect.height/2, 0); _modelViewMatrix.appendScale(2.0/rect.width, -2.0/rect.height, 1); } internal function get modelViewMatrix() : Matrix3D { return _modelViewMatrix; } public function configureBackBuffer(width:uint, height:uint) : void { _context3D.configureBackBuffer(width, height, 0, false); }Step 27: Handle Batches
The final step in the creation of our Stage3D game demo is to handle the addition and removal of geometry batches as well as a loop that calls the draw function on each batch. This way, when our game's main
ENTER_FRAMEevent is fired, it will move the sprites around on screen via the entity manager and then tell the sprite stage system to draw itself, which in turn tells all known batches to draw.Because this is a heavily optimized demo, there will only be one batch in use, but this will change in future tutorials as we add more eye candy.
public function addBatch(batch:LiteSpriteBatch) : void { batch.parent = this; _batches.push(batch); } public function removeBatch(batch:LiteSpriteBatch) : void { for ( var i:uint = 0; i < _batches.length; i++ ) { if ( _batches[i] == batch ) { batch.parent = null; _batches.splice(i, 1); } } } // loop through all batches // (this demo uses only one) // and tell them to draw themselves public function render() : void { for ( var i:uint = 0; i < _batches.length; i++ ) { _batches[i].draw(); } } } // end class } // end packageStep 28: Compile and Run!
We're almost done! Compile your SWF, fix any typos, and check out the graphical goodness. You should have a demo that looks like this:
If you are having difficulties compiling, note that this project needs a class that was made by Adobe which handles the compilation of AGAL shaders, which is included in the source code .zip file download.
Just for reference, and to ensure that you've used the correct filenames and locations for everything, here is what your FlashDevelop project should look like:
Tutorial Complete: You Are Awesome
That's it for tutorial one in this series! Tune in next week to watch the game slowly evolve into a great-looking, silky-smooth 60 FPS shoot-em-up. In the next part, we will implement player controls (using the keyboard to move around) and add some movement, sounds and music to the game.
I'd love to hear from you regarding this tutorial. I warmly welcome all readers to get in touch with me via twitter: @mcfunkypants or my blog mcfunkypants.com or on Google+ any time. I'm always looking for new topics to write future tutorials on, so feel free to request one. Finally, I'd love to see the games you make using this code!
Thanks for reading. See you next week. Good luck and HAVE FUN!
We’re starting a new community project: every week, we’ll post a web app or game and ask you all for your feedback on what it gets right, what it gets wrong, and how we could all learn from its design choices. We’ll also frequently offer our own thoughts. This week, Ashish Bogawat gives us a rundown of the New York Times’s app for Chrome.
Introduction
It wasn’t long ago that Google took the desktop web browser market by storm with Chrome. Along with a whole bunch of fresh ideas and relentless focus on lightning fast browsing, the feature that caught all other browsers unaware was the Chrome Web Store. What started with a whole lot of overhyped links to websites and a handful of specially designed browser-based apps has quickly turned into an avalanche of native applications that seem to take better advantage of the browser’s capabilities with every release, making Chrome the clear leader in the browser space as far as third-party develop support is concerned.
The New York Times app was one of the first native apps on Chrome – and was, for a long time, their showcase for the future of web-based rich internet applications. Built specifically to take advantages of the browser’s innards, the app took news reading in a browser to an entirely new level and set the trend for others to follow. It is only apt, then, that we start this series with the NYTimes Chrome app.
I would like to divide this critique into two broad sections: what I think works very well in the app from a user experience standpoint, and what doesn’t. The focus here is purely on the the interface and interaction design, and does not attempt to get into the technicalities of how the app was developed – something I would rather leave to the experts.
What I Like About the App
The first thing that strikes you when you launch the app is how clean, simple and snappy it feels. The designers have clearly spent a lot of time ensuring that the one thing that stays in focus the most is the content – the news.
A good 90% of the screen real estate is dedicated to the content which, by default, contains the top news stories of the moment. On the right, the Sections list is clearly stated, yet unobtrusive. Since I am not a paid subscriber to the NY Times, the lock icons clearly tell me that I’m only going to have restricted access to anything other than the Top News category.
The design for the content is a nice hybrid between traditional newspaper layouts and more modern, for-screen interactive layouts. Bigger stories get more space on the left with stories getting smaller and smaller as they go towards the right and demand less importance. Since the interactive medium affords designers the possibility of giving readers a glimpse of the content before delving deeper, a much more voluminous scattering of stories is possible here than in a printed newspaper, and the app takes good advantage of this ability.
The layout also adapts to some extent to the content it represents. The Photos section, for example, is much more a collage of images than a grid of stories since there’s little to read there. While clicking a story opens up the full text of that news item, clicking a photo pulls up a slideshow of related images.
Of course, the app also scores a lot of points in giving the reader the ability to control their news reading experience. If you don’t like the default layout, there are a whole bunch of alternative styles available from the ‘Layout’ link in the bottom right. Sure, none of these are drastically different from each other, but for the audience of this app – long time readers who are likely to spend a decent amount of time within it – the subtle variations can mean the difference between easy reading and frustration.
The ability to navigate through the entire app with keyboard shortcuts (primarily the arrow keys) is a huge advantage. Again, this is a big win for avid readers. As with pretty much every reading app worth its while, the abilities to bookmark and share stories, and to customize the font size when reading long articles are common sense inclusions, and nicely implemented.
What Doesn’t Work as Well
As with most apps, though, not everything is perfectly where it should be. Subtle design decisions like the save icon (+) in the top-right corner of each story synopsis, the color changes on rollover or the lock icons to denote premium content are good as long as one is savvy enough to figure out what they might mean. And yes, one might argue that computer literacy – at least in a country like the US – is at a stage where non-savvy users are rare. But my problem remains that affordances for interactive elements in a design this flat pose a learning curve to some extent for all users.
From a user interface design perspective, some decisions make very little sense. At first glance, the right-side panel looks like an accordion where I expect the Layout, My Account and Shortcuts to open up inside the panel to reveal details like the Sections do. Instead, they are all links that behave entirely differently when clicked. No hover states for pretty much anything in this panel as well as the bottom panel in the story view are glaring omissions that don’t make any sense. The page navigation arrows don’t even have a cursor change to indicate that they are active!
Another problem which is possibly unique to me is that some of the keyboard shortcuts just don’t work. When in the main view, hitting the / key is supposed to switch me over to the selecting articles using arrow keys, but I just can’t seem to get it to work. Maybe I’m doing something wrong or my browser/OS combination is at fault, but I expect the designers to have tested the most common scenarios (mine is a simple Chrome Beta on Windows 7 setup that’s far from rare) before releasing the app.
There are also other bugs like blank pages at the end of stories, which leads one to wonder what level of testing goes into the release of an app of this magnitude.
Wrap-Up
So yes, the app has its pros and cons. All in all, though, I like what it achieves with its brave attempt to bring the best of both worlds to the platform. News on the web has come a long way since the days of scanned PDFs of newspapers playing through clumsy Flash widgets, and the future seems only brighter. Now to wait and see which publication one-ups NY Times in building something truly revolutionary.
Your Turn
What do you think of NYTimes for Chrome? Share your constructive criticism in the comments below!
And if you’ve got a browser app or game that you’d like the Activetuts+ community to do a critique on, submit it here. We’re looking forward to seeing what you’ve built.
Flash games are very much the bread and butter of indie pop-nerd culture. If you consider the slices of bread the menu and the game itself, what is left? The butter – the very substance that makes the bread taste that much more delicious. And in terms of a Flash game, what comes in between menus and games are the transitions!
Final Result Preview
This is an example pattern of the transition effect that we will be working towards:
Step 1: Setting Up
Per usual we need to create a new Flash File (ActionScript 3.0). Set its width to 400px, its height to 200px, and the frame rate to 30fps. The background color can be left as the default. Save the file; it can be named whatever you please. I named mine Transitions.fla.
Next we need to create a document class. Go to your Flash file’s properties and set its class to
Transitions. Then create the document class:package { import flash.display.*; import flash.events.*; public class Transitions extends MovieClip { static public var val:Number = new Number(); static public var transitionAttached:Boolean = new Boolean(); public function Transitions() { val = 0; transitionAttached = false; } } }The code just introduced two variables. The first will be used to select the effect’s pattern, and the second will be used to check against having multiple instances of the effect on the stage.
Step 2: Creating the Square Sprite
Our next step is to create the sprite that will be used as each square for the transition. Create a new class and save it as Square.as:
package{ import flash.display.*; import flash.events.*; public class Square extends Sprite{ public var squareShape:Shape = new Shape(); public function Square(){ } } }We use the
squareShapevariable to draw our shape inside the Sprite. Draw a rectangle 40px by 40px (Which is the full size) and set its scale to0.1, a tenth of its size – this will aid us in the effect later:Step 3: Creating the Effect
Create another new class for the effect itself. Once we are finished, adding the effect to the stage will be very simple:
package{ import flash.display.*; import flash.events.*; import flash.utils.*; public class FadeEffect extends Sprite{ public var currentFadeOut:int = 00; public var currentSquares:int = 01; public var pauseTime:int = 01; public var tempNum:int = 00; public var fading:String = "in"; public var fadeinTimer:Timer = new Timer(100); public var fadeoutTimer:Timer = new Timer(100); public var fadeArray:Array = [ //top [[01,01,01,01,01,01,01,01,01,01], [02,02,02,02,02,02,02,02,02,02], [03,03,03,03,03,03,03,03,03,03], [04,04,04,04,04,04,04,04,04,04], [05,05,05,05,05,05,05,05,05,05]], //bottom [[05,05,05,05,05,05,05,05,05,05], [04,04,04,04,04,04,04,04,04,04], [03,03,03,03,03,03,03,03,03,03], [02,02,02,02,02,02,02,02,02,02], [01,01,01,01,01,01,01,01,01,01]]]; public var squaresArray:Array = new Array(); public function FadeEffect(){ } } }You are probably thinking “that is a heck of a lot of variables, what all are they used for?”:
currentFadeOut- used as a check fortempNumto see how many squares are to be scaledcurrentSquares- the current value indicating which squares should be attached and/or scaledpauseTime- a simple integer to give a slight pause in between transitions and removing itselftempNum- used to check what numbers in the array are to be scaledfading- a string to check if the transition is fading in or outfadeinTimer- a timer that is called to begin the fading in of the current value of currentSquaresfadeoutTimer- another timer that is called to begin the fading out of the current value of currentSquaresfadeArray- the 3D array that contains all the transition patternssquaresArray- an array for the Square spritesOur effect will begin by initiating an event listener for
fadeInTimerand starting it. We also need to add an event listener to continuously scale all of the sprites to their correct sizes. Use the following code inside the constructor:fadeinTimer.addEventListener("timer", fadeSquaresInTimer); fadeinTimer.start(); addEventListener(Event.ENTER_FRAME, enterFrame);The next step is to create those two event listeners. We will start with the easier of the two, the
enterFramefunction:public function enterFrame(e:Event){ for each(var s1 in squaresArray){ tempNum+=1; if(fading=="in"){ if(s1.scaleX<=1){ s1.scaleX+=0.05; s1.scaleY+=0.05; } }else if(fading=="out"){ if(tempNum<=currentFadeOut){ if(s1.scaleX>=0.1){ s1.scaleX-=0.05; s1.scaleY-=0.05; }else{ if(s1.visible == true){ s1.visible = false; } } } } } tempNum=00; }It may not make total sense right now, but this should help shed some light.
s1is the instance name that will be given to the Squares when we create them in a later function.squaresArrayto keep track of the number of them and we perform the same operation for every object in the array.tempNum(used in the fading out if-statement) which is used to scale the sqaures in the order that they were added to the array. This means it is not pattern dependant and will work with any pattern.After that…
fadingis true or not.currentFadeOut(these are the ones that should be scaling, all others should remain at full scale until the value increases).It’s time to add the event listener for the timer:
public function fadeSquaresInTimer(e:Event){ fadeSquaresIn(fadeArray[Transitions.val]); currentSquares+=1; }At first glance it looks less complicated, but you should notice that we are calling a function with the
fadeArrayas the parameter. Which pattern is selected from the array depends on what you setvalequal to in the Transitions class; right now it should use the first pattern becausevalis set to 0.The next step is to create the
fadeSquaresInfunction that is called from the previous timer:public function fadeSquaresIn(s:Array){ for (var row=0; row<s[0].length; row++) { for (var col=0; col<s.length; col++) { } } }First thing that we accomplish is iterating through the selected pattern. We start at row 1, colomn 1 and cycle through every colomn until the end of the row has been reached. Then we move onto the next row and repeat the process.
The next thing to do is compare the current item in the array to the value of
currentSquares:if(int(s[col][row]) == currentSquares){ }If they are equivalent we add a square, position it accordingly, and push it onto the
squaresArrayso that it can be scaled:We are almost done with this function, we just have to perform a check for when there are the same number of squares as there are items in the pattern. We do so by adding the following if-statement outside both for-loops:
if(squaresArray.length == (s[0].length * s.length)){ fadeinTimer.stop(); addEventListener(Event.ENTER_FRAME, pauseBetween); }Self explanatory – we stopped the timer and called an event listener for the pause between fading in and fading out. That function is used to initiate the fading out and may also be used to cause change in your game:
public function pauseBetween(e:Event){ pauseTime+=1; if(pauseTime==60){ currentSquares=01; fading="out"; fadeoutTimer.addEventListener("timer", fadeSquaresOutTimer); fadeoutTimer.start(); removeEventListener(Event.ENTER_FRAME, pauseBetween); } }We won’t spend much time on this function due to its simplicity. Here we increase the value of
pauseTime, and once it equals 60 (meaning two seconds have passed) we set the value ofcurrentSquaresback to 1, setfadingto"out"so that the squares can scale backwards, remove the listener forpauseBetween()itself, and add an event listener for this new function:public function fadeSquaresOutTimer(e:Event){ fadeSquaresOut(fadeArray[Transitions.val]); currentSquares+=1; }This works much like
fadeSquaresInTimer(), though this time we are calling the functionfadeSquaresOut():public function fadeSquaresOut(s:Array){ for (var row=0; row<s[0].length; row++) { for (var col=0; col<s.length; col++) { if(int(s[col][row]) == currentSquares){ currentFadeOut+=1; } } } }We cycle through, but this time when we find an equivalent item we increase the value of
currentFadeOutso that the next item in thesquaresArraycan begin fading out.Almost finished now; all that’s left is to stop the timer and remove the effect. Add this if-statement outside of the two for-loops:
if(currentFadeOut == (s[0].length * s.length)){ fadeoutTimer.stop(); pauseTime=01; addEventListener(Event.ENTER_FRAME, delayedRemove); }This checks whether all of the items have begun fading out. If so, it then stops the timer, sets
pauseTimeback to 1 and adds an event listener for the functiondelayedRemove():public function delayedRemove(e:Event){ pauseTime+=1; if(pauseTime==30){ Transitions.transitionAttached = false; removeEventListener(Event.ENTER_FRAME, delayedRemove); stage.removeChild(this); } }Like before we increase the value of
pauseTime, and once it equals 30 (1 second) we set the boolean back tofalseso that the effect can be added once again. We remove this event listener and we remove this effect from the stage.Step 4: Adding the Effect
Now comes the easy part. Add the following code inside the document class constructor to add the effect:
if(transitionAttached == false){ transitionAttached = true; var f1:Sprite=new FadeEffect; stage.addChild(f1); }Step 5: Creating More Patterns
Feel free to create your own patterns! It’s extremely simple, just create a new 2D array inside the 3D array. Here is the array that I have created (just replace your 3D array with it). It includes 8 different transitions:
You can change the value of
Transitions.valto choose another pattern – for example, ifvalis3, the transition will sweep in from the right.Conclusion
Thanks for taking the time to read this tutorial. If you have any questions please leave a comment below. And if you would like a challenge, try making the effect fade in with one pattern and fade out with an opposing one.
Each month, we bring together a selection of the best tutorials and articles from across the whole Tuts+ network. Whether you’d like to read the top posts from your favourite site, or would like to start learning something completely new, this is the best place to start!
Psdtuts+ — Photoshop Tutorials
Create a Baseball-Inspired Text Effect in Photoshop
Applying texture to a text effect can be a lot of fun. In this tutorial we will explain how to create a baseball-inspired text effect using layer styles, patterns, and brushes. Let’s get started!
Visit Article
Create a Mini Planet Using Photoshop’s 3D Capabilities
When most people think about Photoshop, they probably don’t think about 3D. What most people don’t realize, however, is that Photoshop CS5 Extended includes some powerful tools to help you render your artwork in 3D. In this tutorial we will demonstrate how to create a mini planet using Photoshop’s 3D capabilities. Let’s get started!
Visit Article
Create a Coffee Cake Photo Manipulation – Tuts+ Premium Tutorial
In this Tuts+ Premium tutorial, author Stephen Petrany will demonstrate how to take pieces from multiple photos and seamlessly blend them into a "coffee cake" photo manipulation. This tutorial will also explore unique ways to work with paths and smart objects. If you are looking to take your photo manipulation skills to the next level then Log in or Join Now to get started!
Visit Article
Nettuts+ — Web Development Tutorials
The Largest jQuery Class in the World
A couple weeks ago, Tuts+ Premium launched a free new real-time course, called “30 Days to Learn jQuery.” After signing up, each member receives an email, linking to a new video lesson for an entire month.
Visit Article
How to Customize Your Command Prompt
Lately, I’ve been getting this question a lot: “how did you get your terminal to look the way it does?” If you’ve noticed my terminal and are curious about how I set it up, this is the tutorial for you! Of course, what you learn here will be enough to get you started on creating your own custom command prompt, as well!
Visit Article
Attention Developers: NewRelic is your Secret Weapon
While the title of this article may sound like a cliche, hatched in the bowels of PR hell, I’m serious when I say that NewRelic is your secret weapon.
Visit Article
Vectortuts+ — Illustrator Tutorials
How to Create a Vintage Type Postcard
Follow this in-depth look at the process of designing type for a vintage style postcard in Adobe Illustrator CS5. Harken back to an era when postcards were all the rage with this friendly type style. The tutorial will delve into clipping masks, using bitmap images, working with layers and type effects.
Visit Article
Create a Block Game Interface in Illustrator
In the following tutorial you will learn how to create a block game interface in Adobe Illustrator CS5. Vector game graphics allow for versatile artwork. The workflow presented in this tutorial will teach you how to create game graphics in Illustrator. These techniques can be applied to multiple interface design and game design projects. It’s time to jump in, learn to create these shapes, and give them colorful graphic depth.
Visit Article
Illustrator Tutorials for Creating Vintage Graphics and Retro Illustration
If youre looking to improve your vector design skills, learn how to use Illustrator on a deeper level, and discover how to create vintage vector graphics, then you’ve landed on the right article. We’ve assembled a collection of tutorials that show you how to create vintage illustrations, and retro graphics using Illustrator effects and a variety of professional workflows.
Visit Article
Webdesigntuts+ — Web Design Tutorials
Principles for Successful Button Design
There are a thousand ways to design and create buttons today and you only need to spend a small amount of time looking through work on dribbble to get a sense of them. A great deal of these examples are exactly the same, but occasionally there are the odd few that feel like they’ve had a little more care and attention in their making.
Visit Article
Orman Clark’s Vertical Navigation Menu: The CSS3 Version
Next in the Orman Clark’s coded PSD series is his awesome looking Vertical Navigation Menu. We’ll recreate it with CSS3 and jQuery while using the minimal amount of images possible.
Visit Article
Coding the SimpleAdmin Theme: Login Page
It’s time to translate our admin layout into a working template. We’ll begin by setting out the markup for our Login page, then we’ll hit the stylesheets..
Visit Article
Phototuts+ — Photography Tutorials
The Stock Market: Exploring Stock Photography
Creative professionals all over the world frequently require high quality images, but often don’t have the budget to hire a photographer for small projects. Enter stock photography: an industry where awesome photographs are out there and ripe for the using. Today, we’ll be taking a look at the wild world of the stock market – stock photography, that is.
Visit Article
Lightroom 4 Beta: Packed with New Features
In six short years, Adoble Lightroom has changed the way many photographers manage their images. With powerful cataloging and developing features, Lightroom offers photographers the ability to customize their photo management workflow and manage the thousands of images more efficiently than ever before. Adobe’s innovation continues with Lightroom 4, which is currently in Beta. Today, we’ll be taking a look at some of the new features of the latest iteration of Lightroom.
Visit Article
A Primer to Digital Medium Format Camera
Over the last few months, I’ve observed a trend among several well known photographers. No longer satisfied with crop factor cameras or even 35mm equivalent full frame digital cameras, more and more photographers are jumping to digital medium format. What are the advantages offered by digital medium formats, and will you be using one anytime soon? Read on to find out.
Visit Article
Cgtuts+ — Computer Graphics Tutorials
Achieving 3D Realism: Reception Area Render With 3D Studio Max & V-Ray, Part 1
The following tutorial is based on a real project. This unique tutorial will take users through the real process of creating shaders with bespoke physical properties and applying textures based on real photo references.
Visit Article
Create And Render A Still Life Scene In Blender, Using Cycles
Today, we’ll have a brief introduction to Blender’s new rendering engine – Cycles. This tutorial will cover modeling a small and easy still life scene, setting up different types of materials used in cycles and then finally lighting and rendering the scene.
Visit Article
An Introduction To UVMapping In 3d Studio Max Using The Unwrap UVW Modifier
So UVMapping… you hate it, I hate it. But unfortunately it’s a necessary step in the process of completing most cg projects. In this tutorial we’ll look at creating uvs using the ‘Unwrap UVW’ modifier in 3D Studio Max, and discuss what uv mapping is, why it’s necessary and some ways to approach it.
Visit Article
Aetuts+ — After Effects Tutorials
Create The Amazing Spider-Man Title Sequence Entirely In After Effects
Nancy will show us how to create the title sequence for the Amazing Spider-Man entirely in After Effects using ShapeShifter AE. She shows us how to combine Shape Layers + Layer Masks to model and animate Spideys symbol. Download the free project file and follow along. You’ll be amazed at how easy it is to create!
Visit Article
D Transforming Text With ShapeShifter AE
In this tutorial, we will be taking a look at how to build this 3D transforming text animation using Mettle’s ShapeShifter AE plug-in. We will also be enhancing some of the elements using 3rd party plug-ins such as Trapcode Shine (CC Light Burst alternative), Frischluft’s Out of Focus (Lens Blur alternative), and RE:Vision RSMB (CC Force Motion Blur alternative).
Visit Article
Create An Awesome Array Of Shattering Strings
We’ll be starting in Cinema 4d to create text fragments and use XPresso to export the Mograph positional data to After Effects. From there, we’ll jump over into After Effects and use expressions to connect 3d nulls to 2d data points… We’ll also be using a macro in Microsoft Word to edit multiple lines of expressions. It doesn’t matter if you’re a Cinema 4d user or strictly an After Effects user, today’s tutorial should be something helpful for everyone!
Visit Article
Audiotuts+ — Audio & Production Tutorials
The 15 Minute Mix
Consider this your challenge for today. Take a song that you just recorded, or have been working on and mix it in 15 minutes. Shut off everything, pull the faders up and follow the following tutorial. Use a stopwatch to keep track of time and when you should be switching tasks.
If you don’t have any sessions to try, you can use any of these 50 different multi-tracks.
Visit Article
Audio Tutorial Sites That Will Keep You Learning
The reason you’re here on our site is because you’re interested in audio tutorials. I think we do a great job: we have a huge number of excellent tuts – both free and premium. But we know we haven’t cornered the market. There are an amazing number of audio tut sites out there, and the number seems to grow every year. Here are 35 of the best.
Visit Article
Quick Tip: Creating Skrillex Style Tech Basslines in NI Massive
This series of quick tips will outline how you can use the ever powerful NI Massive synth to create techy basslines used by artists such as Skrillex. In this example I have used Cubase but the same principles will translate to pretty much any other DAW. Here is an example of the kind of sound you can expect to end up with at the end of this series:
Visit Article
Activetuts+ — Flash, Flex & ActionScript Tutorials
Review: Construct 2, a Drag and Drop HTML5 Game Maker
Construct 2 is an HTML5 game making tool that doesn’t require any programming knowledge. You just drag and drop items around, add behaviors to them, and make them come alive with “events”.
Visit Article
Number Systems: An Introduction to Binary, Hexadecimal, and More
Ever see crazy binary numbers and wonder what they meant? Ever see numbers with letters mixed in and wonder what is going on? You’ll find out all of this and more in this article. Hexadecimal doesn’t have to be scary.
Visit Article
Understanding Affine Transformations With Matrix Mathematics
Inspired by Prof. Wildberger in his lecture series on linear algebra, I intend to implement his mathematical ideas with Flash. We shall not delve into the mathematical manipulation of matrices through linear algebra: just through vectors. This understanding, although diluting the elegance of linear algebra, is enough to launch us into some interesting possibilities of 2×2 matrix manipulation. In particular, we’ll use it to apply various shearing, skewing, flipping, and scaling effects to images at runtime.
Visit Article
Wptuts+ — WordPress Tutorials
Creating a Filterable Portfolio with WordPress and jQuery
Learn in this tutorial how to make a filterable Portfolio with jQuery integrated with WordPress, remember that this portfolio kind can make a big difference on your themes!
Visit Article
How to Include JavaScript and CSS in Your WordPress Themes and Plugins
Knowing the proper way to include JavaScript and CSS files in your WordPress themes and plugins is very important for designers and developers. If you don’t adhere to best practices, you run the risk of conflicting with other themes and plugins, and potentially creating problems that could have been easily avoided. This article is intended as a reference for playing nicely with others.
Visit Article
How to Create a Simple Post Rating System With WordPress and jQuery
There already are many post rating system plugins out there. Surprisingly, no one fits my needs, they either are too complicated or with too many built-in options. So, in this tutorial, you’ll learn how to build your own simple post rating functionality, directly within your theme files. There’s no need for plugin!
Visit Article
Mobiletuts+ — Mobile Development Tutorials
Getting Started With RenderScript on Android
RenderScript is a scripting language on Android that allows you to write high performance graphic rendering and raw computational code. Learn more about RenderScript and write your first graphics app that leverages RenderScript in this tutorial.
Visit Article
PhoneGap From Scratch: Twitter & Maps
Want to learn how to use PhoneGap, but don’t know where to get started? Join us as we put together ’Sculder”, not only a tribute to an excellent science fiction TV series, but a fully-fledged native mobile application for the believer in you!
Visit Article
Supplementing iAd Placement with AdMob
Click-based advertising within a mobile application is a great way to make some money off of your free or inexpensive applications. While there are many choices out there, many iOS developers tend to go with the iAds platform for a variety of reasons including simplicity, aesthetics, and a high CPM.
Visit Article
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
graphicsobject 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.Elementclass and create an Element in theinit()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
testElementvariable which holds our element. We create a new Element and assign that to ourtestElementvariable. 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()FunctionThis function does the following:
caughtparameter, 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).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.
boundsXwill hold how many elements could be drawn in one row.randomXtakes thisboundsX, multiplies it with a Number between zero and one, and floors it. IfboundsXis 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.boundsYwill hold how many elements can be drawn in one column.randomYtakes thisboundsY, multiplies it with a Number between zero and one, and floors it.In the for loop, we check whether the apple’s new x and y positions are identical to any of the
snake_vectorselements. If so, we call theplaceApple()function again (recursive function), and set the parameter of it tofalse. (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 theinit()function.private function init(e:Event = null):void { /* . . . */ placeApple(false); }Notice that we pass
falseas the parameter. It’s logical, because we didn’t catch the apple in theinit()function yet. We will only catch it in themoveIt()function.Now there are only three more functions to write: the
directionChanged(),moveIt()and thegameOver()functions.Step 8: Starting the
moveIt()FunctionThe
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: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_valueplus 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()FunctionThis 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
deadvariable to true, stop the movement with the timer, remove every child of the class and call theinit()function, like we just started the game.Now, let’s get back to the
moveIt()function.Step 10: Continuing the
moveIt()FunctionWe 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()FunctionWe 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 thegameOver()function.Put this at the end of the
init()function (setting up the listener function):And this at the end of the
gameOver()function: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?
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
flagvariable, 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 theinit()function for that).So we need to change the
directionChanged()function a little. The if blocks’ heads should be changed: add a&& flagclause 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: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:
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 namedSuperApple. Whenever you catch a super apple, the snakes parts could lengthen by three elements, perhaps. This could be set with setters/getters in theSuperAppleclass.If you wish to do this, and you get stuck somewhere, just leave me a comment here.
Thank you for your time!
In this tutorial you’ll learn how to create a spinning wheel using Flash and AS3, with an interface that’s suitable for both mouse- and touch-based devices.
Final Result Preview
Let’s take a look at the final result we will be working towards:
Click and drag your mouse vertically to spin the wheel; the longer the line you drag, the faster the wheel will spin! Once it stops, the colored bar at the bottom will display the color the wheel landed on.
Step 1: Brief Overview
Using pre-made graphic elements we’ll create a colorful interface that will be powered by several ActionScript 3 classes.
The user will be able to spin the wheel using a dragging gesture represented by a line on the screen; a taller line will make a faster spin.
Step 2: Flash Document Settings
Open Flash and create a 500x300px document. Set the frame rate to 24fps.
Step 3: Interface
A colorful nice looking interface will be displayed, made up of multiple shapes, MovieClips and more.
The simple shapes were created using the Flash Pro drawing tools, and since they’re easy to duplicate I won’t explain their creation. Make sure the wheel’s rotation point is in the center.
You can always look at the FLA in the source download files.
Step 4: Instance Names
The image above shows the Instance Names of the various MovieClips. Pay special attention to the
wheel.pMovieClips; these are the little black lines that divide the colors in the wheel, and are inside thewheelMovieClip. They are namedp1top10, going clockwise.Step 5: TweenMax
We’ll use a different tween engine than the default one included in Flash; this will make the color transition of the
colorMCsymbol a lot easier.You can download TweenMax from the Greensock website.
Step 6: Set Main Class
Add the class name,
Main, to the Class field in the Publish section of the Properties panel to associate the FLA with the Main document class.Step 7: Create a new ActionScript Class
Create a new (Cmd + N) ActionScript 3.0 Class and save it as Main.as in your class folder.
Step 8: Class Structure
Create your basic class structure to begin writing your code.
package { import flash.display.Sprite; public class Main extends Sprite { public function Main():void { // constructor code } } }Step 9: Required Classes
These are the classes we’ll need to import for our class to work. The
importdirective makes externally defined classes and packages available to your code.Step 10: Variables
These are the variables we’ll use; read the comments in the code to know more about them:
Step 11: Constructor
The constructor is a function that runs when an object is created from a class, and is the first to execute when you make an instance of an object. Since this is our document class, it’ll run as soon as the SWF loads.
public final function Main():void { //code... }Step 12: Paddles Vector
First we add the various paddle MovieClips to the vector, and add the listeners – we’ll write the
listeners()function next.public final function Main():void { paddles.push(wheel.p1, wheel.p2, wheel.p3, wheel.p4, wheel.p5, wheel.p6, wheel.p7, wheel.p8, wheel.p9, wheel.p10); listeners('add'); }Step 13: Listeners
This function will add or remove the listeners according to the parameter. Mouse Listeners are set to draw the line that will control the wheel.
private final function listeners(action:String):void { if(action == 'add') { stage.addEventListener(MouseEvent.MOUSE_DOWN, startDraw); stage.addEventListener(MouseEvent.MOUSE_UP, spinWheel); } else { stage.removeEventListener(MouseEvent.MOUSE_DOWN, startDraw); stage.removeEventListener(MouseEvent.MOUSE_UP, spinWheel); } }Step 14: Movement Line
The next function starts to create a line based on the current mouse position, and places it on the stage. It’s triggered when the mouse is clicked.
private final function startDraw(e:MouseEvent):void { line = new Shape(); addChild(line); line.graphics.moveTo(mouseX, mouseY); line.graphics.lineStyle(8, 0x000000, 0.3);//you can change the line color and style here stage.addEventListener(MouseEvent.MOUSE_MOVE, drawLine); }Step 15: Draw Line
While the mouse is moved, the line continues in that direction.
private final function drawLine(e:MouseEvent):void { line.graphics.lineTo(mouseX, mouseY); }Step 16: Spin the Wheel
The next code runs when the mouse button is released, finishing the line. The drawing listeners are removed to avoid drawing multiple lines and the speed is calculated according to the height of the line. Finally, an EnterFrame event is called to actually rotate the wheel.
private final function spinWheel(e:MouseEvent):void { stage.removeEventListener(MouseEvent.MOUSE_MOVE, drawLine); listeners('rm'); speed = line.height * 0.1; removeChild(line); line = null; stage.addEventListener(Event.ENTER_FRAME, spin); }Step 17: Rotate the Wheel
This is the function that will spin the wheel and detect what value it lands on:
private final function spin(e:Event):void { /* Rotate Wheel */ wheel.rotationZ += speed;Step 18: Detect Value
Here we detect the current value of the wheel based on the last paddle it touched.
/* Detect Value */ for(var i:int = 0; i < 10; i++) { if(indicator.hArea.hitTestObject(paddles[i])) { lastPaddle = paddles[i].name; } }Step 19: Decrease Speed
The wheel’s speed is reduced every frame to eventually stop the spinning.
Step 20: Reset Wheel
All values are reset when the wheel stops. A function that will run an action according to the final value is called.
/* Remove listener and reset speed when wheel stops */ if(speed <= 0) { stage.removeEventListener(Event.ENTER_FRAME, spin); speed = 0; setBarColor(lastPaddle); listeners('add'); } }Step 21: Set Bar Color
This function will run a custom action according to the last value of the wheel. In this case it changes the color of the bottom bar, but you could make it do anything else.
function setBarColor(action:String):void { switch(action) { case 'p1': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0xF15D5D, tintAmount:1}}); break; case 'p2': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0xC06CA8, tintAmount:1}}); break; case 'p3': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0x644D9B, tintAmount:1}}); break; case 'p4': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0x5E98C6, tintAmount:1}}); break; case 'p5': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0x4789C2, tintAmount:1}}); break; case 'p6': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0x55C4CB, tintAmount:1}}); break; case 'p7': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0x57BC80, tintAmount:1}}); break; case 'p8': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0x90CC6C, tintAmount:1}}); break; case 'p9': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0xEBE666, tintAmount:1}}); break; case 'p10': TweenMax.to(colorMC, 0.5, {colorTransform:{tint:0xF29C69, tintAmount:1}}); break; } }Conclusion
Change the code to perform your own actions!
I hope you liked this tutorial, thank you for reading!
It’s Premium time again! To kick off our Shoot-’Em-Up Session (and to follow up on last year’s Flash and AS3 shooter tutorial), Carlos Yanez has written a tutorial that’ll teach you how to create a simple space shooter game with HTML5 and the EaselJS library.
Premium Preview
Let’s take a look at the final result we will be working towards:
Click to play the demo.
Read the Full Tutorial
Premium members can access the full tutorial right away!
If you’re not yet a Premium member, you can still read the first few steps of the tutorial.
Tuts+ Premium Membership
We run a Premium membership system which periodically gives members access to extra tutorials, like this one, from across the whole Tuts+ network. If you’re a Premium member, you can log in and download the tutorial. If you’re not a member, you can of course join today!
Also, don’t forget to follow @envatoactive on twitter, circle us on Google+, like us on Facebook, and grab the Activetuts+ RSS Feed to stay up to date with the latest tutorials and articles.
In this tutorial we will go from asking “What is Flixel?” to having an indoor room and a keyboard-controlled character in the top-down role playing game style (think Zelda).
Final Result Preview
Let’s take a look at the final result we will be working towards:
Step 1: Understanding the Project Structure
For the visual people among us, let’s see how everything will be organized so the rest will make sense.
Basically, we have all of our artwork stored in the
assetsfolder and all of our ActionScript files stored in thesrcfolder. If you want to use this tutorial as the basis for your own game engine, thetopdownfolder contains the generic stuff (a.k.a. the engine) and thetutorialfolder shows how to use it.You’ll probably notice rather quickly that the art files have really long names. Rather than showing you a tutorial filled with compelling red boxes (the apex of my artistic ability), we will use some open source artwork from OpenGameArt. Each file is named to show the source, the artist, and the license. So, for example,
armor (opengameart - Redshrike - ccby30).pngmeans it’s an image of armor, downloaded from OpenGameArt, created by the artist known as Redshrike, and it uses the CC-BY-30 license (Creative Commons Attribution).Long story short – these art files can be used for any purpose as long as we link back to the site and give credit to the artist.
Here’s a description of each source file in the project:
topdown/TopDownEntity.as– base class for any moveable sprites in our top-down RPGtopdown/TopDownLevel.as– base class for a top-down RPG leveltutorial/Assets.as– imports any images that we need to use in this tutorialtutorial/IndoorHouseLevel.as– defines an indoor room with some objects lying aroundtutorial/Player.as– a keyboard-controlled, animated Rangertutorial/PlayState.as– Flixel state that controls our gameDefault.css– an empty file needed to prevent the Flex compiler from giving us a warningMain.as– entry point for the applicationPreloader.as– Flixel preloaderNow let’s get down to business!
Step 2: Firing Up Flixel
Flixel is a 2D game engine for ActionScript 3. To quote the home page:
The most important thing to know about Flixel is that it is designed to use bitmap images (raster graphics) instead of Flash-style vector graphics. You can use Flash movie clips, but it takes a little massaging. Since I don’t feel like giving a massage today, we will be using images for all our art.
Flixel comes with a tool that creates a dummy project for you. This tool creates the three files that are in the root of our project:
Default.css,Main.as, andPreloader.as. These three files form the basis for almost any project in Flixel. SinceDefault.cssis just there to avoid a compiler warning, let’s take a look atMain.as.package { import org.flixel.*; import tutorial.*; [SWF(width="480", height="480", backgroundColor="#ffffff")] [Frame(factoryClass="Preloader")] public class Main extends FlxGame { /** * Constructor */ public function Main() { super(240, 240, PlayState, 2); } } }There are only three lines of importance here. First off, we tell Flash to use a 480×480 window with a white background. Then we tell Flash to use our
Preloaderclass while loading. Finally, we tell Flixel to use a 240×240 window (zooming in by a factor of 2 to make things look bigger) and to usePlayStateonce everything is ready to go.Let me share a quick word about Flixel’s states. In Flixel, states are kind of like a window, but you can only have one at a time. So, for example, you could have a state for your game’s main menu (
MainMenu), and when a user clicks theStart Gamebutton you switch toPlayState. Since we want our game to just get going immediately, we just need one state (PlayState).Next up is
Preloader.as.package { import org.flixel.system.FlxPreloader; public class Preloader extends FlxPreloader { /** * Constructor */ public function Preloader():void { className = "Main"; super(); } } }Not much to see here. Since we extend from
FlxPreloader, Flixel really just takes care of it. The only thing to note is that if you changedMainto some other name, you would have to changeclassNamehere on the highlighted line.We’re almost up to seeing something on the screen now. All we need is a Flixel state to get the ball rolling, so here’s
PlayState.as.package tutorial { import org.flixel.*; /** * State for actually playing the game * @author Cody Sandahl */ public class PlayState extends FlxState { /** * Create state */ override public function create():void { FlxG.mouse.show(); } } }If you compiled this code, you’d get a marvelous black screen with a mouse cursor. Never fear, it gets better from here.
Step 3: Creating a Basic Level
Now that we have Flixel up and running, it’s time to make a top-down RPG level. I like to give you reusable classes so you can make your own levels, so we’ll actually create a generic level class that we can use to make something more interesting later. This is
topdown/TopDownLevel.as.package topdown { import org.flixel.*; /** * Base class for all levels * @author Cody Sandahl */ public class TopDownLevel extends FlxGroup { /** * Map */ public var state:FlxState; // state displaying the level public var levelSize:FlxPoint; // width and height of level (in pixels) public var tileSize:FlxPoint; // default width and height of each tile (in pixels) public var numTiles:FlxPoint; // how many tiles are in this level (width and height) public var floorGroup:FlxGroup; // floor (rendered beneath the walls - no collisions) public var wallGroup:FlxGroup; // all the map blocks (with collisions) public var guiGroup:FlxGroup; // gui elements /** * Player */ public var player:TopDownEntity; public var playerStart:FlxPoint = new FlxPoint(120, 120); /** * Constructor * @param state State displaying the level * @param levelSize Width and height of level (in pixels) * @param blockSize Default width and height of each tile (in pixels) */ public function TopDownLevel(state:FlxState, levelSize:FlxPoint, tileSize:FlxPoint):void { super(); this.state = state; this.levelSize = levelSize; this.tileSize = tileSize; if (levelSize && tileSize) this.numTiles = new FlxPoint(Math.floor(levelSize.x / tileSize.x), Math.floor(levelSize.y / tileSize.y)); // setup groups this.floorGroup = new FlxGroup(); this.wallGroup = new FlxGroup(); this.guiGroup = new FlxGroup(); // create the level this.create(); } /** * Create the whole level, including all sprites, maps, blocks, etc */ public function create():void { createMap(); createPlayer(); createGUI(); addGroups(); createCamera(); } /** * Create the map (walls, decals, etc) */ protected function createMap():void { } /** * Create the player, bullets, etc */ protected function createPlayer():void { player = new TopDownEntity(playerStart.x, playerStart.y); } /** * Create text, buttons, indicators, etc */ protected function createGUI():void { } /** * Decide the order of the groups. They are rendered in the order they're added, so last added is always on top. */ protected function addGroups():void { add(floorGroup); add(wallGroup); add(player); add(guiGroup); } /** * Create the default camera for this level */ protected function createCamera():void { FlxG.worldBounds = new FlxRect(0, 0, levelSize.x, levelSize.y); FlxG.camera.setBounds(0, 0, levelSize.x, levelSize.y, true); FlxG.camera.follow(player, FlxCamera.STYLE_TOPDOWN); } /** * Update each timestep */ override public function update():void { super.update(); FlxG.collide(wallGroup, player); } } }All of the variables have their own descriptions in the source code, so I won’t bore you with too much repetition. I should, however, explain groups in Flixel.
We have three groups defined here:
floorGroup,wallGroup, andguiGroup. Flixel uses groups to determine in what order to render sprites (to decide what’s on top when they overlap) and to handle collisions. We want the player to be able to walk around on a floor (no collisions needed), but we also want walls and objects (collisions definitely needed) so we need two groups. We also need a separate group for our user interface (guiGroup) so we can make sure it gets rendered on top of everything else.Groups are rendered in the order they are added, which is determined in our
addGroups()function. Since we wantguiGroupto always be on top, we calladd(guiGroup)after all the other groups. If you make your own groups and forget to calladd(), they won’t show up on the screen.In our constructor, we store some useful values (like the number of tiles in the level) and call
create(). Thecreate()function shows you what goes into a Flixel level – a map, a player, an interface, groups (to control rendering order and collisions), and a camera view. Each of these gets its own function to help keep things more readable and so we can re-use common functionality. For instance, take a look atcreateCamera()./** * Create the default camera for this level */ protected function createCamera():void { FlxG.worldBounds = new FlxRect(0, 0, levelSize.x, levelSize.y); FlxG.camera.setBounds(0, 0, levelSize.x, levelSize.y, true); FlxG.camera.follow(player, FlxCamera.STYLE_TOPDOWN); }We won’t need to change this function to make our own indoor level. Flixel has a built-in camera for top-down games (
FlxCamera.STYLE_TOPDOWN). All we’re really doing here is telling the camera not to leave the level (by callingsetBounds()) and telling the camera to follow the player (by callingfollow()) if the level is bigger than the screen and requires scrolling. This will work for almost every kind of level, so we can keep it here rather than re-coding this for each of our levels.The only other thing to notice is in
update()./** * Update each timestep */ override public function update():void { super.update(); FlxG.collide(wallGroup, player); }FlxG.collide(wallGroup, player)causes the player to bump into walls rather than walking through them. Since we don’t callFlxG.collide(floorGroup, player), the player can walk all over the floors with nary a collision in sight (same thing forguiGroup, too).Finally, we need to make
PlayStateuse our fancy level.package tutorial { import org.flixel.*; import topdown.*; /** * State for actually playing the game * @author Cody Sandahl */ public class PlayState extends FlxState { /** * Constants */ public static var LEVEL_SIZE:FlxPoint = new FlxPoint(240, 240); // level size (in pixels) public static var BLOCK_SIZE:FlxPoint = new FlxPoint(16, 16); // block size (in pixels) /** * Current level * NOTE: "public static" allows us to get info about the level from other classes */ public static var LEVEL:TopDownLevel = null; /** * Create state */ override public function create():void { FlxG.mouse.show(); // load level LEVEL = new TopDownLevel(this, LEVEL_SIZE, BLOCK_SIZE); this.add(LEVEL); } } }Remember to call
this.add(LEVEL)unless you want to stare at a black screen forever. As the comment states, I usedpublic static var LEVELas a convenience for the future. Suppose you add some artificial intelligence to your game and your AI needs to know where the player is located; this way, you can callPlayState.LEVEL.playerand keep things nice and easy. It’s not necessarily the prettiest way to do things, but it’ll get the job done if used sparingly.Step 4: Creating a Basic Entity
An entity is something that needs to be displayed and can move around. This could be the player, a computer-controlled character, or perhaps even something like an arrow. Since there can be many entities on a level, we want a generic class that we can use to save ourselves some time. Take a look at
topdown/TopDownEntity.as.package topdown { import org.flixel.*; /** * A moveable object in the game (player, enemy, NPC, etc) * @author Cody Sandahl */ public class TopDownEntity extends FlxSprite { /** * Constants */ public static const SIZE:FlxPoint = new FlxPoint(16, 18); // size in pixels /** * Constructor * @param X X location of the entity * @param Y Y location of the entity */ public function TopDownEntity(X:Number = 100, Y:Number = 100):void { super(X, Y); makeGraphic(SIZE.x, SIZE.y, 0xFFFF0000); // use this if you want a generic box graphic by default } } }Notice that we extend from
FlxSprite. This gives us access to much of the power behind Flixel.makeGraphic()creates a rectangular bitmap of the given size (16×18 in this case), using the color you pass in. This color is in0xAARRGGBBformat, so0xFFFF0000means we’re creating a solid red box (I warned you about my artistic abilities). You can mess around with this value to see how the color changes. In fact, we now have something other than a blank screen!Still not too exciting, but at least we can see something, right?
Step 5: Creating an Indoor Room
I don’t know about you, but I’m tired of looking at that black background. Let’s make it look like a room. Here’s
tutorial/IndoorHouseLevel.as.package tutorial { import org.flixel.*; import topdown.*; /** * A basic indoor scene * @author Cody Sandahl */ public class IndoorHouseLevel extends TopDownLevel { /** * Floor layer */ protected static var FLOORS:Array = new Array( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ); /** * Wall layer */ protected static var WALLS:Array = new Array( 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 ); /** * Constructor * @param state State displaying the level * @param levelSize Width and height of level (in pixels) * @param blockSize Default width and height of each tile (in pixels) */ public function IndoorHouseLevel(state:FlxState, levelSize:FlxPoint, blockSize:FlxPoint):void { super(state, levelSize, blockSize); } /** * Create the map (walls, decals, etc) */ override protected function createMap():void { var tiles:FlxTilemap; // floors tiles = new FlxTilemap(); tiles.loadMap( FlxTilemap.arrayToCSV(FLOORS, 15), // convert our array of tile indices to a format flixel understands Assets.FLOORS_TILE, // image to use tileSize.x, // width of each tile (in pixels) tileSize.y, // height of each tile (in pixels) 0, // don't use auto tiling (needed so we can change the rest of these values) 0, // starting index for our tileset (0 = include everything in the image) 0, // starting index for drawing our tileset (0 = every tile is drawn) uint.MAX_VALUE // which tiles allow collisions by default (uint.MAX_VALUE = no collisions) ); floorGroup.add(tiles); // walls // FFV: make left/right walls' use custom collision rects tiles = new FlxTilemap(); tiles.loadMap( FlxTilemap.arrayToCSV(WALLS, 15), // convert our array of tile indices to a format flixel understands Assets.WALLS_TILE, // image to use tileSize.x, // width of each tile (in pixels) tileSize.y // height of each tile (in pixels) ); wallGroup.add(tiles); } } }The first thing you notice are those two giant arrays of numbers,
FLOORSandWALLS. These arrays define our map layers. The numbers are tile indices based on the artwork we’re using. I’ve zoomed in on the image we’re using for our walls to show you what I’m talking about.Notice that zero is blank (draw nothing). The floor image, on the other hand, is just one tile, repeated, at the moment. That means we want to draw every tile (including zero). So if you look at the
createMap()function, our code to load in the floor is longer than our code to load in the walls.We start with
FlxTilemap.arrayToCSV(FLOORS, 15), which converts our big array into a format Flixel likes (CSV). The number at the end tells Flixel how many values are in each row. Next we tell Flixel which image to use (Assets.FLOORS_TILE– I’ll explain what that’s about in the next step). After defining the size of each block in the image, we have four more values for our floor than for our walls. Since we want all the tiles (including zero) drawn for our floor, we need to pass in these extra values.The only one that’s a little weird is the last:
uint.MAX_VALUE. Every tile number (zero through the number of tiles in our image) that is above the number passed at this parameter will be marked for collisions. Everything below this number will ignore collisions by default. So, if you had a wall that the player could walk through, you could put it at the end of your image (a high index) and use this value to have Flixel ignore collisions. Since we never want any collisions to happen with the floor, we useuint.MAX_VALUEbecause every tile index will be below this value and will therefore not have collisions.Finally, we have to remember to add our tilemaps to a group or they won’t show up on the screen. Before we can run the project, though, we need to load in our artwork.
Step 6: Loading Assets
Since we’re using images, we need to let Flash know about them. One of the more straightforward ways to do this is by embedding them in your SWF. Here’s how we’re doing that in this project (found in
tutorial/Assets.as).package tutorial { import flash.utils.ByteArray; /** * Embeds and imports all assets for the game * @author Cody Sandahl */ public class Assets { // sprites [Embed(source = "../../assets/sprites/ranger (opengameart - Antifarea - ccby30).png")] public static var RANGER_SPRITE:Class; [Embed(source = "../../assets/sprites/rug1 (opengameart - Redshrike - ccby30).png")] public static var RUG1_SPRITE:Class; [Embed(source = "../../assets/sprites/rug2 (opengameart - Redshrike - ccby30).png")] public static var RUG2_SPRITE:Class; [Embed(source = "../../assets/sprites/bookcase (opengameart - Redshrike - ccby30).png")] public static var BOOKCASE_SPRITE:Class; [Embed(source = "../../assets/sprites/chair_down (opengameart - Redshrike - ccby30).png")] public static var CHAIRDOWN_SPRITE:Class; [Embed(source = "../../assets/sprites/chair_left (opengameart - Redshrike - ccby30).png")] public static var CHAIRLEFT_SPRITE:Class; [Embed(source = "../../assets/sprites/chair_right (opengameart - Redshrike - ccby30).png")] public static var CHAIRRIGHT_SPRITE:Class; [Embed(source = "../../assets/sprites/chair_up (opengameart - Redshrike - ccby30).png")] public static var CHAIRUP_SPRITE:Class; [Embed(source = "../../assets/sprites/table_round (opengameart - Redshrike - ccby30).png")] public static var TABLEROUND_SPRITE:Class; [Embed(source = "../../assets/sprites/armor (opengameart - Redshrike - ccby30).png")] public static var ARMOR_SPRITE:Class; [Embed(source = "../../assets/sprites/bed (opengameart - Redshrike - ccby30).png")] public static var BED_SPRITE:Class; // tiles [Embed(source = "../../assets/tiles/walls (opengameart - daniel siegmund - ccby30).png")] public static var WALLS_TILE:Class; [Embed(source = "../../assets/tiles/floor_wood (opengameart - Redshrike - ccby30).png")] public static var FLOORS_TILE:Class; } }I’m giving you all the artwork at once, because it’s not all that complicated once you get the hang of it. Let’s take a look at the highlighted lines. Here we are loading in two images: one for our walls and one for our floor. If you remember in the last step, we told Flixel to use
Assets.WALLS_TILEandAssets.FLOORS_TILEwhen loading in the map layers. This is where we define those variables.Notice that we use a path relative to the
Assets.asfile. You can also embed things like XML files, SWF files, and a ton of other assets. All we need, however, are images. For more information on embedding assets in Flash, check out this article from the Nightspade blog.Now that we have our images embedded and accessible, we can tell
PlayState.asto use our newfangled level./** * Create state */ override public function create():void { FlxG.mouse.show(); // load level LEVEL = new IndoorHouseLevel(this, LEVEL_SIZE, BLOCK_SIZE); this.add(LEVEL); }We changed the highlighted line from using
TopDownLevelto using our newIndoorHouseLevel. Now if you run the project you should see something that looks a bit more like a room.Step 7: Adding Objects and Eye Candy
It might be a room, but it’s a boring room. Let’s spruce it up a bit with some furniture. First, we need some more groups and some variables inside
IndoorHouseLevel.decalGroupwill allow us to add some rugs (purely visual eye candy), whileobjectGroupwill allow us to add some furniture that will get in the player’s way. The other variables are the pieces of furniture we will be adding in a moment.Next, we need to add these objects to the level. We’re adding the highlighted line and everything beneath it.
/** * Create the map (walls, decals, etc) */ override protected function createMap():void { var tiles:FlxTilemap; // floors tiles = new FlxTilemap(); tiles.loadMap( FlxTilemap.arrayToCSV(FLOORS, 15), // convert our array of tile indices to a format flixel understands Assets.FLOORS_TILE, // image to use tileSize.x, // width of each tile (in pixels) tileSize.y, // height of each tile (in pixels) 0, // don't use auto tiling (needed so we can change the rest of these values) 0, // starting index for our tileset (0 = include everything in the image) 0, // starting index for drawing our tileset (0 = every tile is drawn) uint.MAX_VALUE // which tiles allow collisions by default (uint.MAX_VALUE = no collisions) ); floorGroup.add(tiles); // walls // FFV: make left/right walls' use custom collision rects tiles = new FlxTilemap(); tiles.loadMap( FlxTilemap.arrayToCSV(WALLS, 15), // convert our array of tile indices to a format flixel understands Assets.WALLS_TILE, // image to use tileSize.x, // width of each tile (in pixels) tileSize.y // height of each tile (in pixels) ); wallGroup.add(tiles); // objects createObjects(); } /** * Add all the objects, obstacles, etc to the level */ protected function createObjects():void { var sprite:FlxSprite; // create custom groups decalGroup = new FlxGroup(); objectGroup = new FlxGroup(); // decals (decorative elements that have no functionality) sprite = new FlxSprite( 16, // x location 16, // y location Assets.RUG1_SPRITE // image to use ); decalGroup.add(sprite); sprite = new FlxSprite( 11 * tileSize.x, // x location (using tileSize to align it with a tile) 1.5 * tileSize.y, // y location (showing that you don't need to line up with a tile) Assets.RUG2_SPRITE // image to use ); decalGroup.add(sprite); // objects and obstacles // NOTE: this group gets tested for collisions bookcase = new FlxSprite( 32, // x location 0, // y location (showing that you can overlap with the walls if you want) Assets.BOOKCASE_SPRITE // image to use ); bookcase.immovable = true; // don't allow the player to move this object objectGroup.add(bookcase); table = new FlxSprite(192, 192, Assets.TABLEROUND_SPRITE); table.immovable = true; objectGroup.add(table); sprite = new FlxSprite(176, 192, Assets.CHAIRRIGHT_SPRITE); sprite.immovable = true; objectGroup.add(sprite); sprite = new FlxSprite(216, 192, Assets.CHAIRLEFT_SPRITE); sprite.immovable = true; objectGroup.add(sprite); armor = new FlxSprite(192, 0, Assets.ARMOR_SPRITE); armor.immovable = true; objectGroup.add(armor); bed = new FlxSprite(16, 192, Assets.BED_SPRITE); bed.immovable = true; objectGroup.add(bed); }I’m using an extra function,
createObjects(), simply to keep things easier to read. The comments explain each individual object, but let me offer a few general observations. First, we always need to remember to calladd()for each object or it won’t get displayed. In addition, we need to use the right group (mapGroup,floorGroup,decalGroup,objectGroup, etc.) when calling add() or it will mess up our render order and our collision detection.Also take notice of all the various ways we can decide where to place our objects and decals. We can hard code the values (like we do with the first rug), we can use
tileSizeto align it with the floor and wall tiles (like we do with the second rug), and we can mix and match to our heart’s content. Just know that Flixel won’t detect it if we place something off the level or overlapping another object – it assumes we know what we’re doing.Now we need to display our new groups in the right order and handle collisions. Add these functions to the bottom of
IndoorHouseLevel./** * Decide the order of the groups. They are rendered in the order they're added, so last added is always on top. */ override protected function addGroups():void { add(floorGroup); add(wallGroup); add(decalGroup); add(objectGroup); add(player); add(guiGroup); } /** * Update each timestep */ override public function update():void { super.update(); // NOTE: map -> player collision happens in super.update() FlxG.collide(objectGroup, player); }Since we want our new groups to render on top of the floors and walls, we need to completely re-do the
addGroups()function that we had inTopDownLevel. We also need to add collision detection for our furniture inobjectGroup. Once again, since we don’t callFlxG.collide()fordecalGroup, the player won’t be stymied by our imposing rugs. Now our room is looking a little less vacant.Step 8: Creating Our Player
I keep talking about collisions, but it’s hard to collide with an immobile red box. Over the next three steps we will add keyboard controls to our red box before finally making it a proper animated sprite. Let’s create
tutorial/Player.as.package tutorial { import org.flixel.*; import topdown.*; /** * Player-controlled entity * @author Cody Sandahl */ public class Player extends TopDownEntity { /** * Constructor * @param X X location of the entity * @param Y Y location of the entity */ public function Player(X:Number=100, Y:Number=100):void { super(X, Y); } } }This is the skeleton we’ll be using to flesh out a more interesting player. Now that we have our custom player, we need to use it in
IndoorHouseLevel. Add this function at the end of the class./** * Create the player */ override protected function createPlayer():void { player = new Player(playerStart.x, playerStart.y); }This changed from using
TopDownEntityto usingPlayer. Now let’s make this red box move around.Step 9: Adding Keyboard Controls
Since we might want entities other than Player to be able to move, we’re going to add some functionality to
TopDownEntity. Here’s the new version.package topdown { import org.flixel.*; /** * A moveable object in the game (player, enemy, NPC, etc) * @author Cody Sandahl */ public class TopDownEntity extends FlxSprite { /** * Constants */ public static const SIZE:FlxPoint = new FlxPoint(16, 18); // size in pixels public static const RUNSPEED:int = 80; /** * Constructor * @param X X location of the entity * @param Y Y location of the entity */ public function TopDownEntity(X:Number = 100, Y:Number = 100):void { super(X, Y); makeGraphic(SIZE.x, SIZE.y, 0xFFFF0000); // use this if you want a generic box graphic by default // movement maxVelocity = new FlxPoint(RUNSPEED, RUNSPEED); drag = new FlxPoint(RUNSPEED * 4, RUNSPEED * 4); // decelerate to a stop within 1/4 of a second } /** * Update each timestep */ public override function update():void { updateControls(); super.update(); } /** * Check keyboard/mouse controls */ protected function updateControls():void { acceleration.x = acceleration.y = 0; // no gravity or drag by default } /** * Move entity left */ public function moveLeft():void { facing = LEFT; acceleration.x = -RUNSPEED * 4; // accelerate to top speed in 1/4 of a second } /** * Move entity right */ public function moveRight():void { facing = RIGHT; acceleration.x = RUNSPEED * 4; // accelerate to top speed in 1/4 of a second } /** * Move entity up */ public function moveUp():void { facing = UP; acceleration.y = -RUNSPEED * 4; // accelerate to top speed in 1/4 of a second } /** * Move playe rdown */ public function moveDown():void { facing = DOWN; acceleration.y = RUNSPEED * 4; // accelerate to top speed in 1/4 of a second } } }We’ve added a new constant,
RUNSPEED, that determines how quickly our entities move. Then we setmaxVelocityanddrag(deceleration) in our constructor. After that, we callupdateControls()each frame so we can check for keyboard, mouse, or AI (depending on our needs). Finally, we add some helper functions for moving in each direction. Notice that we updatefacingin each of these. This is a handy way to know which animation to use later down the line.Now we need to actually use the keyboard inside
Player. Add this function after the constructor./** * Check for user input to control this entity */ override protected function updateControls():void { super.updateControls(); // check keys // NOTE: this accounts for someone pressing multiple arrow keys at the same time (even in opposite directions) var movement:FlxPoint = new FlxPoint(); if (FlxG.keys.pressed("LEFT")) movement.x -= 1; if (FlxG.keys.pressed("RIGHT")) movement.x += 1; if (FlxG.keys.pressed("UP")) movement.y -= 1; if (FlxG.keys.pressed("DOWN")) movement.y += 1; // check final movement direction if (movement.x < 0) moveLeft(); else if (movement.x > 0) moveRight(); if (movement.y < 0) moveUp(); else if (movement.y > 0) moveDown(); }So every frame we check what keys are being pressed. Flixel allows us to test keys in different ways. Here we’re using
pressed(), which istruefor as long as the key is being held down. If we usedjustPressed(), it would only betrueimmediately after the player presses the key, even if the key is held down after that. That would be reversed if we usedjustReleased().As I state in the comments, I want to handle the case where the user is pressing left and right (for example) at the same time by not moving. Incrementing or decrementing
movement.xbased on which arrow is pressed allows us to do that becausemovement.xwould be zero if both left and right were being pressed.If you run the project now, you should be able to move the red box around with the arrow keys and see collisions happen between the box, and the walls or furniture (but not the rugs).
You’ll probably notice that the box doesn’t go all the way over to the left and right walls. This is a more esoteric aspect of Flixel. Flixel uses rather simple collision detection (but allows us to make it more complicated if we want to). Since the images we’re using for all the walls are the same size (16×16), Flixel uses 16×16 as the collision size even though most of the left and right wall images are transparent. Fixing that behavior is beyond the scope of this tutorial, but it can be done.
Step 10: Adding Animations
I promised we wouldn’t stick with the red box (endearing though it is), so here we go with an animated sprite. Since we probably want the ability to animate future entities, we will be adding the basic functionality to
TopDownEntityinstead ofPlayer. Here are the new constructor,createAnimations(), andupdate()functions forTopDownEntity./** * Constructor * @param X X location of the entity * @param Y Y location of the entity */ public function TopDownEntity(X:Number = 100, Y:Number = 100):void { super(X, Y); makeGraphic(SIZE.x, SIZE.y, 0xFFFF0000); // use this if you want a generic box graphic by default // movement maxVelocity = new FlxPoint(RUNSPEED, RUNSPEED); drag = new FlxPoint(RUNSPEED * 4, RUNSPEED * 4); // decelerate to a stop within 1/4 of a second // animations createAnimations(); } /** * Create the animations for this entity * NOTE: these will be different if your art is different */ protected function createAnimations():void { addAnimation("idle_up", [1]); addAnimation("idle_right", [5]); addAnimation("idle_down", [9]); addAnimation("idle_left", [13]); addAnimation("walk_up", [0, 1, 2], 12); // 12 = frames per second for this animation addAnimation("walk_right", [4, 5, 6], 12); addAnimation("walk_down", [8, 9, 10], 12); addAnimation("walk_left", [12, 13, 14], 12); addAnimation("attack_up", [16, 17, 18, 19], 12, false); // false = don't loop the animation addAnimation("attack_right", [20, 21, 22, 23], 12, false); addAnimation("attack_down", [24, 25, 26, 27], 12, false); addAnimation("attack_left", [28, 29, 30, 31], 12, false); } /** * Update each timestep */ public override function update():void { updateControls(); updateAnimations(); super.update(); }FlxSpriteassumes that, if we’re animating, we have multiple frames of animation stored in a single image (called a sprite sheet). While our red box doesn’t have frames to animate, the artwork we will be using does. If you use artwork that is arranged differently in your own game, you would need to change these frame numbers. Additionally, if you look at the idle animations you’ll notice that we need to pass in an array of frame indices even if we only have one.Here’s the ranger sprite sheet we’ll be using for our player just so you can see a little more clearly.
Note that we included some blank frames in the sprite sheet. This was mainly for convenience in editing the animations since our bottom four animations (attacking) have one more frame than our top four animations (walking). Also notice that we are using the middle frame from each of the walking animations as our idle animation.
As long as all of our entities in our game use these same frame numbers, we never have to change the animation code. If our artwork used a different number of frames to animate walking and attacking, we would have updated the frames passed into
addAnimation()accordingly. Since the animations are in their own function –createAnimations(), you can also override this function to make some entities have different animations than the rest.We also made another new function to show the right animation:
updateAnimations()./** * Based on current state, show the correct animation * FFV: use state machine if it gets more complex than this */ protected function updateAnimations():void { // use abs() so that we can animate for the dominant motion // ex: if we're moving slightly up and largely right, animate right var absX:Number = Math.abs(velocity.x); var absY:Number = Math.abs(velocity.y); // determine facing if (velocity.y < 0 && absY >= absX) facing = UP; else if (velocity.y > 0 && absY >= absX) facing = DOWN; else if (velocity.x > 0 && absX >= absY) facing = RIGHT; else if (velocity.x < 0 && absX >= absY) facing = LEFT // up if (facing == UP) { if (velocity.y != 0 || velocity.x != 0) play("walk_up"); else play("idle_up"); } // down else if (facing == DOWN) { if (velocity.y != 0 || velocity.x != 0) play("walk_down"); else play("idle_down"); } // right else if (facing == RIGHT) { if (velocity.x != 0) play("walk_right"); else play("idle_right"); } // left else if (facing == LEFT) { if (velocity.x != 0) play("walk_left"); else play("idle_left"); } }This is more laborious than it is complicated. Basically we are calculating how much we’re moving vertically and horizontally. Whichever has more movement, we use that direction’s animation. This would come into play if you are moving at an angle and suddenly bump into something. Whichever direction you can still move in will determine the animation used.
We have only one more thing to do before we can finally put the red box out of its misery. We need to tell
Playerto use the ranger sprite sheet./** * Constructor * @param X X location of the entity * @param Y Y location of the entity */ public function Player(X:Number=100, Y:Number=100):void { super(X, Y); loadGraphic( Assets.RANGER_SPRITE, // image to use true, // animated false, // don't generate "flipped" images since they're already in the image TopDownEntity.SIZE.x, // width of each frame (in pixels) TopDownEntity.SIZE.y // height of each frame (in pixels) ); }Once again we are going to our
Assetsclass to pull in the image we want. The comments tell you what’s going on, but let me tell you a bit about "flipped" images. Instead of generating different animations when travelling left/right and up/down, Flixel can just flip the "right" animation to make it "left" and flip the "up" animation to make it "down" (or vice versa). Our "up" and "down" animations look very different (and we already have the artwork with all the directions), so we tell Flixel not to bother with flipping the animations.Now we have a true indoor top-down RPG level!
Step 11: Adding a GUI
As a bonus, let’s see how to add GUI elements to the screen. We’re going to add a simple set of instructions at the top so users know what to do once they load up this level. Let’s add a GUI to
IndoorHouseLevel./** * Create text, buttons, indicators, etc */ override protected function createGUI():void { var instructions:FlxText = new FlxText(0, 0, levelSize.x, "Use ARROW keys to walk around"); instructions.alignment = "center"; guiGroup.add(instructions); }This adds a text area at the top of the screen that is as wide as the level and uses
centeralignment. As always, it must be added to the right group for it to show up.Conclusion
Now you have everything you need to create your own top-down RPG levels. Thank you for sticking with me and go make something fun!