Experiment 3: Bouncy Balls
Sections in this Article:
The Ball
This is a great time to introduce two core aspects of object orientated programming, classes and objects [not quite the same thing although this issue is sometimes confused!]. In the bouncy ball example, a class is used to describe the ball and only the ball! This includes its behaviour, size and position on the screen. For example, a ball accelerates downwards due to the pull of gravity and when it hits a boundary, some (but not all) of the energy is used to reverse the velocity of the ball e.g. a bounce. The object on the other hand, is a single instance of a ball; each time a new ball is needed, for example when the user clicks on the screen, the program uses the class definition of a ball to create a new object in memory to present that new ball.
The snippet of code below is the class that describes the ball used in the bouncy ball example:
ball.js 1class Ball { 2 3 x = 0; 4 y = 0; 5 vx = 1; 6 vy = 0; 7 radius = 10; 8 gravity = 0.1; 9 bounceLoss = 0.9; 10 11 constructor(radius, gravity, bounceLoss, boundingArea ) { 12 this.radius = radius; 13 this.gravity = gravity; 14 this.bounceLoss = bounceLoss; 15 16 this.Initialise(boundingArea); 17 } 18 19 Initialise(boundingArea) { 20 this.vx= (Math.random()*20)-10; 21 this.vy = 0; 22 this.x = (boundingArea.width * Math.random()/2) + boundingArea.width/2; 23 this.y = -this.radius*2; // start just off screen 24 } 25 26 Update(bounds) { 27 let applyGravity = true; 28 29 // animate ball 30 this.x+=this.vx; 31 this.y+=this.vy; 32 33 // bounds check 34 if( this.x > bounds.width-this.radius ) this.x = bounds.width-this.radius; 35 36 // following bounds detection could be made much more simpler, but user 37 // might resize bounds when ball is outside... 38 if( this.vx<0) { 39 if( (this.x-this.radius) <= 0 ) { 40 this.x = this.radius; 41 this.vx = -this.vx*this.bounceLoss; 42 } 43 } else { 44 if( (this.x+this.radius) >= bounds.width ) { 45 this.x = bounds.width-this.radius; 46 this.vx = -this.vx*this.bounceLoss; 47 } 48 } 49 50 51 if( this.vy<0) { 52 // travelling up 53 if( (this.y-this.radius) <= 0 ) { 54 this.y = this.radius; 55 this.vy = -this.vy; 56 } 57 58 } else { 59 // travelling down 60 if( (this.y+this.radius) > bounds.height ) { 61 this.y = bounds.height-(this.radius); 62 63 // don't apply gravity if we're at rock bottom; can't accelerate downwards at this point! 64 if( Math.abs(this.vy) <2 ) { // is ground sticky, any vy < 0.8 doesn't result in a bounce! 65 this.vy = 0; 66 applyGravity = false; 67 68 //some friction to x 69 this.vx *= 0.99; 70 71 } else { 72 this.vy = -(this.vy*this.bounceLoss); 73 74 // not a perfect bounce, introduce some vx 75 this.vx+=(Math.random()-0.5)/4; 76 77 } 78 } 79 } 80 81 if(applyGravity) { 82 // simulate some gravity (not proper physics!) 83 this.vy+=0.5; 84 } 85 } 86}
While this may look horribly complicated at first glance, it really isn't. The really nice thing about a class is that the code should be self contained [ideally not coupled to anything else] and should be desisgned to be re-used over and over again. In the bouncy ball example, each individual ball that bounces around the screen is represented by a ball object with a svg circle representing the visual representation of the ball. For each new ball that is required a ball object and an svg circle need to be created. How that comes about will be discussed shortly, but the key thing to note is the ball object is not coupled to the svg circle which means, we could quite easily use a different display technology, e.g. a canvas at a later time and we wouldn't need to change anything in the Ball class - pretty neat!
But before we go into all of that, let’s look at what’s in the Ball class.
Class Definition
The class definition describes the class. In its simple form, it describes the name of the class in this case “Ball” and then the behaviour and properties/fields of the class. The entire class definition is essentially Lines 1 to 86, but Line 1 defines the class and its name – everything else between the curly brackets is the class.
Stripping everything back, in its simple form, a class looks like this:
class <name> { // // class definition // }
Within the class we define two things, fields and methods. Fields are
the bits of data stored in the class, for example, the
radius
of the ball. Methods are the things we can request the class to do, for
example
Update
for the next frame in the animation [perhaps
CalculateNextFrame
would have been a better name?].
The Constructor
Classes have one special method called a constructor. This is a function that is called when an object, or ball in this example, is being created. It's purpose is basically to get everything ready in the object. In the example above, the constructor is defined on Lines 11-17. Note, the ball isn't created by this function - something else to be discussed later does this!
ball.js 11 constructor(radius, gravity, bounceLoss, boundingArea ) { 12 this.radius = radius; 13 this.gravity = gravity; 14 this.bounceLoss = bounceLoss; 15 16 this.Initialise(boundingArea); 17 }
The ball constructor takes 4 parameters,
radius
,
gravity
,
bounceLoss
and
boundingArea
.
These parameters are defined as follows:
radius
– a number that defines the radius of the ball – you’ll see in the
example, that balls have differing radius.
gravity
– a constant that defines the downward acceleration of the ball, e.g.
gravity. This isn’t really needed as all items on Earth experience the
same gravity, but in this example, we ‘could’ permit each ball to
experience different gravity! [Why not try hacking the code to see how
this would look?]
bounceLoss
– this is a factor that determines how much velocity is lost with each
bounce. For example, a value of 0.9 means 90% of the velocity is
conserved in a bounce. A value of greater than 1 would mean that the
ball would gain more energy with each bounce! You may notice in the
example, bigger balls loose more energy than the smaller balls - this
is how this is achieved!
boundingArea
– an object literal with properties
width
and
height
representing the size of the screen where the ball bounces [this is one
of the things I don’t like about JavaScript; you have to take it on
faith that the
boundingArea
has
width
and
height
properties, or add some defensive code in case it doesn’t].
All the constructor really does is copy the parameters into the fields of the class then initialise everything in the class ready to be animated. The initialisation is done by the function on Lines 19-24:
Class Methods
The
Initialise
method calculates some random x velocity for the ball (Line 20);
negative values means it will drift to the left, positive values means
it will drift to the right. The ball initially starts with no vertical
velocity (Line 21). Line 22 positions the ball somewhere
between the left and right of the boundary; which is why the
boundingArea
is passed in, and Line 23 positions the ball just off the top
edge of the screen.
ball.js 19 Initialise(boundingArea) { 20 this.vx= (Math.random()*20)-10; 21 this.vy = 0; 22 this.x = (boundingArea.width * Math.random()/2) + boundingArea.width/2; 23 this.y = -this.radius*2; // start just off screen 24 }
The bulk of what happens to a Ball is defined in the
Update
method on Lines 26-85. This method should be called for each
frame of the animation. There’s quite a bit going on here so we’ll
break it down, but effectively, this method calculates everything ready
for the next frame in the experiment.
Line 27 sets up a local variable to determine if gravity is to be applied; when the ball is in free space and falling, gravity will be applied, but when the ball is a rest, running along the bottom of the screen, no further gravity can be applied so we'd set this variable to false!
ball.js 27 let applyGravity = true;
Lines 30 and 31 calculate next new position of the ball based upon how much x velocity and how much y velocity the ball has:
ball.js 30 this.x+=this.vx; 31 this.y+=this.vy;
It’s worth noting; this is not the final position of the ball – what happens if by doing this calculation, the ball is positioned off or partially through the edge of screen? That's the purpose of the next few lines of code, they determine what happens...
Lines 33-79 are where the clever stuff happens. This code examines the position and velocity of the ball, ensures the ball remains inside the boundary (even if the boundary is re-sized) and handles bouncing off the bottom and sides of the screen. Here’s the detail...
Line 34 ensure the ball is always with the left and right boundaries.
ball.js 34 if( this.x > bounds.width-this.radius ) this.x = bounds.width-this.radius;
Lines 38-48 Look at the left/right component of the ball’s velocity. If it’s heading left (velocity x is negative) and the ball is positioned off the screen (ball centre and its radius), then it will be positioned on the edge of the screen and ‘bounced’. If the velocity is positive e.g. heading right, we do the same but for the opposite side of the screen. A bounce is achieved by negating the current x velocity and taking away a little bit due to a bounce loss (see lines 41 and 46 respectively)!
ball.js 38 if( this.vx<0) { 39 if( (this.x-this.radius) <= 0 ) { 40 this.x = this.radius; 41 this.vx = -this.vx*this.bounceLoss; 42 } 43 } else { 44 if( (this.x+this.radius) >= bounds.width ) { 45 this.x = bounds.width-this.radius; 46 this.vx = -this.vx*this.bounceLoss; 47 } 48 }
That's the left and right position of a ball sorted, now what about the up and down? This is what Lines 51-79 do:
ball.js 51 if( this.vy<0) { 52 // travelling up 53 if( (this.y-this.radius) <= 0 ) { 54 this.y = this.radius; 55 this.vy = -this.vy; 56 } 57 58 } else { 59 // travelling down 60 if( (this.y+this.radius) > bounds.height ) { 61 this.y = bounds.height-(this.radius); 62 63 // don't apply gravity if we're at rock bottom; can't accelerate downwards at this point! 64 if( Math.abs(this.vy) <2 ) { // is ground sticky, any vy < 0.8 doesn't result in a bounce! 65 this.vy = 0; 66 applyGravity = false; 67 68 //some friction to x 69 this.vx *= 0.99; 70 71 } else { 72 this.vy = -(this.vy*this.bounceLoss); 73 74 // not a perfect bounce, introduce some vx 75 this.vx+=(Math.random()-0.5)/4; 76 77 } 78 } 79 }
Going up is the easy bit, in the unlikely event that a ball is able to bounce off the top of the screen [how can it do that? Suggestions in the comments please], then the ball is positioned at the top of the screen and it’s velocity reversed without any bounce loss – that’s all done on Lines 51-56.
Lines 59-79 consider what happens when a ball is heading downwards (see condition on Line 51). The only time something needs to be done is when the ball hits or goes through the bottom of the boundary – Line 60 detects when this happens and Line 61 ensures the ball can’t go through the bottom of the screen!
There are a few things also being simulated here that I didn’t mention earlier. The purpose of which, is to give the ball even more realism. First off; ground has a bit of a sticky effect – a ball will bounce only when it has exceeds a minimum y-velocity, if it doesn't the ball, will effectively stick to the ground and not bounce. That’s the purpose of Line 64 to test for this condition. Lines 65-69 determine what happens in this case by setting vertical velocity (up/down) to zero and cancelling gravity (Line 66) e.g. the ball can’t accelerate downward through the bottom of the screen as the bottom of the screen is pushing back-up! The ball is now effectively rolling left to right along the bottom of the screen and therefore, Line 69 introduces a bit of friction to the horizontal velocity of a ball which effectively slows it down over time.
Our Ball is almost complete. There is just one thing remaining to do; apply gravity (if applicable) to accelerate it downward. That’s done on Lines 81-84:
ball.js 81 if(applyGravity) { 82 // simulate some gravity (not proper physics!) 83 this.vy+=0.5; 84 }
Phew we're there - we've implemented a class that represents a ball
but.... it doesn't do anything yet [insert sad face]. For that, we have
to instantiate a ball object, create some visual representation of it
and update the graphics each time we've called the Ball's
Update
method. This we'll be discussed in the next section...
Continue on reading to Balls! Bringing it Together...