8 Ball Pool
Description: Learn how to create an 8 ball pool game
Author: John Bezanis
Added: July 9th 2008
Version: Flash 8
Pool is an interesting project for flash because it demonstrates multiple objects interacting with each other, as well as being a fun game to create and play. This tutorial assumes some experience with drawing on the stage, symbols, and basic actionscript knowledge.
We start by creating a new document of dimensions 600x400. Draw a pool table on the stage. One method to assure the table is symmetrical is to draw one quadrant of the table and then duplicate it.

Once the table is drawn, create a new layer and draw a rectangle to fit the area of the green (the inner bounds of the bumpers). Select the rectangle and right click -> convert to symbol. Name this area hitarea and in the properties tab, set the instance name to tablearea. If it isn't exactly the size of the inner square, you can reshape it now. In the properties tab, set Color->Alpha to 0% to make the hitarea box invisible.

Next, we will create the balls. Start by drawing a circle. Set the height and width to 20. Right click -> convert to symbol and name it balls. Set the linkage identifier to balls. Open the symbol and move it to position -10,-10 to center it. In the actions tab, type stop(); to keep it at this frame.

Alter the graphic so it looks like a 1 ball. At frame 2 on the timeline, right click -> insert keyframe. Change this graphic to appear like a 2 ball. Repeat through the 15 ball, and at frame 16, draw a cue ball. Go back to the main stage and delete the ball off of the stage.

Next up, draw the poolstick. Draw the poolstick and convert it to a movie clip. Name it poolstick and give it the linkage identifier poolstick. Set the Y position to 0 and make sure it is centered along the x axis. Go back into the main stage and delete it from the screen.

The image tray is two parts. First, draw a rectangle. Next, drag the movieclip hit area from the library onto the stage and resize it so it is the size of the rectangle. Set the alpha to 0 and give it the instance name trayarea.

