Playing Around with Elastic Collisions
In this tutorial we will create a game where the objective is to prevent other objects from colliding with your cursor. We won't use Flash's built-in hitTestObject()
methods; instead we will write our own collision detection routines.
Every few weeks, we revisit some of our reader's favorite posts from throughout the history of the site. This tutorial was first published in February of 2011.
Final Result Preview
Let's take a look at the final result we will be working towards:
Step 1: Start Off
Create a new Flash file (ActionScript 3.0)
Set the stage dimensions to 500x500px and FPS to 32.
Step 2: The Ball Class
This class will contain all data related to one ball. A ball has a _mass
, a _radius
, an _xSpeed
and a _ySpeed
. So we will make a property for each. In the constructor we pass the mass, the angle and the speed of the ball. Because the class will be linked to a display object we can retrieve the the radius of our ball by dividing the width of the display object by 2. The _xSpeed
and _ySpeed
can be calculated by using simple sine and cosine functions.
1 |
|
2 |
package
|
3 |
{
|
4 |
import flash.display.Stage |
5 |
import flash.display.Sprite |
6 |
import flash.events.Event |
7 |
|
8 |
public class Ball extends Sprite |
9 |
{
|
10 |
private var _radius:Number = 0 |
11 |
private var _mass:Number = 0 |
12 |
private var _xSpeed:Number = 0 |
13 |
private var _ySpeed:Number = 0 |
14 |
|
15 |
public function Ball(mass:Number = 10.0, angle:Number = Math.PI, speed:Number = 10.0):void |
16 |
{
|
17 |
this.mass = mass |
18 |
this._radius = this.width/2 |
19 |
this.xSpeed = speed*Math.sin(angle) |
20 |
this.ySpeed = speed*Math.cos(angle) |
21 |
}
|
22 |
}
|
23 |
}
|
For more information on these trigonometric Math.sin() and Math.cos() functions, see this Quick Tip.
Step 3: Providing Getters and Setters
In our Ball class we provide getters and setters for our properties.
1 |
|
2 |
public function get radius():Number |
3 |
{
|
4 |
return this._radius |
5 |
}
|
6 |
|
7 |
public function set mass(mass:Number):void |
8 |
{
|
9 |
this._mass = mass |
10 |
}
|
11 |
|
12 |
public function get mass():Number |
13 |
{
|
14 |
return this._mass |
15 |
}
|
16 |
|
17 |
public function set xSpeed(xSpeed:Number):void |
18 |
{
|
19 |
this._xSpeed = xSpeed |
20 |
}
|
21 |
|
22 |
public function get xSpeed():Number |
23 |
{
|
24 |
return this._xSpeed |
25 |
}
|
26 |
|
27 |
public function set ySpeed(ySpeed:Number):void |
28 |
{
|
29 |
this._ySpeed = ySpeed |
30 |
}
|
31 |
|
32 |
public function get ySpeed():Number |
33 |
{
|
34 |
return this._ySpeed |
35 |
}
|
Step 4: Update Function
This function updates the x and y properties of our ball according to the _xSpeed
and _ySpeed
. We'll implement this function in our Ball
class.
1 |
|
2 |
public function update():void |
3 |
{
|
4 |
this.x += _xSpeed |
5 |
this.y += _ySpeed |
6 |
}
|
Step 5: The Completed Class
We'll finish up our Ball
class in this step.
1 |
|
2 |
package
|
3 |
{
|
4 |
import flash.display.Stage |
5 |
import flash.display.Sprite |
6 |
import flash.events.Event |
7 |
|
8 |
public class Ball extends Sprite |
9 |
{
|
10 |
private var _radius:Number = 0 |
11 |
private var _mass:Number = 0 |
12 |
private var _xSpeed:Number = 0 |
13 |
private var _ySpeed:Number = 0 |
14 |
|
15 |
public function Ball(mass:Number = 10.0, angle:Number = Math.PI, speed:Number = 10.0):void |
16 |
{
|
17 |
this.mass = mass |
18 |
this._radius = this.width/2 |
19 |
this.xSpeed = speed*Math.sin(angle) |
20 |
this.ySpeed = speed*Math.cos(angle) |
21 |
}
|
22 |
|
23 |
public function get radius():Number |
24 |
{
|
25 |
return this._radius |
26 |
}
|
27 |
|
28 |
public function set mass(mass:Number):void |
29 |
{
|
30 |
this._mass = mass |
31 |
}
|
32 |
|
33 |
public function get mass():Number |
34 |
{
|
35 |
return this._mass |
36 |
}
|
37 |
|
38 |
public function set xSpeed(xSpeed:Number):void |
39 |
{
|
40 |
this._xSpeed = xSpeed |
41 |
}
|
42 |
|
43 |
public function get xSpeed():Number |
44 |
{
|
45 |
return this._xSpeed |
46 |
}
|
47 |
|
48 |
public function set ySpeed(ySpeed:Number):void |
49 |
{
|
50 |
this._ySpeed = ySpeed |
51 |
}
|
52 |
|
53 |
public function get ySpeed():Number |
54 |
{
|
55 |
return this._ySpeed |
56 |
}
|
57 |
|
58 |
public function update():void |
59 |
{
|
60 |
this.x += _xSpeed |
61 |
this.y += _ySpeed |
62 |
}
|
63 |
}
|
64 |
}
|
Step 6: Display Objects for our Ball Class
In the source-files I included a start FLA which contains all the library items you need. You can draw them yourself if you want, of course. Make sure your FLA has the following display objects:
(Ed. note: that's a typo: "ennemyball" should say "enemyball".)
Step 7: Linking our Library Objects
The Ball
class we just created has to be linked to the enemyball
Sprite in the library.
The playerball
Sprite must have Ball
as base class and PlayerBall
as class.
The score
movie clip must have a Score
class.
Step 8: The Application Class (Document Class)
The Application
class will contain all the game logic. We import all the classes we need. As you can see we'll be using TweenMax.
Next we define our field variables. The first field variable is the ballPlayer
.
Because the base class of our playerball
Sprite is Ball
we can store this Class in the ballPlayer
variable. This makes it easier later on to check for collisions between the ballPlayer
and the enemy balls.
The second field variable is an array which will contain all our enemy balls. The third variable is the timer that will be used to perform the main game loop. The fourth and last field is an instance of our Score
library object that will be used to display the elapsed game time. In the constructor we call the init()
function which I'll explain in the next step.
1 |
|
2 |
package
|
3 |
{
|
4 |
import flash.display.Sprite |
5 |
import flash.display.Graphics |
6 |
import flash.events.Event |
7 |
import flash.events.TimerEvent |
8 |
import flash.events.MouseEvent |
9 |
import flash.geom.Matrix |
10 |
import flash.utils.Timer |
11 |
import flash.ui.Mouse |
12 |
import com.greensock.TweenMax |
13 |
import com.greensock.easing.* |
14 |
|
15 |
public class Application extends Sprite |
16 |
{
|
17 |
|
18 |
private var ballPlayer:Ball |
19 |
private var eballs:Array |
20 |
private var tmr:Timer |
21 |
private var score:Score |
22 |
|
23 |
public function Application():void |
24 |
{
|
25 |
init() |
26 |
}
|
27 |
}
|
28 |
}
|
Don't forget to link the document class!.
Step 9: The init() Function
Take a look at this code:
1 |
|
2 |
private function init():void |
3 |
{
|
4 |
ballPlayer = new PlayerBall() |
5 |
eballs = new Array() |
6 |
tmr = new Timer(10) |
7 |
score = new Score() |
8 |
|
9 |
stage.align = "TL" |
10 |
stage.scaleMode = "noScale" |
11 |
Mouse.hide() |
12 |
|
13 |
setBackground() |
14 |
|
15 |
score.x = stage.stageWidth/2 |
16 |
score.y = stage.stageHeight/2 |
17 |
stage.addChild(score) |
18 |
|
19 |
stage.addEventListener(MouseEvent.MOUSE_MOVE, updatePlayerBall) |
20 |
stage.addChild(ballPlayer) |
21 |
|
22 |
tmr.addEventListener(TimerEvent.TIMER, updateTime) |
23 |
|
24 |
stage.addEventListener(MouseEvent.CLICK, startGame) |
25 |
}
|
In the first four lines we initialize our field variables.
Next we make sure our stage is aligned to the top left corner and does not scale.
We hide the mouse cursor. Our cursor will be replaced with the playerball
Sprite. Next we call the setBackground
function(explained in the next step).
We center our score
on the screen and add it to the display list. To update the position of ballPlayer
we attach a MouseEvent.MOUSE_MOVE event to the stage.
The updatePlayerBall
function (explained in step 11) will handle this MouseEvent. Next we add the ballPlayer
to the display list.
The timer will be used to display the game time. We attach a TimerEvent.TIMER listener to our timer, which will trigger the updateTime()
function (explained in Step 12) every 10 milliseconds.
Finally, we add a MouseEvent.CLICK to our stage. The startGame
function (explained in step 13) will then start our game.
Step 10: setBackground() Function
This function adds a radial gradient background to the display list. To draw a gradient on a Sprite you must define the type of gradient, the colors you want to use, alpha values of the colors, the ratios (these define the distribution of the colors)and the spread method.
For more information, see this Quick Tip on gradients.
1 |
|
2 |
private function setBackground():void |
3 |
{
|
4 |
var type:String = "radial" |
5 |
var colors:Array = [0xffffff,0xcccccc] |
6 |
var alphas:Array = [ 1, 1 ] |
7 |
var ratios:Array = [ 0, 255 ] |
8 |
var matr:Matrix = new Matrix() |
9 |
matr.createGradientBox(stage.stageWidth, stage.stageHeight, Math.PI / 2, 0, 0 ) |
10 |
//SpreadMethod will define how the gradient is spread. Note!!! Flash uses CONSTANTS to represent String literals
|
11 |
var sprMethod:String = "pad" |
12 |
//Start the Gradietn and pass our variables to it
|
13 |
var sprite:Sprite = new Sprite() |
14 |
//Save typing + increase performance through local reference to a Graphics object
|
15 |
var g:Graphics = sprite.graphics |
16 |
g.beginGradientFill( type, colors, alphas, ratios, matr, sprMethod ) |
17 |
g.drawRect(0,0,stage.stageWidth,stage.stageHeight) |
18 |
|
19 |
stage.addChild(sprite) |
20 |
}
|
Step 11: updatePlayerBall() Function
This function updates the position of ballPlayer
according to the position of your mouse.
1 |
|
2 |
private function updatePlayerBall(e:MouseEvent):void |
3 |
{
|
4 |
ballPlayer.x = mouseX |
5 |
ballPlayer.y = mouseY |
6 |
}
|
Step 12: updateTime() Function
We calculate the time in seconds and put it inside the textbox of our score
Sprite. Every 5000ms (five seconds) we add a new ball to the game.
1 |
|
2 |
private function updateTime(e:TimerEvent):void |
3 |
{
|
4 |
score.txtScore.text = String(((tmr.currentCount*tmr.delay)/1000).toFixed(2)); |
5 |
if((tmr.currentCount*tmr.delay) % 5000 == 0) |
6 |
{
|
7 |
addBall(); |
8 |
}
|
9 |
}
|
Step 13: startGame() Function
The game is started by clicking the stage. First we remove the listener for the stage click, so that we can't start the game serveral times. We add three balls to the game by calling the addBall()
function (explained in the next step) three times. We start our timer which will update our game time.
Finally we add an ENTER_FRAME event to our stage. The gameLoop()
function (explained in Step 15) will update the position of our enemy balls.
1 |
|
2 |
private function startGame(e:MouseEvent):void |
3 |
{
|
4 |
stage.removeEventListener(MouseEvent.CLICK, startGame) |
5 |
addBall() |
6 |
addBall() |
7 |
addBall() |
8 |
tmr.start() |
9 |
stage.addEventListener(Event.ENTER_FRAME, gameLoop) |
10 |
}
|
Step 14: addBall() Function
First we make a new instance of our Ball
class. We position the ball
randomly on the stage with an alpha of 0 and add it to the display list.
Next we tween the alpha back to 1. (I use TweenMax, it's included in the source files. You can also use the built-in Flash tween engine.) The second tween isn't really a tween. It just waits a second and the onComplete
function pushes the ball
into our eballs
array. This way the gameLoop()
function (explained in the next step) can handle the rest.
1 |
|
2 |
private function addBall():void |
3 |
{
|
4 |
var ball:Ball = new Ball(10, Math.random()*Math.PI*2, 5) |
5 |
ball.x = Math.random()*stage.stageWidth |
6 |
ball.y = Math.random()*stage.stageHeight |
7 |
ball.alpha = 0 |
8 |
stage.addChild(ball) |
9 |
TweenMax.to(ball, 0.5, {alpha:1}) |
10 |
TweenMax.to(ball, 0, {delay: 1, onComplete:function():void{eballs.push(ball)}}) |
11 |
}
|
Step 15: gameLoop() Function
Every frame will go through this function.
1 |
|
2 |
private function gameLoop(e:Event):void |
3 |
{
|
4 |
for (var i:uint = 0; i < eballs.length; i++) |
5 |
{
|
6 |
for (var j:uint = i + 1; j < eballs.length; j++) |
7 |
{
|
8 |
if (collision(eballs[i], eballs[j])) |
9 |
{
|
10 |
doCollision(eballs[i], eballs[j]) |
11 |
}
|
12 |
}
|
13 |
|
14 |
if(collision(eballs[i], ballPlayer)) |
15 |
{
|
16 |
endOfGame() |
17 |
break
|
18 |
}
|
19 |
|
20 |
eballs[i].update() |
21 |
checkBounds(eballs[i]) |
22 |
}
|
23 |
}
|
We start by iterating through all of our enemy balls.
The second for-loop checks for collisions between the enemy balls. The loop starts at 'i + 1'. This way we don't double check the collisions.
Next we check if the ballPlayer
hits the enemy ball. If so, the game is finished. Then we update the position of our enemy ball.
We make sure the balls stay in the game screen by calling the function checkBounds()
(explained later on).
Step 16: collision() Function
This function checks whether any given pair of balls are colliding.
First we calculate the x distance and the y distance between the two balls. Using Pythagoras's Theorem (see the following diagram) we calculate the absolute distance between them. If the distance is less or equal to the sum of the the radii of the balls we have a collision.
1 |
|
2 |
private function collision(ball1:Ball, ball2:Ball):Boolean |
3 |
{
|
4 |
var xDist:Number = ball1.x - ball2.x |
5 |
var yDist:Number = ball1.y - ball2.y |
6 |
var Dist:Number = Math.sqrt(xDist * xDist + yDist * yDist) |
7 |
|
8 |
return Dist <= ball1.radius + ball2.radius |
9 |
}
|
Step 17: doCollision() Function
This function will calculate the new x- and y-speeds of the balls according to the speed and angle of the collision. Warning: math ;)
First we calculate the horizontal distance between the two balls and then the vertical distance between the balls. With these distances (and a little more trigonometry) we can calculate the angle between the balls (see diagram).
Next we calculate what I call the magnitude of each ball. (We have an xspeed vector and a yspeed vector; the magnitude is the vector sum of those.) Then we calculate the angle of each ball (similar to the previous angle calculation).
Next we rotate the new x- and y-speeds of each ball. What we are actually doing is rotating the coordinate system. By rotating our axes we have a 1D collision. (See the following diagram).
Newton says that the total amount of kinetic energy in a closed system is constant. Now we use these formulae:
v1 = (u1*(m1-m2) + 2*m2*u2)/(m1+m2)
v2 = (u2*(m2-m1) + 2*m1*u1)/(m1+m2)
where:
v1 = final xSpeedBall 1
v2 = final xSpeedBall 2
m1 = mass ball 1
m2 = mass ball 2
u1 = initial speed ball 1
u2 = initial speed ball 2
The y-speeds don't change as it is a 1D collision.
With these formulae we can calculate the xSpeed
and ySpeed
of each ball.
Now whe have the new x- and y-speeds in our rotated coordinate system. The last step is to convert everything back to a normal coordinate system. We use Math.PI/2
because the angle between xSpeed
and ySpeed
must always be 90 degrees (pi/2 radians).
1 |
|
2 |
private function doCollision(ball1:Ball, ball2:Ball):void |
3 |
{
|
4 |
var xDist:Number = ball1.x - ball2.x |
5 |
var yDist:Number = ball1.y - ball2.y |
6 |
var collisionAngle:Number = Math.atan2(yDist, xDist) |
7 |
|
8 |
var magBall1:Number = Math.sqrt(ball1.xSpeed*ball1.xSpeed+ball1.ySpeed*ball1.ySpeed) |
9 |
var magBall2:Number = Math.sqrt(ball2.xSpeed*ball2.xSpeed+ball2.ySpeed*ball2.ySpeed) |
10 |
|
11 |
var angleBall1:Number = Math.atan2(ball1.ySpeed, ball1.xSpeed) |
12 |
var angleBall2:Number = Math.atan2(ball2.ySpeed, ball2.xSpeed) |
13 |
|
14 |
var xSpeedBall1:Number = magBall1 * Math.cos(angleBall1-collisionAngle) |
15 |
var ySpeedBall1:Number = magBall1 * Math.sin(angleBall1-collisionAngle) |
16 |
var xSpeedBall2:Number = magBall2 * Math.cos(angleBall2-collisionAngle) |
17 |
var ySpeedBall2:Number = magBall2 * Math.sin(angleBall2-collisionAngle) |
18 |
|
19 |
var finalxSpeedBall1:Number = ((ball1.mass-ball2.mass)*xSpeedBall1+(ball2.mass+ball2.mass)*xSpeedBall2)/(ball1.mass+ball2.mass) |
20 |
var finalxSpeedBall2:Number = ((ball1.mass+ball1.mass)*xSpeedBall1+(ball2.mass-ball1.mass)*xSpeedBall2)/(ball1.mass+ball2.mass) |
21 |
var finalySpeedBall1:Number = ySpeedBall1 |
22 |
var finalySpeedBall2:Number = ySpeedBall2 |
23 |
|
24 |
ball1.xSpeed = Math.cos(collisionAngle)*finalxSpeedBall1+Math.cos(collisionAngle+Math.PI/2)*finalySpeedBall1 |
25 |
ball1.ySpeed = Math.sin(collisionAngle)*finalxSpeedBall1+Math.sin(collisionAngle+Math.PI/2)*finalySpeedBall1 |
26 |
ball2.xSpeed = Math.cos(collisionAngle)*finalxSpeedBall2+Math.cos(collisionAngle+Math.PI/2)*finalySpeedBall2 |
27 |
ball2.ySpeed = Math.sin(collisionAngle)*finalxSpeedBall2+Math.sin(collisionAngle+Math.PI/2)*finalySpeedBall2 |
28 |
}
|
To find more information about elastic collisions take a look at hoomanr.com.
Step 18: endOfGame() Function
This is run when the game ends.
1 |
|
2 |
private function endOfGame():void |
3 |
{
|
4 |
tmr.stop() |
5 |
Mouse.show() |
6 |
stage.removeEventListener(MouseEvent.MOUSE_MOVE, updatePlayerBall) |
7 |
stage.removeEventListener(Event.ENTER_FRAME, gameLoop) |
8 |
|
9 |
while(eballs.length > 0) |
10 |
{
|
11 |
TweenMax.to(eballs[0], 0.5, {scaleX:0, scaleY:0, ease:Bounce.easeOut}) |
12 |
eballs.splice(0,1) |
13 |
}
|
14 |
|
15 |
TweenMax.to(ballPlayer, 0.5, {scaleX:0, scaleY:0, ease:Bounce.easeOut}) |
16 |
}
|
First off all we stop the timer. We show the mouse again. Next we remove both the MOUSE_MOVE and ENTER_FRAME event listeners. Finally we make all the balls on the stage invisible.
Step 19: checkBounds() Function
This function makes sure the balls stay inside the game screen. So if the ball hits the upper or lower side, we reverse the ySpeed
. if the ball hits the left or the right side of the screen we reverse the xSpeed
. It uses similar logic to the ball collision detection function to check whether the edges of the ball hits an edge of the screen.
1 |
|
2 |
private function checkBounds(ball:Ball):void |
3 |
{
|
4 |
if((ball.x + ball.radius) > stage.stageWidth) |
5 |
{
|
6 |
ball.x = stage.stageWidth - ball.radius |
7 |
ball.xSpeed *= -1 |
8 |
}
|
9 |
if((ball.x - ball.radius) < 0) |
10 |
{
|
11 |
ball.x = 0 + ball.radius |
12 |
ball.xSpeed *= -1 |
13 |
}
|
14 |
if((ball.y + ball.radius) > stage.stageHeight) |
15 |
{
|
16 |
ball.y = stage.stageHeight - ball.radius |
17 |
ball.ySpeed *= - 1 |
18 |
}
|
19 |
if((ball.y - ball.radius) < 0) |
20 |
{
|
21 |
ball.y = 0 + ball.radius |
22 |
ball.ySpeed *= - 1 |
23 |
}
|
24 |
}
|
Step 20: The Complete Application Class
We have completed our Application class. We now have a working game!!!
1 |
|
2 |
package
|
3 |
{
|
4 |
import flash.display.Sprite; |
5 |
import flash.display.Graphics; |
6 |
import flash.events.Event; |
7 |
import flash.events.TimerEvent; |
8 |
import flash.events.MouseEvent; |
9 |
import flash.geom.Matrix; |
10 |
import flash.utils.Timer; |
11 |
import flash.ui.Mouse; |
12 |
import com.greensock.TweenMax; |
13 |
import com.greensock.easing.*; |
14 |
public class Application extends Sprite |
15 |
{
|
16 |
|
17 |
private var ballPlayer:Ball; |
18 |
private var eballs:Array; |
19 |
private var tmr:Timer; |
20 |
private var score:Score; |
21 |
public function Application():void |
22 |
{
|
23 |
init(); |
24 |
}
|
25 |
|
26 |
private function init():void |
27 |
{
|
28 |
ballPlayer = new PlayerBall(); |
29 |
eballs = new Array(); |
30 |
tmr = new Timer(10); |
31 |
score = new Score(); |
32 |
|
33 |
stage.align = "TL"; |
34 |
stage.scaleMode = "noScale"; |
35 |
Mouse.hide(); |
36 |
|
37 |
setBackground(); |
38 |
|
39 |
score.x = stage.stageWidth / 2; |
40 |
score.y = stage.stageHeight / 2; |
41 |
stage.addChild(score); |
42 |
|
43 |
stage.addEventListener(MouseEvent.MOUSE_MOVE, updatePlayerBall); |
44 |
stage.addChild(ballPlayer); |
45 |
|
46 |
tmr.addEventListener(TimerEvent.TIMER, updateTime); |
47 |
|
48 |
stage.addEventListener(MouseEvent.CLICK, startGame); |
49 |
}
|
50 |
|
51 |
private function setBackground():void |
52 |
{
|
53 |
var type:String = "radial"; |
54 |
var colors:Array = [0xffffff,0xcccccc]; |
55 |
var alphas:Array = [1,1]; |
56 |
var ratios:Array = [0,255]; |
57 |
var matr:Matrix = new Matrix(); |
58 |
matr.createGradientBox(stage.stageWidth, stage.stageHeight, Math.PI / 2, 0, 0 ); |
59 |
//SpreadMethod will define how the gradient is spread. Note!!! Flash uses CONSTANTS to represent String literals
|
60 |
var sprMethod:String = "pad"; |
61 |
//Start the Gradietn and pass our variables to it
|
62 |
var sprite:Sprite = new Sprite(); |
63 |
//Save typing + increase performance through local reference to a Graphics object
|
64 |
var g:Graphics = sprite.graphics; |
65 |
g.beginGradientFill( type, colors, alphas, ratios, matr, sprMethod ); |
66 |
g.drawRect(0,0,stage.stageWidth,stage.stageHeight); |
67 |
stage.addChild(sprite); |
68 |
}
|
69 |
|
70 |
private function updatePlayerBall(e:MouseEvent):void |
71 |
{
|
72 |
ballPlayer.x = mouseX; |
73 |
ballPlayer.y = mouseY; |
74 |
}
|
75 |
|
76 |
private function updateTime(e:TimerEvent):void |
77 |
{
|
78 |
score.txtScore.text = String(((tmr.currentCount*tmr.delay)/1000).toFixed(2)); |
79 |
if ((tmr.currentCount*tmr.delay) % 5000 == 0) |
80 |
{
|
81 |
addBall(); |
82 |
}
|
83 |
}
|
84 |
|
85 |
private function startGame(e:MouseEvent):void |
86 |
{
|
87 |
stage.removeEventListener(MouseEvent.CLICK, startGame); |
88 |
addBall(); |
89 |
addBall(); |
90 |
addBall(); |
91 |
tmr.start(); |
92 |
stage.addEventListener(Event.ENTER_FRAME, gameLoop); |
93 |
}
|
94 |
|
95 |
private function addBall():void |
96 |
{
|
97 |
var ball:Ball = new Ball(10,Math.random() * Math.PI * 2,5); |
98 |
ball.x = Math.random() * stage.stageWidth; |
99 |
ball.y = Math.random() * stage.stageHeight; |
100 |
ball.alpha = 0; |
101 |
stage.addChild(ball); |
102 |
TweenMax.to(ball, 0.5, {alpha:1}); |
103 |
TweenMax.to(ball, 0, {delay: 1, onComplete:function():void{eballs.push(ball)}}); |
104 |
}
|
105 |
|
106 |
private function gameLoop(e:Event):void |
107 |
{
|
108 |
for (var i:uint = 0; i < eballs.length; i++) |
109 |
{
|
110 |
for (var j:uint = i + 1; j < eballs.length; j++) |
111 |
{
|
112 |
if (collision(eballs[i],eballs[j])) |
113 |
{
|
114 |
doCollision(eballs[i], eballs[j]); |
115 |
}
|
116 |
}
|
117 |
|
118 |
if (collision(eballs[i],ballPlayer)) |
119 |
{
|
120 |
endOfGame(); |
121 |
break; |
122 |
}
|
123 |
|
124 |
eballs[i].update(); |
125 |
checkBounds(eballs[i]); |
126 |
}
|
127 |
}
|
128 |
|
129 |
private function collision(ball1:Ball, ball2:Ball):Boolean |
130 |
{
|
131 |
var xDist:Number = ball1.x - ball2.x; |
132 |
var yDist:Number = ball1.y - ball2.y; |
133 |
var Dist:Number = Math.sqrt(xDist * xDist + yDist * yDist); |
134 |
|
135 |
if (Dist <= ball1.radius + ball2.radius) |
136 |
{
|
137 |
if (ball1.x < ball2.x) |
138 |
{
|
139 |
ball1.x -= 2; |
140 |
ball2.x += 2; |
141 |
}
|
142 |
else
|
143 |
{
|
144 |
ball1.x += 2; |
145 |
ball2.x -= 2; |
146 |
}
|
147 |
|
148 |
if (ball1.y < ball2.y) |
149 |
{
|
150 |
ball1.y -= 2; |
151 |
ball2.y += 2; |
152 |
}
|
153 |
else
|
154 |
{
|
155 |
ball1.y += 2; |
156 |
ball2.y -= 2; |
157 |
}
|
158 |
}
|
159 |
|
160 |
|
161 |
return Dist <= ball1.radius + ball2.radius; |
162 |
}
|
163 |
|
164 |
private function doCollision(ball1:Ball, ball2:Ball):void |
165 |
{
|
166 |
var xDist:Number = ball1.x - ball2.x; |
167 |
var yDist:Number = ball1.y - ball2.y; |
168 |
var collisionAngle:Number = Math.atan2(yDist,xDist); |
169 |
var magBall1:Number = Math.sqrt(ball1.xSpeed * ball1.xSpeed + ball1.ySpeed * ball1.ySpeed); |
170 |
var magBall2:Number = Math.sqrt(ball2.xSpeed * ball2.xSpeed + ball2.ySpeed * ball2.ySpeed); |
171 |
var angleBall1:Number = Math.atan2(ball1.ySpeed,ball1.xSpeed); |
172 |
var angleBall2:Number = Math.atan2(ball2.ySpeed,ball2.xSpeed); |
173 |
var xSpeedBall1:Number = magBall1 * Math.cos(angleBall1 - collisionAngle); |
174 |
var ySpeedBall1:Number = magBall1 * Math.sin(angleBall1 - collisionAngle); |
175 |
var xSpeedBall2:Number = magBall2 * Math.cos(angleBall2 - collisionAngle); |
176 |
var ySpeedBall2:Number = magBall2 * Math.sin(angleBall2 - collisionAngle); |
177 |
var finalxSpeedBall1:Number = ((ball1.mass-ball2.mass)*xSpeedBall1+(ball2.mass+ball2.mass)*xSpeedBall2)/(ball1.mass+ball2.mass); |
178 |
var finalxSpeedBall2:Number = ((ball1.mass+ball1.mass)*xSpeedBall1+(ball2.mass-ball1.mass)*xSpeedBall2)/(ball1.mass+ball2.mass); |
179 |
var finalySpeedBall1:Number = ySpeedBall1; |
180 |
var finalySpeedBall2:Number = ySpeedBall2; |
181 |
ball1.xSpeed = Math.cos(collisionAngle) * finalxSpeedBall1 + Math.cos(collisionAngle + Math.PI / 2) * finalySpeedBall1; |
182 |
ball1.ySpeed = Math.sin(collisionAngle) * finalxSpeedBall1 + Math.sin(collisionAngle + Math.PI / 2) * finalySpeedBall1; |
183 |
ball2.xSpeed = Math.cos(collisionAngle) * finalxSpeedBall2 + Math.cos(collisionAngle + Math.PI / 2) * finalySpeedBall2; |
184 |
ball2.ySpeed = Math.sin(collisionAngle) * finalxSpeedBall2 + Math.sin(collisionAngle + Math.PI / 2) * finalySpeedBall2; |
185 |
}
|
186 |
|
187 |
private function endOfGame():void |
188 |
{
|
189 |
tmr.stop(); |
190 |
Mouse.show(); |
191 |
stage.removeEventListener(MouseEvent.MOUSE_MOVE, updatePlayerBall); |
192 |
stage.removeEventListener(Event.ENTER_FRAME, gameLoop); |
193 |
|
194 |
while (eballs.length > 0) |
195 |
{
|
196 |
TweenMax.to(eballs[0], 0.5, {scaleX:0, scaleY:0, ease:Bounce.easeOut}); |
197 |
eballs.splice(0,1); |
198 |
}
|
199 |
|
200 |
TweenMax.to(ballPlayer, 0.5, {scaleX:0, scaleY:0, ease:Bounce.easeOut}); |
201 |
}
|
202 |
|
203 |
private function checkBounds(ball:Ball):void |
204 |
{
|
205 |
if ((ball.x + ball.radius) > stage.stageWidth) |
206 |
{
|
207 |
ball.x = stage.stageWidth - ball.radius; |
208 |
ball.xSpeed *= -1; |
209 |
}
|
210 |
if ((ball.x - ball.radius) < 0) |
211 |
{
|
212 |
ball.x = 0 + ball.radius; |
213 |
ball.xSpeed *= -1; |
214 |
}
|
215 |
if ((ball.y + ball.radius) > stage.stageHeight) |
216 |
{
|
217 |
ball.y = stage.stageHeight - ball.radius; |
218 |
ball.ySpeed *= -1; |
219 |
}
|
220 |
if ((ball.y - ball.radius) < 0) |
221 |
{
|
222 |
ball.y = 0 + ball.radius; |
223 |
ball.ySpeed *= -1; |
224 |
}
|
225 |
}
|
226 |
}
|
227 |
}
|
Conclusion
That's it for this tutorial. Of course you could add the possibility to restart the game, but that shouldn't be too hard. This basic example of elastic collisions can be used for bigger games like a billiards game or similar.
I hope you liked this tutorial, thanks for reading!