Breakout

December 15th, 2008 by Jim Burrows

[kml_flashembed movie="http://www.jimburrows.net/blog/Games/Breakout.swf" height="500" width="600" /]

In this tutorial we will focus on creating a very simple, yet popular, arcade game from the past. I call it Breakout, but I've seen it called by many other names. The object is to take out all the bricks at the top of the screen by using a bar located at the bottom of the screen to deflect a ball back up to the top to take out bricks. The round ends when all bricks have been cleared. The game ends when the ball goes through the bottom of the screen. I'll start out by showing the code in full, and then I'll break it down and explain the working parts.

Full Code:

var _left, _right:Boolean=false;
var movementspeed:Number=0;
var MAXMOVEMENTSPEED:int=10;
 
var brickarray:Array = new Array();
var numberofcolumns:int=6;
var numberofrows:int=4;
var brickcount:int=0;
 
var ballmovementspeedX:Number=9+round;
var ballmovementspeedY:Number=9+round;
var ballvelocity:Number = Math.sqrt(ballmovementspeedX*ballmovementspeedX+ballmovementspeedY*ballmovementspeedY);
 
ball.x=300;
ball.y=457;
bar.x=300;
bar.y=492;
bar.width=125-((round-1)*5);
 
/***********Brick Placement************/
for(var i:int=0; i<numberofrows; i++){
	for(var j:int=0; j<numberofcolumns; j++){
		var brick:Brick = new Brick();
        container_mc.addChild(brick);
        brick.x = 30+(20+brick.width)*j;
        brick.y = 30+(5+brick.height)*i;
        brickarray.push(brick);
		brickcount++;
    }
}
 
/**************Round Start*************/
ball.stage.addEventListener(KeyboardEvent.KEY_DOWN, startBall);
function startBall(e:KeyboardEvent):void{
	if(e.keyCode==Keyboard.SPACE){
		addEventListener(Event.ENTER_FRAME, playGame);
		bar.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
		bar.stage.addEventListener(KeyboardEvent.KEY_UP, keyReleased);
		addEventListener(Event.ENTER_FRAME, moveTheBar);
	}
}
 
/********************************************************
*********Three Event Listeners For Bar Movement**********
********************************************************/
function keyPressed(e:KeyboardEvent):void{
	if(e.keyCode==Keyboard.LEFT){
		_left=true;
	}
	if(e.keyCode==Keyboard.RIGHT){
		_right=true;
	}
}
 
function keyReleased(e:KeyboardEvent):void{
	if(e.keyCode==Keyboard.LEFT){
		_left=false;
	}
	if(e.keyCode==Keyboard.RIGHT){
		_right=false;
	}
}
 
function moveTheBar(e:Event):void{
	if(_left==false &#038;&#038; _right==false){
		movementspeed/=1.1;
	}
	if(_left==true &#038;&#038; movementspeed>=MAXMOVEMENTSPEED/-1+.8){
		movementspeed-=.8;
	}
	if(_right==true &#038;&#038; movementspeed<=MAXMOVEMENTSPEED-.8){
		movementspeed+=.8;
	}
 
	if(bar.x<bar.width/2){
		bar.x=bar.width/2+.1;
		movementspeed=0;
	}
	if(bar.x>stage.stageWidth-bar.width/2){
		bar.x=stage.stageWidth-bar.width/2-.1;
		movementspeed=0;
	}
	bar.x+=movementspeed;
}
 