Finally, we create a rack em button. Just draw a graphic, convert it to symbol, set the type to button, and set the instance name to newgame.
Add the following code to the main stage and your pool game is good to go:
- //Pool by John Bezanis for swfspot.com
- //Conversion for degrees to radians, calculated once for increased performance
- degreestoradians = Math.PI/180;
- //total balls sunk
- ballssunk = 0;
- //Amount of space in between balls when racked and in the tray
- spacer = .2;
- //multiply the poolstick's power by 50 percent of its distance from the ball
- powermultiplier = .5;
- //Percentage balls are slowed down at each frame
- friction = .965;
- //Determine the interval of time between checking the ball movement/collisions in milliseconds
- //a lower number gives better accuracy but is more cpu intense
- //if this number is adjusted, friction and powermultiplier should also be adjusted.
- movetime = 20;
- //Define a function to shuffle an array
- Array.prototype.shuffle = function() {
- for (i=0; i<this.length; i++) {
- this.push(this.splice(Math.floor(Math.random()*this.length), 1));
- }
- };
- //Main function that adds the balls to the table
- function newGame() {
- //Set the position of the rack
- rackx = tablearea._x+tablearea._width*.75;
- racky = tablearea._y+tablearea._height/2;
- //attach the 8 ball at the center of the rack
- this.attachMovie('balls', 'ball8', this.getNextHighestDepth(), {_x:rackx, _y:racky});
- //go to the frame with the 8 ball graphic
- ball8.gotoAndStop(8);
- //initialize the x and y veocity to 0
- ball8.xvel = 0;
- ball8.yvel = 0;
- //set the ball to not sunk
- ball8.sunk = 0;
- //create an array with all of the other balls
- ballArray = [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15];
- //shuffle the array, shuffling the position of the balls
- ballArray.shuffle();
- //calculate the horizontal difference between two rows of balls
- horizdiff = Math.sqrt(Math.pow(ball8._width+spacer, 2)-Math.pow((ball8._height+spacer)/2, 2));
- //this array contains the coordinates of the balls within the rack
- ballcoords = [[rackx-horizdiff*2, racky], [rackx-horizdiff, racky-(ball8._height+spacer)/2], [rackx-horizdiff, racky+(ball8._height+spacer)/2], [rackx, racky-(ball8._height+spacer)], [rackx, racky+(ball8._height+spacer)], [rackx+horizdiff, racky-(ball8._height+spacer)*1.5], [rackx+horizdiff, racky-(ball8._height+spacer)*.5], [rackx+horizdiff, racky+(ball8._height+spacer)*.5], [rackx+horizdiff, racky+(ball8._height+spacer)*1.5], [rackx+horizdiff*2, racky-(ball8._height+spacer)*2], [rackx+horizdiff*2, racky-(ball8._height+spacer)], [rackx+horizdiff*2, racky], [rackx+horizdiff*2, racky+(ball8._height+spacer)], [rackx+horizdiff*2, racky+(ball8._height+spacer)*2]];
- //loop through all of the balls, creating them and positioning them in the rack
- for (ballpos=0; ballpos<14; ballpos++) {
- this.attachMovie('balls', 'ball'+ballArray[ballpos], this.getNextHighestDepth(), {_x:ballcoords[ballpos][0], _y:ballcoords[ballpos][1]});
- //initialize the x and y velocity to 0
- eval('ball'+ballArray[ballpos]).xvel = 0;
- eval('ball'+ballArray[ballpos]).yvel = 0;
- //set the ball to not sunk
- eval('ball'+ballArray[ballpos]).sunk = 0;
- //change the graphic to represent the ball
- eval('ball'+ballArray[ballpos]).gotoAndStop(ballArray[ballpos]);
- }
- //attach the cue ball
- this.attachMovie('balls', 'ball0', this.getNextHighestDepth(), {_x:tablearea._x+tablearea._width/4, _y:tablearea._y+tablearea._height/2});
- //frame 16 is thge cue ball frame
- ball0.gotoAndStop(16);
- //allow the user to move the cue ball around the left quarter of the table
- scratch();
- //process the balls on the table on each interval, which its length is determined by 'movetime'
- var ballinterval:Number = setInterval(this, "moveBalls", movetime);
- }
- function moveBalls() {
- //loop through each ball, moving it according to its velocity
- for (curball=0; curball<=15; curball++) {
- //if the ball is sunk, move it along the bottom tray
- if (eval('ball'+curball).sunk) {
- //move the ball towards the right of the tray until it hits the end or another ball
- eval('ball'+curball)._x = Math.min(trayarea._x+trayarea._width-eval('ball'+curball)._width/2-eval('ball'+curball)._width*(eval('ball'+curball).sunk-1)-spacer*(eval('ball'+curball).sunk-1), eval('ball'+curball)._x+5);
- } else {
- //move the ball according to its velocity
- eval('ball'+curball)._x += eval('ball'+curball).xvel;
- eval('ball'+curball)._y += eval('ball'+curball).yvel;
- //decrease the velocity to account for friction
- eval('ball'+curball).xvel *= friction;
- eval('ball'+curball).yvel *= friction;
- //if the ball is moving very slowly, stop its movement completely
- if (Math.abs(eval('ball'+curball).xvel)<.02 && Math.abs(eval('ball'+curball).yvel)<.02) {
- eval('ball'+curball).xvel = 0;
- eval('ball'+curball).yvel = 0;
- }
- //Ball bounces off of the right bumper
- if (eval('ball'+curball)._x+eval('ball'+curball)._width/2>tablearea._x+tablearea._width) {
- //if the ball is high enough or low enough to fall into a pocket, sink it
- if (eval('ball'+curball)._y-eval('ball'+curball)._height*.75<tablearea._y || eval('ball'+curball)._y+eval('ball'+curball)._height*.75>tablearea._y+tablearea._height) {
- sinkBall(curball);
- } else {
- //reverse the velocity and adjust for the distance past the bumper
- eval('ball'+curball).xvel *= -1;
- eval('ball'+curball)._x -= (eval('ball'+curball)._x+eval('ball'+curball)._width/2-(tablearea._x+tablearea._width));
- }
- //ball bounces off of the left bumper
- } else if (eval('ball'+curball)._x-eval('ball'+curball)._width/2<tablearea._x) {
- //if the ball is high enough or low enough to fall into a pocket, sink it
- if (eval('ball'+curball)._y-eval('ball'+curball)._height*.75<tablearea._y || eval('ball'+curball)._y+eval('ball'+curball)._height*.75>tablearea._y+tablearea._height) {
- sinkBall(curball);
- } else {
- //reverse the velocity and adjust for the distance past the bumper
- eval('ball'+curball).xvel *= -1;
- eval('ball'+curball)._x -= (eval('ball'+curball)._x-eval('ball'+curball)._width/2)-tablearea._x;
- }
- }
- //ball bounces off of the bottom bumper
- if (eval('ball'+curball)._y+eval('ball'+curball)._height/2>tablearea._y+tablearea._height) {
- //if the ball is left or right enough to fall into a pocket, or in the center pocket area, sink it
- if (eval('ball'+curball)._x-eval('ball'+curball)._width*.75<tablearea._x || eval('ball'+curball)._x+eval('ball'+curball)._width*.75>tablearea._x+tablearea._width || (eval('ball'+curball)._x-eval('ball'+curball)._width*.75<tablearea._x+tablearea._width/2 && eval('ball'+curball)._x+eval('ball'+curball)._width*.75>tablearea._x+tablearea._width/2)) {
- sinkBall(curball);
- } else {
- //reverse the velocity and adjust for the distance past the bumper
- eval('ball'+curball).yvel *= -1;
- eval('ball'+curball)._y -= (eval('ball'+curball)._y+eval('ball'+curball)._height/2-(tablearea._y+tablearea._height));
- }
- //ball bounces off of the top bumper
- } else if (eval('ball'+curball)._y-eval('ball'+curball)._height/2<tablearea._y) {
- //if the ball is left or right enough to fall into a pocket, or in the center pocket area, sink it
- if (eval('ball'+curball)._x-eval('ball'+curball)._width*.75<tablearea._x || eval('ball'+curball)._x+eval('ball'+curball)._width*.75>tablearea._x+tablearea._width || (eval('ball'+curball)._x-eval('ball'+curball)._width*.75<tablearea._x+tablearea._width/2 && eval('ball'+curball)._x+eval('ball'+curball)._width*.75>tablearea._x+tablearea._width/2)) {
- sinkBall(curball);
- } else {
- //reverse the velocity and adjust for the distance past the bumper
- eval('ball'+curball).yvel *= -1;
- eval('ball'+curball)._y -= (eval('ball'+curball)._y-eval('ball'+curball)._height/2)-tablearea._y;
- }
- }
- }
- }
- //check for ball collisions
- //Check the distances between balls
- for (i=0; i<=15; i++) {
- //If the cue ball has been sunk and is draggable, do not check it for collisions
- if (ball0.onPress) {
- i++;
- }
- a = this["ball"+i];
- for (j=i+1; j<=15; j++) {
- b = this["ball"+j];
- //determine the distance on the x and y planes
- var dx = b._x-a._x;
- var dy = b._y-a._y;
- //use the pythagorean theorem to determine the distance between two balls
- var dist = Math.sqrt(dx*dx+dy*dy);
- //if there is a collision between two balls, process it.
- if (dist<a._width/2+b._width/2) {
- _root.solveBalls(a, b);
- }
- }
- }
- }
- //grabbed this function from https://www.mochiads.com/community/forum/topic/circular-hit-test-thingy-problem
- function solveBalls(ballA, ballB) {
- var x1 = ballA._x;
- var y1 = ballA._y;
- //determine the distance between the two balls along the x and y planes
- var dx = ballB._x-x1;
- var dy = ballB._y-y1;
- //determine the actual distance using the pythagorean theorem
- var dist = Math.sqrt(dx*dx+dy*dy);
- //determine the radius of the two balls averaged together
- radius = ballA._width/4+ballB._width/4;
- //it calculates the distance, i could have passed it to the function but it works this way
- normalX = dx/dist;
- normalY = dy/dist;
- midpointX = (x1+ballB._x)/2;
- midpointY = (y1+ballB._y)/2;
- //Now this calculates the normal and mid points..
- ballA._x = midpointX-normalX*radius;
- ballA._y = midpointY-normalY*radius;
- ballB._x = midpointX+normalX*radius;
- ballB._y = midpointY+normalY*radius;
- //shifts the two balls to a different location so they don't hit each other
- dVector = (ballA.xvel-ballB.xvel)*normalX+(ballA.yvel-ballB.yvel)*normalY;
- dvx = dVector*normalX;
- dvy = dVector*normalY;
- //This calculates the new speeds for the balls
- ballA.xvel -= dvx;
- ballA.yvel -= dvy;
- ballB.xvel += dvx;
- ballB.yvel += dvy;
- //assigns the new values
- }
- //sink a ball when it hits a pocket
- function sinkBall(curball) {
- //if the sunk ball is the cue ball, it causes a scratch
- if (curball == 0) {
- //bring the cue ball back up and let the player move it around
- scratch();
- //else it isn't the cue ball, so move the ball down to the bottom tray
- } else {
- //make sure the ball isn't already sunk
- if (eval('ball'+curball).sunk == 0) {
- //enumerate the number of balls sunk and give this ball that value
- //the value is used to determine how far to the right the ball rolls
- eval('ball'+curball).sunk = (++ballssunk);
- //stop the ball's movement. We will manually move it to the right
- eval('ball'+curball).xvel = 0;
- eval('ball'+curball).yvel = 0;
- //move the ball down into the tray
- eval('ball'+curball)._y = trayarea._y+trayarea._height/2;
- //move the ball to the left side of the tray
- eval('ball'+curball)._x = trayarea._x+eval('ball'+curball)._width/2;
- }
- }
- }
- //
- function scratch() {
- //allow the player to drag the cue ball around the left quarter of the table
- //spacer gives a buffer so the ball will not go in a hole when it is being drug around
- ball0.startDrag(true, tablearea._x+ball0._width/2+spacer, tablearea._y+ball0._height/2+spacer, tablearea._x+tablearea._width/4, tablearea._y+tablearea._height-ball0._height/2-spacer);
- //reset the speed of the cue ball to 0
- ball0.xvel = 0;
- ball0.yvel = 0;
- //the ball has been pressed, place it on the table and show the pool stick
- ball0.onPress = function() {
- //Check the distances between balls
- a = this;
- //don't drop the ball if it is touching another ball
- for (j=1; j<=15; j++) {
- b = this._parent["ball"+j];
- var dx = b._x-a._x;
- var dy = b._y-a._y;
- //determine the distance between 2 balls
- var dist = Math.sqrt(dx*dx+dy*dy);
- if (dist<a._width/2+b._width/2) {
- return;
- }
- }
- //make sure no other balls are moving before dropping the cue ball
- for (i=1; i<=15; i++) {
- if (eval('ball'+i).xvel != 0 || eval('ball'+i).yvel != 0) {
- return;
- }
- }
- //The ball is not touching any other balls and all other balls have stopped moving, so drop it on the table
- ball0.stopDrag();
- //show the pool stick
- showStick();
- //delete this function so the player cannot click the ball anymore
- delete ball0.onPress;
- };
- }
- //Show the pool stick
- function showStick() {
- //if a pool stick already exists, delete it
- if (poolstick) {
- delete poolstick;
- }
- //create a new poolstick
- this.attachMovie('poolstick', 'poolstick', this.getNextHighestDepth(), {_x:ball0._x, _y:ball0._y});
- //initialize the power to 0
- poolstick.power = 0;
- //initialize the distance to pull back the stick to 0
- poolstick.curdist = 0;
- //on each frame move the stick according to the mouse and ball position
- poolstick.onEnterFrame = function() {
- //if the pool stick isn't fading out (it hasn't been clicked and released) move it according to the ball's position
- if (poolstick._alpha == 100) {
- poolstick._x = ball0._x;
- poolstick._y = ball0._y;
- }
- //If the poolstick hasn't been clicked yet, adjust its angle based on the mouse's angle relative to the cue ball
- if (poolstick.onPress) {
- //set the angle
- stickangle = Math.atan(-(_ymouse-ball0._y)/(_xmouse-ball0._x))/(Math.PI/180);
- if ((_xmouse-ball0._x)<0) {
- stickangle += 180;
- }
- if ((_xmouse-ball0._x)>=0 && (-(_ymouse-ball0._y))<0) {
- stickangle += 360;
- }
- poolstick._rotation = -stickangle-90;
- //else the pool stick has been clicked, so keep its angle locked from when it was clicked
- } else {
- //when the pool stick is clicked, allow the player to bring it back, giving it power
- if (poolstick.onRelease) {
- //the power is based on the distance the mouse is from the ball relative to when it was first clicked
- //keep the power stored for when the stick is released
- //set a cap to how far away from the ball the stick can go
- poolstick.power = Math.min(100, Math.max(0, (Math.sqrt(Math.pow(_xmouse-ball0._x, 2)+Math.pow(_ymouse-ball0._y, 2))-poolstick.clickdistance)));
- //set the distance equal to the amount of power applied
- poolstick.curdist = poolstick.power;
- //else the pool stick has been released. Start bringing the stick towards the ball
- } else {
- //move the stick towards the ball
- poolstick.curdist = Math.max(0, poolstick.curdist-20);
- //if there is no more distance to get to the ball, hit it with the stick
- if (poolstick.curdist == 0) {
- //if this is the first frame the poolstick has hit the ball, change the ball's velocity
- if (poolstick._alpha == 100) {
- //hit the ball in the direction the pool stick is pointed
- ball0.xvel = Math.sin(poolstick._rotation*degreestoradians)*(poolstick.power*powermultiplier);
- ball0.yvel = -Math.cos(poolstick._rotation*degreestoradians)*(poolstick.power*powermultiplier);
- }
- //start fading out the pool stick
- poolstick._alpha -= 10;
- //if the pool stick is fully invisible, delete it and set an interval that waits until all balls have stopped moving
- if (poolstick._alpha<=0) {
- //called every 30 milliseconds
- setMovementInterval();
- removeMovieClip(poolstick);
- }
- }
- }
- }
- //if the poolstick hasn't been released or clicked yet, move it to the cue ball's edge
- if (poolstick._alpha == 100) {
- //We figure out the direction the poolstick is pointing, and then move it away from the ball according to its angle
- poolstick._x += -Math.sin(degreestoradians*poolstick._rotation)*(ball0._width/2+poolstick.curdist);
- poolstick._y += Math.cos(degreestoradians*poolstick._rotation)*(ball0._width/2+poolstick.curdist);
- }
- };
- //adds the function to drag the poolstick when it is clicked
- poolstickAction();
- }
- //add a listener to the poolstick to start dragging when it is clicked
- function poolstickAction() {
- //the listener that waits for the poolstick to be clicked
- poolstick.onPress = function() {
- //determine the distance of the mouse from the ball, and store it
- poolstick.clickdistance = Math.sqrt(Math.pow(_xmouse-ball0._x, 2)+Math.pow(_ymouse-ball0._y, 2));
- //add a listener that waits for the user to release the mouse
- poolstick.onRelease = poolstick.onReleaseOutside=function () {
- //get rid of the current poolstick listeners
- delete poolstick.onRelease;
- delete poolstick.onReleaseOutside;
- //if the poolstick is not pulled back, recreate the
- if (poolstick.power<=0) {
- //else the poolstick is not pulled back, so recreate the poolstick action
- poolstickAction();
- }
- };
- //delete the poolstick press listener
- delete poolstick.onPress;
- };
- }
- //create an interval to check if any balls have moved since the last interval
- function setMovementInterval() {
- //run the interval every 'movetime' milliseconds
- var movementinterval:Number = setInterval(this, "checkMovement", movetime);
- }
- //function that is called by the interval
- function checkMovement():Void {
- //if the poolstick is still on the table, do not check if the balls have stopped moving
- if (poolstick) {
- return;
- }
- //loop through all of the balls to check for movement
- for (i=0; i<=15; i++) {
- if (eval('ball'+i).xvel != 0 || eval('ball'+i).yvel != 0) {
- return;
- }
- }
- //if the user hasn't scratched, show the poolstick
- if (!ball0.onPress) {
- showStick();
- }
- }
- //function called when the newgame button is pressed, aka rack 'em
- newgame.onPress = function() {
- //if ball0 exists, all of the balls exist, so we first delete all of the balls
- if (ball0) {
- for (i=0; i<=15; i++) {
- //delete all of the balls
- removeMovieClip('ball'+i);
- }
- //delete the main function that runs at the start of each frame
- delete onEnterFrame;
- }
- //if the poolstick exists, delete it
- if (poolstick) {
- removeMovieClip('poolstick');
- }
- //start a new game
- newGame();
- };
The source code and built swf are available below:
Download the Source File
Download the Built swf File
Comments Currently Disabled


