Adding simulated gravity, elasticity, and friction to your objects adds a sense of realism that otherwise would not exist in 2D. These properties are major forces in nature that people feel and understand at nearly every moment of their lives. This means that people who play games expect objects to act in a particular way when these properties are applied. Our job is to simulate those effects as closely as possible, while minimizing the processing power necessary to create them. While there are some very complicated physics equations we could use to create these effects, we will use simplified versions that work well with the limited resources available to HTML5 Canvas in a web browser.
A very simple, yet seemingly realistic gravitational effect can be
achieved by applying a constant gravity value to the y
velocity of an object moving on a vector. To
do this, select a value for gravity, such as .1
, and then add that value to the y
velocity of your object on every call to
drawScreen()
.
For this example, let’s simulate a ball with a radius
of 15
pixels being shot from a cannon that rests
near the bottom of the canvas. The ball will move at a speed
of 4
pixels per frame, with an angle
of
305
degrees. This means it will move
up and to the right on the canvas. If we did not apply any gravity, the
ball would simply keep moving on that vector until it left the canvas
(actually, it would keep moving, we just would not see it any
longer).
You have seen the code to create an effect like this already. In
the canvasApp()
function, we would
create the starting variables like this:
var speed = 4; var angle = 305; var radians = angle * Math.PI/ 180; var radius = 15; var vx = Math.cos(radians) * speed; var vy = Math.sin(radians) * speed;
Next, we create the starting point for the ball as p1
, and then create a dynamic object that
holds all the values we created for the ball
object:
var p1 = {x:20,y:theCanvas.width-radius}; var ball = {x:p1.x, y:p1.y, velocityx: vx, velocityy:vy, radius:radius};
If we want to add gravity to the application, we would first
create a new variable named gravity
and set it to a constant value of .1
:
var gravity = .1;
Next, in the drawScreen()
function, we apply this gravity value to the ball
object when it is drawn to the canvas
(ball.velocityy += gravity
). We want
the ball to stop moving when it hits the “ground” (the bottom of the
canvas), so we test to see whether the y
position of the ball
plus the radius
of the ball (the outer edge) has passed
the bottom of the canvas (ball.y + ball.radius
<= theCanvas.height
). If so, we stop the ball’s
movement:
if (ball.y + ball.radius <= theCanvas.height) { ball.velocityy += gravity; } else { ball.velocityx = 0; ball.velocityy = 0; ball.y = theCanvas.height - ball.radius; }
Next, we apply the constant x
velocity and the new y
velocity to
ball
, and draw it to the
canvas:
ball.y += ball.velocityy; ball.x += ball.velocityx; context.fillStyle = "#000000"; context.beginPath(); context.arc(ball.x,ball.y,ball.radius,0,Math.PI*2,true); context.closePath(); context.fill();
Figure 5-17 shows what the path looks like when simple gravity is applied to a ball moving on a vector. We have added the points to illustrate the path.
You can test out Example 5-14 with the file CH5EX14.html in the code distribution, or type in the full code listing below.
Example 5-14. Simple gravity
a<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX14: Simple Gravity</title> <script src="modernizr-1.6.min.js"></script> <script type="text/javascript"> window.addEventListener('load', eventWindowLoaded, false); function eventWindowLoaded() { canvasApp(); } function canvasSupport () { return Modernizr.canvas; } function canvasApp() { if (!canvasSupport()) { return; } function drawScreen () { context.fillStyle = '#EEEEEE'; context.fillRect(0, 0, theCanvas.width, theCanvas.height); //Box context.strokeStyle = '#000000'; context.strokeRect(1, 1, theCanvas.width-2, theCanvas.height-2); if (ball.y + ball.radius <= theCanvas.height) { ball.velocityy += gravity; } else { ball.velocityx = 0; ball.velocityy = 0; ball.y = theCanvas.height - ball.radius; } ball.y += ball.velocityy; ball.x += ball.velocityx; context.fillStyle = "#000000"; context.beginPath(); context.arc(ball.x,ball.y,ball.radius,0,Math.PI*2,true); context.closePath(); context.fill(); } var speed = 4; var gravity = .1; var angle = 305; var radians = angle * Math.PI/ 180; var radius = 15; var vx = Math.cos(radians) * speed; var vy = Math.sin(radians) * speed; theCanvas = document.getElementById("canvasOne"); context = theCanvas.getContext("2d"); var p1 = {x:20,y:theCanvas.width-radius}; var ball = {x:p1.x, y:p1.y, velocityx: vx, velocityy:vy, radius:radius}; setInterval(drawScreen, 33); } </script> </head> <body> <div style="position: absolute; top: 50px; left: 50px;"> <canvas id="canvasOne" width="500" height="500"> Your browser does not support HTML5 Canvas. </canvas> </div> </body> </html>
The last example showed what a cannonball might look like if it was shot out, landed on a surface, and stuck there with no reaction. However, even a heavy cannonball will bounce when it hits the ground.
To create a bouncing effect we do not have to change the code very
much at all. In drawScreen()
, we
first apply gravity
on every frame;
then, instead of stopping the ball if it hits the bottom of the canvas,
we simply need to reverse the y
velocity of ball
when it hits the
ground.
In CH5EX14.html you would replace this code…
if (ball.y + ball.radius <= theCanvas.height) { ball.velocityy += gravity; } else { ball.velocityx = 0; ball.velocityy = 0; ball.y = theCanvas.height - ball.radius; }
…with this:
ball.velocityy += gravity; if ((ball.y + ball.radius) > theCanvas.height) { ball.velocityy = -(ball.velocityy) }
This code will send the ball bouncing back “up” the canvas. Since
it is still traveling on the vector, and gravity is applied every time
drawScreen()
is called, the ball will
eventually come down again as the applied gravity
overtakes the reversed y
velocity.
Figure 5-18 shows what the cannonball looks like when the bounce is applied.
To achieve a nice-looking bounce for this example, we also
changed the angle
of the vector in
canvasApp()
to 295
:
var angle = 295;
Example 5-15 offers the full code.
Example 5-15. Simple gravity with a bounce
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX15: Gravity With A Bounce</title> <script src="modernizr-1.6.min.js"></script> <script type="text/javascript"> window.addEventListener('load', eventWindowLoaded, false); function eventWindowLoaded() { canvasApp(); } function canvasSupport () { return Modernizr.canvas; } function canvasApp() { if (!canvasSupport()) { return; } function drawScreen () { context.fillStyle = '#EEEEEE'; context.fillRect(0, 0, theCanvas.width, theCanvas.height); //Box context.strokeStyle = '#000000'; context.strokeRect(1, 1, theCanvas.width-2, theCanvas.height-2); ball.velocityy += gravity; if ((ball.y + ball.radius) > theCanvas.height) { ball.velocityy = -(ball.velocityy) } ball.y += ball.velocityy; ball.x += ball.velocityx; context.fillStyle = "#000000"; context.beginPath(); context.arc(ball.x,ball.y,ball.radius,0,Math.PI*2,true); context.closePath(); context.fill(); } var speed = 5; var gravity = .1; var angle = 295; var radians = angle * Math.PI/ 180; var radius = 15; var vx = Math.cos(radians) * speed; var vy = Math.sin(radians) * speed; theCanvas = document.getElementById("canvasOne"); context = theCanvas.getContext("2d"); var p1 = {x:20,y:theCanvas.width-radius}; var ball = {x:p1.x, y:p1.y, velocityx: vx, velocityy:vy, radius:radius}; setInterval(drawScreen, 33); } </script> </head> <body> <div style="position: absolute; top: 50px; left: 50px;"> <canvas id="canvasOne" width="500" height="500"> Your browser does not support HTML5 Canvas. </canvas> </div> </body> </html>
In physics, the elasticity of a bouncing ball refers to how much energy is conserved when a ball bounces off a surface. We already covered a bit about conservation of energy when we discussed balls colliding, but when we are simulating objects falling, we need to take a slightly different path with our code. In Example 5-15, we applied 100% elasticity and the ball bounced forever (actually, this was only implied since we did not consider elasticity at all). However, in real life, balls usually lose some of their energy every time they bounce off a surface. The amount of energy conserved depends on the material the ball is made from, as well as the surface it is bouncing on. For example, a rubber Super Ball is much more elastic than a cannonball and will bounce higher on the first bounce off a surface. Both will bounce higher off a concrete surface than a surface made of thick mud. Eventually, both will come to rest on the surface as all the energy is transferred away from the ball.
We can simulate simple elasticity by applying a constant value to
the ball when it bounces off the ground. For this example, we will set
the speed
of the ball to 6
pixels per frame, and the angle
to 285
. We will keep our gravity
at .1
, but set a new variable named elasticity
to .5
. To make this more straightforward, we will
also assume that the surface the ball is bouncing on does not add or
subtract from the elasticity of the ball.
In canvasApp()
we would set the
new properties like this:
var speed = 6; var gravity = .1; var elasticity = .5; var angle = 285;
We then add the new elasticity
property to the ball
object because,
unlike gravity
, elasticity describes
a property of an object, not the entire world it resides within. This
means that having multiple balls with different values for elasticity
would be very easy to implement:
var ball = {x:p1.x, y:p1.y, velocityx: vx, velocityy:vy, radius:radius, elasticity: elasticity};
In the drawScreen()
function,
we still add the gravity
value to the
y
velocity (velocityy
). However, instead of simply
reversing the y
velocity when the
ball
hits the bottom of the canvas,
we also multiply the y
velocity by
the elasticity
value stored in the
ball.elasticity
property. This
applies the elasticity to the bounce, preserving the y
velocity by the percentage value of elasticity
for the object:
ball.velocityy += gravity; if ((ball.y + ball.radius) > theCanvas.height) { ball.velocityy = -(ball.velocityy)*ball.elasticity; } ball.y += ball.velocityy; ball.x += ball.velocityx;
In Figure 5-19 you can see what this application looks like when executed in a web browser.
With gravity
applied, the
bounce is not exactly as you might expect. Gravity is always pulling
down on our object, so the effect of a loss of y
velocity due to an elastic bounce is
pronounced.
The full code is shown in Example 5-16.
Example 5-16. Simple gravity with bounce and elasticity
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX16: Gravity With A Vector With Bounce And Elasticity</title> <script src="modernizr-1.6.min.js"></script> <script type="text/javascript"> window.addEventListener('load', eventWindowLoaded, false); function eventWindowLoaded() { canvasApp(); } function canvasSupport () { return Modernizr.canvas; } function canvasApp() { if (!canvasSupport()) { return; } function drawScreen () { context.fillStyle = '#EEEEEE'; context.fillRect(0, 0, theCanvas.width, theCanvas.height); //Box context.strokeStyle = '#000000'; context.strokeRect(1, 1, theCanvas.width-2, theCanvas.height-2); ball.velocityy += gravity; if ((ball.y + ball.radius) > theCanvas.height) { ball.velocityy = -(ball.velocityy)*ball.elasticity; } ball.y += ball.velocityy; ball.x += ball.velocityx; context.fillStyle = "#000000"; context.beginPath(); context.arc(ball.x,ball.y,ball.radius,0,Math.PI*2,true); context.closePath(); context.fill(); } var speed = 6; var gravity = .1; var elasticity = .5; var angle = 285; var radians = angle * Math.PI/ 180; var radius = 15; var vx = Math.cos(radians) * speed; var vy = Math.sin(radians) * speed; theCanvas = document.getElementById("canvasOne"); context = theCanvas.getContext("2d"); var p1 = {x:20,y:theCanvas.width-radius}; var ball = {x:p1.x, y:p1.y, velocityx: vx, velocityy:vy, radius:radius, elasticity: elasticity}; setInterval(drawScreen, 33); } </script> </head> <body> <div style="position: absolute; top: 50px; left: 50px;"> <canvas id="canvasOne" width="500" height="500"> Your browser does not support HTML5 Canvas. </canvas> </div> </body> </html>
Now that we have a ball traveling on a vector that is affected by
both gravity and elasticity, we have one more element to add to make the
animation more realistic. In the previous example, the y
velocity was affected by gravity and
elasticity, but the ball still traveled on the x-axis without any
degradation in velocity. We will fix this issue now by adding friction
into the equation.
In physics, friction is a force that resists
the motion of an object. We have already discussed friction as it
applies to colliding balls, and this implementation is similar except
that it affects only the x
velocity.
For our purposes, we will achieve simple friction by degrading the
x
velocity as gravity degrades the
y
velocity.
Taking the code from Example 5-16, in canvasApp()
we create a new variable named
friction
. This is the amount of
pixels to degrade the x
velocity on
every frame:
var friction = .008;
Notice that the amount is quite small. Friction does not have to
be a large value to look realistic—it just needs to be applied uniformly
each time drawScreen()
is called. In
drawScreen()
, we apply friction
to the x
velocity like this:
ball.velocityx = ball.velocityx - ( ball.velocityx*friction);
This is the same type of proportional application of friction we
used with the colliding balls, but again, this time we applied it only
to the x
velocity.
Figure 5-20 shows what this final version of our application looks like when executed in a web browser.
Example 5-17 gives the full code for CH5EX17.html, the final code of our simple gravity, simple elasticity, and simple friction example.
Example 5-17. Gravity with a vector with bounce friction
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CH5EX17: Gravity With A Vector With Bounce Friction</title> <script src="modernizr-1.6.min.js"></script> <script type="text/javascript"> window.addEventListener('load', eventWindowLoaded, false); function eventWindowLoaded() { canvasApp(); } function canvasSupport () { return Modernizr.canvas; } function canvasApp() { if (!canvasSupport()) { return; } function drawScreen () { context.fillStyle = '#EEEEEE'; context.fillRect(0, 0, theCanvas.width, theCanvas.height); //Box context.strokeStyle = '#000000'; context.strokeRect(1, 1, theCanvas.width-2, theCanvas.height-2); ball.velocityx = ball.velocityx - ( ball.velocityx*friction); ball.velocityy += gravity; if ((ball.y + ball.radius) > theCanvas.height) { ball.velocityy = -(ball.velocityy)*ball.elasticity; } ball.y += ball.velocityy; ball.x += ball.velocityx; context.fillStyle = "#000000"; context.beginPath(); context.arc(ball.x,ball.y,ball.radius,0,Math.PI*2,true); context.closePath(); context.fill(); } var speed = 6; var gravity = .1; var friction = .008; var elasticity = .5; var angle = 285; var radians = angle * Math.PI/ 180; var radius = 15; var vx = Math.cos(radians) * speed; var vy = Math.sin(radians) * speed; theCanvas = document.getElementById("canvasOne"); context = theCanvas.getContext("2d"); var p1 = {x:20,y:theCanvas.width-radius}; var ball = {x:p1.x, y:p1.y, velocityx: vx, velocityy:vy, radius:radius, elasticity: elasticity}; setInterval(drawScreen, 33); } </script> </head> <body> <div style="position: absolute; top: 50px; left: 50px;"> <canvas id="canvasOne" width="500" height="500"> Your browser does not support HTML5 Canvas. </canvas> </div> </body> </html>