/***************************************************
*********************Game Play**********************
***************************************************/
function playGame(e:Event):void{
	for(var i:int=0; i<brickarray.length; i++){
		if(brickarray[i].leftBrick.hitTestObject(ball) || brickarray[i].rightBrick.hitTestObject(ball)){
			container_mc.removeChild(brickarray[i]);
			brickarray.splice(i,1);
			ballmovementspeedX/=-1;
			brickcount--;
			score+=10*round;
		}
 
		if(brickarray[i].bottomBrick.hitTestObject(ball) || brickarray[i].topBrick.hitTestObject(ball)){
			container_mc.removeChild(brickarray[i]);
			brickarray.splice(i,1);
			ballmovementspeedY/=-1;
			brickcount--;
			score+=10*round;
		}
	}
 
	if(ball.hitTestObject(bar)){
		ballmovementspeedX=(ball.x-bar.x)/(bar.width)*ballvelocity;
		ballmovementspeedY=Math.sqrt(Math.abs(ballvelocity*ballvelocity-ballmovementspeedX*ballmovementspeedX));
	}
 
	if(ball.x<ball.width/2 || ball.x>stage.stageWidth-ball.width/2){
		ballmovementspeedX/=-1;
	}
	if(ball.y<ball.height/2){
		ballmovementspeedY/=-1;
	}
 
	if(ball.y>stage.stageHeight){
		removeEventListener(Event.ENTER_FRAME, playGame);
		gotoAndStop(4);
	}
	if(brickcount==0){
		removeEventListener(Event.ENTER_FRAME, playGame);
		gotoAndStop(3);
 
	}
 
	ball.x+=ballmovementspeedX;
	ball.y-=ballmovementspeedY;
	scoreText.text = "Score: " + score;
 
}

Alright Let's break this down:

var _left, _right:Boolean=false;
var movementspeed:Number=0;
var MAXMOVEMENTSPEED:int=10;
 
var brickarray:Array = new Array();
var numberofcolumns:int=6;
var numberofrows:int=4;
var brickcount:int=0;
 
var ballmovementspeedX:Number=9+round;
var ballmovementspeedY:Number=9+round;
var ballvelocity:Number = Math.sqrt(ballmovementspeedX*ballmovementspeedX+ballmovementspeedY*ballmovementspeedY);
 

Here we declare our variables, no big surprise here. movementspeed controls our bar's speed, and ballmovementspeedX,Y controls our ball's speed, obviously. If you notice, the initial ballmovementspeed will be 10(9+round). The subsequent rounds after that will each increase the ball's speed by 1, making each round more difficult than the last. The only tricky one here is ballvelocity. If you're familiar with the pythagorean theorem, that is how ballvelocity is derived. Later on, you will see how that ensures that the ball maintains the same velocity no matter what part of the bar it hits.

 
ball.x=300;
ball.y=457;
bar.x=300;
bar.y=492;
bar.width=125-((round-1)*5);

Here, we initialize properties of both the ball and the bar. Since this is a multi round game, I want these values to be re-initialized after each round, so this code is added. Without this code, the ball and the bar would start the next round where they left off the previous round. The last line of code here is for game difficulty. The bar's width starts out at 125 in the first round, and each round after that it shrinks by 5 pixels.

 
for(var i:int=0; i<numberofrows; i++){
	for(var j:int=0; j<numberofcolumns; j++){
		var brick:Brick = new Brick();
                container_mc.addChild(brick);
                brick.x = 30+(20+brick.width)*j;
                brick.y = 30+(5+brick.height)*i;
                brickarray.push(brick);
		brickcount++;
      }
}

First off, for this to work, I had to make a movieclip of a brick. If you go to the library and right click on that movieclip, and select "properties," there is an option to Export for Actionscript. Check that box, and now we can reference that brick without ever putting it onto the stage. I gave mine a class name of "Brick." In this double for loop, every iteration will create a new Brick with the instance name of "brick." Once a brick is created, it needs to be added to the stage. That is handled with the addChild method. You can add them directly to the next open layer in the stage by just using addChild(brick), but I wanted to put them all in one movieclip, in case I ever wanted to remove them all at once. brick.x and brick.y control where each brick is placed. The brick is then pushed into our array so that it can be easily accessed later for collision detection. Lastly, the brickcount variable is incremented so that we can keep track of how many bricks are on the stage.

 
ball.stage.addEventListener(KeyboardEvent.KEY_DOWN, startBall);
function startBall(e:KeyboardEvent):void{
	if(e.keyCode==Keyboard.SPACE){
		addEventListener(Event.ENTER_FRAME, playGame);
		bar.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
		bar.stage.addEventListener(KeyboardEvent.KEY_UP, keyReleased);
		addEventListener(Event.ENTER_FRAME, moveTheBar);
	}
}

