Perspectives

Ball

Every once in a while we are asked to make a game for a client. I recently worked on a game that centered—as many games do—around a bouncing ball. I thought this would be a good opportunity to put together the first Killswitch multi-part flash tutorial. In this tutorial we'll build a simple but highly customizable billiards game. Credit goes to Tonypa's vector tutorial for some of the concepts and techniques on which this game is based. There is also a helpful article on Euclidean vectors on Wikipedia.

In this first installment we'll get things started with vectors—the basic building block of the game—and get a ball bouncing around inside a confined space.

So, What's a Vector?

This isn't drawing with vectors that we're talking about here. This is physics. A vector is essentially a point (x and y coordinates) and a direction. It can be used to describe the position, direction and velocity of an object, describe forces like wind or gravity, describe obstacles like walls, determining collisions and so on.

For this game I've created a Vector class which has a number of useful methods that are helpful for manipulating vectors. Ultimately they boil down to things like the pythagorean theorem, sine and cosine—trigonometry stuff.

The constructor takes 4 attributes: x and y coordinates, deltaX (horizontal movement component) and deltaY (vertical movement component).

var myVector:Vector = new Vector(1, 1, 4, 3);

Vector Playground

Let's try it out. Download a finished version of this movie for reference if you like.

You'll want to download the Vector.as file.

Create a new flash (AS3) project. Name it something like “vectorPlayground.fla”. Save the project in the same directory as Vector.as or be sure to set the classpath to the directory of Vector.as (in publish settings). Directions on how to do this can be found in Adobe's Flash documentation.

Create a Sprite called “canvas” and add it to the stage. We'll use the graphics package to draw the vectors.

var canvas:Sprite = new Sprite();
addChild(canvas);

Write a function for drawing vectors:

function drawVector(vector:Vector, color:uint):void{
	canvas.graphics.lineStyle(1, color);
	canvas.graphics.moveTo(vector.x, vector.y);
	canvas.graphics.lineTo(vector.x + vector.deltaX, vector.y + vector.deltaY);
}

This function takes a vector and a color for the lines it will draw.

Now create a vector and draw it on the canvas:

var myVector:Vector = new Vector(10, 10, 50, 300);
drawVector(myVector, 0x3F3F39);

try out some of the Vector methods:

// set the vector's angle in degrees
myVector.setAngle(45);
drawVector(myVector, 0xFF55BB); // a pink line

// set the vector's length
myVector.setLength(50); 
drawVector(myVector, 0x5555FF); // a short blue line drawn on top of the pink one

// resets the vectors length and angle respectively
myVector.setPolar(200, 10);
drawVector(myVector, 0x55FF55); // a green line

// the getIntersection() method is used to find the point where two vectors intersect. 
var vector2:Vector = new Vector(100, 10, -20, 200);
drawVector(vector2, 0xDD2200); // a dark red line
var intersection:Point = vector2.getIntersection(myVector);
// draw a circle at the intersection point (x, y, radius)
canvas.graphics.drawCircle(intersection.x, intersection.y, 2);

Ok, enough of that for now. It's time start building the pool game.

The Game

Download a finished version of this movie for reference if you like. Preview the finished movie.

Let's start with a clean FLA. Call it “poolParty.fla”. Create a folder named “classes” in the same directory as pollParty.as and put Vector.as in there. Make sure that you set the classpath to the classes directory. Set the size of the document to 250x380. Set the background color to something dark (pool table green perhaps) because we'll be playing with the (white) cue ball to start.

I've created a Ball class that is essentially a circle that has its own vector. In a later installment we'll spice it up a bit with some nicer graphics. This iteration of the Ball class has a method that draws a circle at the x and y coordinates of the Vector. The other method of note is moveAlongVector().

// Ball.as

public function moveAlongVector():void{
	drawMe();
	// put a cap on how fast the ball can move.
	if(vector.getLength() > radius){
		vector.setLength(radius);
	}
	vector.x += vector.deltaX;
	vector.y += vector.deltaY;
	x = vector.x;
	y = vector.y;
}

This method draws the ball at its current location and then sets the vectors coordinates to the endpoint of the vector and moves the sprite to that point. Calling this repeatedly animates the ball along a line defined by its vector.

Download Ball.as and save it in the classes folder.

Create a ball and add it to the stage. The constructor takes x and y coordinates, deltaX and deltaY, a radius and a color:

// a global radius for all balls
var radius:Number = 7;

var cueBall:Ball = new Ball(180, 50, 1, 4, radius, 0xFFFFFF); 
addChild(cueBall);

This places a ball on the stage and sets its vector pointing down and to the right. The Ball constructor function draws the ball for us with a call to the private method drawMe().

Let's make it move. In poolParty.fla set the frame rate to something high like 60fps. Write an event listener to move the ball along its vector:

addEventListener(Event.ENTER_FRAME, updateMovement);

function updateMovement(e:Event):void{
	cueBall.moveAlongVector();
}

Test this and you should see the ball glide off the table.

Create some borders for the table by making vectors that run along the edges of the stage. I set a margin variable (roughly the diameter of the ball) to set off the borders so there will be room to add in pockets later.

var margin:Number = 16;

// borders
var top:Vector = new Vector(margin, margin, 250 - margin*2, 0);
var right:Vector = new Vector(250 - margin, margin, 0, 380 - margin*2);
var bottom:Vector = new Vector(margin, 380 - margin, 250 - margin*2, 0);
var left:Vector = new Vector(margin, margin, 0, 380 - margin*2);
var walls:Array = [top, right, bottom, left];

You may find it reassuring to know where your vectors are. Feel free to draw them in using the drawVector() function that we used earlier. Don't forget to add the canvas first: 

