Introduction

By the end of this tutorial we will have a complete simulation of a classical problem in physics: the trajectory of a projectile. In this tutorial, we will:

  • Create a function to add vectors
  • Create a vector to represent gravity
  • Add drag and elasticity

And after that we should have a simulation that looks a bit like this:

As usual the full code can be found by clicking the Github link at the top of the page.

Gravity works by exerting an constant downward force to each of the particles in the simulation. If we had stored the particles' movement as two positions, dx and dy (as discussed here), then we could simply add a constant to the dy value. However, since we’re using vectors, it’s a bit more complex because we need to add two vectors. Once we have a function to add vectors (which took me a while to work out, but is probably the most useful thing in these tutorials), everything else will be a lot easier.

Adding Vectors

The addVectors() function takes two vectors (each an angle and a length), and returns single, combined a vector. First we move along one vector, then along the other to find the x,y coordinates for where the particle would end up (labelled (x, y) on the diagram below).

def addVectors((angle1, length1), (angle2, length2)):
    x = math.sin(angle1) * length1 + math.sin(angle2) * length2
    y = math.cos(angle1) * length1 + math.cos(angle2) * length2

We then calculate the vector that gets there directly. To do this we construct a right-angle triangle as shown in the image below. Then we use good old trigonometry and a couple of handy functions from Python’s math module, which I’ve only recently discovered.

The new vector length (speed of the particle) is equal to the hypotenuse of the triangle, which can be calculated using math.hypot(). This takes an x,y coordinate and calculates its distance from the origin (0,0). Note that while the position of our particle on the screen is not (0,0), all the vectors are relative to the particle's position, so can be considered to begin at 0,0.

Diagram showing how to add two polar vectors.

The angle of the new vector is slightly more complex to calculate. First, we find the angle in the triangle, by calculating the arctangent of y/x. We could do this using the math.atan() function but then we would then need to deal with the case of x=0 and work out the sign of angle. However, Python provides us with a handy function math.atan2(), which takes the x, y coordinates, works out the sign of the angle for us and behaves correctly when x=0. Once we have the angle of the triangle, we subtract it from pi/2 to calculate the angle of the vector.

length = math.hypot(x, y)
angle = 0.5 * math.pi - math.atan2(y, x)
return (angle, length)

Gravity

Now we can create a vector for gravity: the angle is pi, which is downward and I chose a magnitude of 0.002 purely by experimentation. Feel free to change it:

gravity = (math.pi, 0.002)

Then, in the Particle's move() function, we add the gravity vector to the particle’s vector:

(self.angle, self.speed) = addVectors((self.angle, self.speed), gravity)

Friction

To complete the trajectory simulation and stop the particles from bouncing forever we need to add two more physical phenomena: drag and elasticity.

Drag represents the loss of speed particles experience as they move through the air - the faster a particle is moving, the more speed is lost. I find it simpler (and computationally quicker) to define a drag variable that represents the inverse of drag. We multiple a particle's speed by this value at each time unit, thus the smaller the value, the more speed is lost. Elasticity represents the loss of speed a particle experiences when it hits a boundary.

You can play with the values to see what looks reasonable, though both should be between 0 and 1. I found that 0.999 and 0.75 respectively work quite well.

drag = 0.999
elasticity = 0.75

To the Particle's move() function add:

self.speed *= drag

Another option would be to multiply by a factor that varied inversely with the particle's size (e.g. self.speed *= (1-self.size/10000.0)), but I don't think that's necessary.

After each of the four boundary conditions, add:

self.speed *= elasticity
self.speed *= elasticity
self.speed *= elasticity
self.speed *= elasticity

And there you have it: a complete simulation of a projectile's trajectory. And we haven't had to explicitly solve any of the equations of motion. Try running the simulation several times to see what happens. You might find it easier to set the y coordinate to 20, so the particle always starts at the top of the simulation. In the next tutorial, we add some user interaction so you can pick up, drop and throw the ball (particle).