This is pretty self explanatory as well. I wanted the ball to start moving when the Spacebar is hit, so I added a listener for that. When the spacebar is hit, all of the other listeners are then invoked. This presents the user from doing anything until the spacebar is clicked.

 
function keyPressed(e:KeyboardEvent):void{
	if(e.keyCode==Keyboard.LEFT){
		_left=true;
	}
	if(e.keyCode==Keyboard.RIGHT){
		_right=true;
	}
}
function keyReleased(e:KeyboardEvent):void{
	if(e.keyCode==Keyboard.LEFT){
		_left=false;
	}
	if(e.keyCode==Keyboard.RIGHT){
		_right=false;
	}
}

Now I'll go over each of the three bar movement functions. The first two just "listen" for when the left and right arrows are pressed down or released. Our two boolean values, _left and _right, are set accordingly. The last function has all the controls, we'll break that down in pieces:

 
function moveTheBar(e:Event):void{
	if(_left==false &#038;&#038; _right==false){
		movementspeed/=1.1;
	}

This if statement checks if none of the keys are being pressed. When that is true, the movementspeed of the bar is continually divided by 1.1. This gives it that "slowing down" effect when no keys are pressed.

 
	if(_left==true &#038;&#038; movementspeed>=MAXMOVEMENTSPEED/-1+.8){
		movementspeed-=.8;
	}

This IF statement is executed when the left arrow key is held down. For each frame that the left arrow is held down, movementspeed is decremented by .8. This way, the bar doesnt automatically stop and move full speed to the left when the left arrow is down. If the bar is quickly going to the right, it will first slow down, stop, and then accelerate to the left.

 
	if(_right==true &#038;&#038; movementspeed<=MAXMOVEMENTSPEED-.8){
		movementspeed+=.8;
	}

Same thing as with the left.

if(bar.x<bar.width/2){
		bar.x=bar.width/2+.1;
		movementspeed=0;
	}

This IF statement makes sure the bar can't go off the stage to the left. When it reaches a certain point, it's like hitting a wall, and the movementspeed is set to 0. Adding the .1 is very important here, otherwise the bar would just be stuck on the left wall forever. By adding the .1, it basically "pushes" the bar off the wall.

if(bar.x>stage.stageWidth-bar.width/2){
		bar.x=stage.stageWidth-bar.width/2-.1;
		movementspeed=0;
	}

Same thing we did for the left wall, we do for the right wall.

bar.x+=movementspeed;
}

In this one line, we tell the bar to constantly be moving at a rate of "movementspeed." If movement speed is positive, the bar will move to the right. If it's negative, to the left.

Lastly is our giant Game Play function. I will break that up into pieces:

 
function playGame(e:Event):void{
	for(var i:int=0; i<brickarray.length; i++){
		if(brickarray[i].leftBrick.hitTestObject(ball) || brickarray[i].rightBrick.hitTestObject(ball)){
			container_mc.removeChild(brickarray[i]);
			brickarray.splice(i,1);
			ballmovementspeedX/=-1;
			brickcount--;
			score+=10*round;
		}

Before I begin, I must add that within the movieclip of each brick, I have 4 invisible movieclips, named leftBrick, rightBrick, bottomBrick, topBrick. These are basically 4 lines that go around the perimeter of the brick, and are used for collision detection. So in this first IF statement, I check for when the ball has collided with the left or right side of one of the bricks. Since this is an ENTER_FRAME listener, the for loop is constantly searching through the brickarray for when one of of its bricks is hit by the ball. When this occurs, the brick is first removed. The next line of code is very important. Since a brick was removed from our array, we don't want there to be an open spot in the brick array that our for loop will be checking. The splice method will take care of that. It takes in two parameters. The first tells where at in brickarray to start removing from, and the second parameter is for how many to remove from that starting point. We only want that one brick to be removed, so our position is "i" and only 1. This will shift the positions of the array that were above that down, and decrement brickarray.length by one. Next, we want the ball to change directions, so by dividing it by negative one, we keep the same speed, just in a different direction. brickcount is then decremented so we can keep track of how many bricks are left on the stage. Lastly, our score is updated.

 
if(brickarray[i].bottomBrick.hitTestObject(ball) || brickarray[i].topBrick.hitTestObject(ball)){
			container_mc.removeChild(brickarray[i]);
			brickarray.splice(i,1);
			ballmovementspeedY/=-1;
			brickcount--;
			score+=10*round;
		}
	}

This IF statement is pretty much the same as the previous one, however this one checks for collision between the ball and the bottom and top of the brick. The only difference here is that the Y direction is reversed, instead of the X direction.

 
	if(ball.hitTestObject(bar)){
		ballmovementspeedX=(ball.x-bar.x)/(bar.width)*ballvelocity;
		ballmovementspeedY=Math.sqrt(Math.abs(ballvelocity*ballvelocity-ballmovementspeedX*ballmovementspeedX));
	}

In this IF statement, we check for when the ball hits the bar. The first line of code checks where at on the bar the ball hit, and accordingly sends the ball off in that direction. That same line of code is basically getting a percentage of the ballvelocity based on what part of the bar it hit. The further away from the center of the bar the ball hits, the higher the ballmovementspeedX will be. The second line of code takes into effect our ballvelocity that we declared from the top. Once again, we use the pythagorean theorem to determine the ballmovementspeedY. This maintains the same ball velocity through the entire round, no matter what part of the bar the ball hits.

 
	if(ball.x<ball.width/2 || ball.x>stage.stageWidth-ball.width/2){
		ballmovementspeedX/=-1;
	}
	if(ball.y<ball.height/2){
		ballmovementspeedY/=-1;
	}

These two IF statements check when the ball has "hit" either the left, right, or top walls of the game. When this occurs, the balls direction is changed accordingly.

 
	if(ball.y>stage.stageHeight){
		removeEventListener(Event.ENTER_FRAME, playGame);
		gotoAndStop(4);
	}

Just like the previous IF statements, this one checks if the ball has "hit" the bottom wall of the game. However, in this case, the game is over. I still it go to and stop on frame 4 of my movie, where I have a little Game Over screen.

 
	if(brickcount==0){
		removeEventListener(Event.ENTER_FRAME, playGame);
		gotoAndStop(3);
	}

Here we check for when all the bricks are gone. In this case, the round is over, and we can continue to Frame 3 where we have a "Round Over" screen. We have to remove our event listener for playGame, otherwise the .swf will still be continuously checking for all this IF conditions. But don't worry, it's added back on at the beginning of each round.

 
	ball.x+=ballmovementspeedX;
	ball.y-=ballmovementspeedY;

Similar to our bar movement, this just continuously updates the position of our ball.

 
scoreText.text = "Score: " + score;

Last, but certainly not least, our score is updated. I created a dynamic textbox at the top of the stage that I gave an instance name of "scoreText" to. Make sure if you're using dynamic text boxes that you embed whatever font you're using.

Side Note: On frame 1 of my .fla, I declared these two variables:

var round:int=1;
var score:int=0;

I declared these on frame one, because I didn't want them to be reinitialized at the beginning of each round. They will only get reinitialized when a new game is started.

Please, leave me any questions or comments about this. I am open to all criticisms. If you have a request to add some other functionality to it, let me know.

One Response to “Breakout”

  1. Johnny Says:

    Movement is way to slow.

Leave a Reply

RSS Feed