var canvas:Sprite = new Sprite();
canvas.graphics.lineStyle(1, 0x000000);
addChild(canvas);

for each(var w in walls){
	drawVector(w, 0xFF00EE);
}

In our updateMovement() function we'll want to add something to check for collisions. Because a ball might hit more than one thing at a time (other balls for example) we'll call this function determineFinalBounce() and pass it the ball and walls as arguments:

function updateMovement(e:Event):void{
	cueBall.moveAlongVector();
	determineFinalBounce(cueBall, walls);
}

The determineFinalBounce() function itself looks like this:

function determineFinalBounce(ball:Ball, obstacles:Array):void{
	var vector = ball.getVector();
	var radius = ball.getRadius();
	var willBounce:Boolean = false; 

	// check walls for collisions and bounce accordingly
	for each(var o in obstacles){
		var vectorToObstacle:Vector = shortestDistance(vector, o);
		if(vectorToObstacle.getLength() <= radius){
			bounce(vector, vectorToObstacle, radius);
		}
	}
}

This function checks all of the obstacles (walls) to see if the ball is close enough to any wall such that it should bounce off of it.

You'll notice that there are two functions missing from the picture still: shortestDistance() and bounce(). Let's look at shortestDistance() first.

shortestDistance() is used to figure out the point of the wall that is closest to the ball.

function shortestDistance(v1:Vector, v2:Vector):Vector{
	// vector from start of v2 to start of v1
	var v2ToV1:Vector = new Vector(v2.x, v2.y, v1.x - v2.x, v1.y - v2.y);
	// create vector from ball to closest part of v2
	var shortest:Vector = v2ToV1.project(v2.normalLeft());
	// move start of shortest to start of v1.
	shortest.x = v1.x;
	shortest.y = v1.y;
	// rotate shortest 180°
	shortest.deltaX *= -1;
	shortest.deltaY *= -1;

	return shortest;
}

shortestDistance() uses two Vector methods that we haven't seen before:

normalLeft() returns a vector that runs perpendicular to the given vector. Think of it as the same vector rotated 90° counterclockwise.

project() returns a vector that is a “projection” of one vector onto another. Imagine one vector casting a shadow onto the other.

The whole function looks something like this if we drew all of the vectors involved line by line:

This diagram requires flash.

Get Adobe Flash player

if the length of the vector returned by shortestDistance() is less than the radius of the ball, then we know that it has come in contact with the wall. Once we have figured out which wall the ball has most immediately come into contact with, we need to make it bounce off of that wall.

bounce() takes the vector of the ball, shortest (the vector representing the shortest distance between the ball and the wall), and the ball's radius as arguments.

// given a vector and the shortest distance between vector's endpoint and an obstacle
// move the vector back to the point where it makes contact (assuming a radius), 
// and reflect its path.
function bounce(v1:Vector, v2:Vector, radius:Number):void{
	// the amount and direction to back up
	var reverse:Vector = new Vector(v2.x, v2.y, -v2.deltaX, -v2.deltaY);
	reverse.setLength( (radius+1) - v2.getLength() );
	var reverseEP:Point = reverse.getEndpoint();

	//surface off of  which reflection will occur
	var surface:Vector = v2.normalLeft();
	var proj1:Vector =  v1.project(v2);
	var proj2:Vector = v1.project(surface);
	proj1.deltaX *= -1;
	proj1.deltaY *= -1;
	var reflection:Vector = proj1.sum(proj2);
	reflection.x = reverseEP.x;
	reflection.y = reverseEP.y;

	if(proj1.dotProduct(v2) > 0){
		reflection.deltaX = v1.deltaX;
		reflection.deltaY = v1.deltaY;
	}
	v1.resetFromVector(reflection);
}

This function does the following:

It finds the point at which the ball makes contact with the wall. It does this by creating a vector reverse, starting at the center of the ball and at an angle of 180° from the balls current vector. The length is set to the difference between the radius and the shortest distance to the wall. The endpoint of reverse is the point where the ball is when it makes contact with the wall.

Then the ball's vector is reflected at an angle found by summing the projection of the ball's vector onto a line parallel to the wall, and the projection of the ball's vector onto a line perpendicular to the wall. sum() is a method of Vector that simply adds the deltaX and deltaY components of two vectors returning a vector with an angle and length somewhere between the two.

There are two other new methods here. The first is dotProduct() which returns a number. I won't go into the details of how the dot product is found. The important part is that if the dot product of two vectors is positive it means that they are pointing toward each other, if it is negative then they are pointing away. bounce() checks the dot product of v2 (shortest) and the reversed projection of v1 onto v2. If it is greater than 0 then we know that the ball was moving away from the wall to begin with, and that reflecting it would be a bad idea because it would turn the ball back toward the wall, which would result in unnatural behavior.

The function looks something like this:

This diagram requires flash.

Get Adobe Flash player

Add the shortestDistance() and bounce() functions to poolParty.fla and test it out. You should see your ball bouncing around the screen like an old-school screen saver.

Now, I know someone is asking “Hang on, why do we need to mess around with all of these vectors? Why not just check the ball's coordinates against the borders of the table and tell it to bounce back when it's gone too far? That would be a lot easier.” That is true, it would be easier and it would work. But the vector technique allows for borders that are not strictly horizontal or vertical. It allows the game designer to build a table with 3, 5, 6 or however many sides they want. Go ahead and try it. Maybe you want to rotate the table a bit or give it a three-dimensional feel with some pseudo-perspective. We will also be using the vectors to determine the correct angle of bouncing when two or more balls collide with another.

That does it for this stage of the the tutorial. Next time we'll add some more balls and smack 'em around.




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites