Pretty much by definition, all simulations have values which change over time. One value that often changes is the position of an object, in which case the object is moving. In this tutorial, we will:

  • Give our particles speed and direction (a velocity vector)
  • Use basic trigonometry to convert vectors into movement
  • Make the particles move randomly

Our simulation is a discrete time simulation, which means that we split time into individual steps. At each step we update the simulation a bit then display the new situation. We keep updating the simulation until the user exits.

To keep our simulation running, we write our code into the infinite while loop that we've already created. The first thing we do therefore is move the following block of code from before the while loop to inside it. This will have no effect on how the program runs but allows us to add additional function calls later.

for particle in my_particles:
    particle.display()
pygame.display.flip()

Representing movement

The simplest way to represent movement is to create two attributes: dx and dy. Then during each pass through the loop, add dx to x and dy to y. So, for example, if a particle has dx=2 and dy=1, it will follow a shallow diagonal, from left to right and top to bottom. This is the method I used for a long time - it is simple and fine for many situations. (EDIT: I have since returned to this method - it is a lot simpler and more efficient. But it's good learn both ways.)

My preferred method now is to create attributes to represent speed and direction (i.e. velocity). This requires a bit more work to start with but makes life a lot easier later one. This method is also good for creating objects that have a constant speed, but varying direction. I actually first started using this method when trying to create an ant simulation. The ants have a creepily realistic movement when given a constant speed and randomly changing their a direction every few seconds.

So let's give our particle object a speed and a direction.

self.speed = 0.01
self.angle = 0

The math module

Since we’re going to use some trigonometry, we need to import Python’s math module. Like the random module, this is part of main Python program so there's no need to download anything extra.

import math

We now have access to various mathematical functions, such as sine and cosine, which we'll use shortly.

Movement vectors

We now need to add a move() function to our particle object. This function will change the x, y coordinates of the particle based on its speed and the angle of its movement.

The diagram below illustrates how we can calculate the change in x and y. I find it simplest to consider an an angle of 0 to be pointing upwards, despite the fact that y-axis actually pointing downwards in computer graphics.

Calculating the change in position given an angle and distance

[EDIT: this is not the standard way to do things. In the standard way, you measure an angle from the x-axis, going clockwise, which results in a change of x coordinate of $\text{speed} \cdot cos(\theta)$ and a change in y coordinate of $\text{speed} \cdot sin(\theta)$.]

To calculate the change in x and y, we use some secondary school-level trigonometry as shown in the code below. Remember to minus the y to take into account the downward pointing y-axis. Although it doesn't make much difference at the moment you will have to be consistent with your signs later.

def move(self):
    self.x += math.sin(self.angle) * self.speed
    self.y -= math.cos(self.angle) * self.speed

Another point to bear in mind is that the angles are all in radians. If you’re used to working with degrees then this might be a little confusing; just remember that $1 \text{ radian} = \frac{180^\circ}{\pi}$. Therefore if you want to make the particle move forwards (left to right) along the screen, then its angle should be $\frac{\pi}{2}$ (90°).

So in Python we set the angle to 90° like so:

self.angle = math.pi / 2

Now we can we call the particles' move() function during the loop immediately before calling their display() function.

for particle in my_particles:
    particle.move()
    particle.display()

If we run this program now, we’ll see the circles moving right, leaving a smear across the screen. The reason the particles smear is that once Pygame has drawn something on the window it will stay there unless drawn over.

Circles smearing as they move right

The easiest way to clear the circles from the previous time step is to fill the whole screen with the background colour. We can do this by simply moving the screen.fill(background_colour) command into the loop. The effect of movement is therefore achieved by drawing a particle, then clearing it and drawing a short distance away.

You might have also got a deprecation warning telling you "integer argument expected, got float" followed by the pygame.draw.circle() function. The reason, as you may have guessed, is that this pygame can only draw circles at integer x, y coordinates and after we move the circles, their coordinates become floating point numbers. Although the Python deals with the problem perfectly well, we should convert the x, y coordinates to integers first:

pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)

Random movement

If you run this program now, you’ll see all the circles moving rightward at the same speed, so it will look like they are all draw on a single moving surface. We can making things more interesting by giving each particle a random speed and direction. We could do this by defining the speed and angle attributes as random in the Particle class, but I prefer to set these values (the default behaviour) to 0. We can update them once the individual objects have been created, by altering our particle-creating loop:

for n in range(number_of_particles):
    size = random.randint(10, 20)
    x = random.randint(size, width-size)
    y = random.randint(size, height-size)

    particle = Particle((x, y), size)
    particle.speed = random.random()
    particle.angle = random.uniform(0, math.pi*2)

    my_particles.append(particle)

Notice that we use the random.random() function to generate a speed between 0 and 1, and the random.uniform() function to create a random angle between 0 and 2 * pi, which covers all angles. If you run the program now, the circles will fly off the screen at random angles and speeds. In the next tutorial we'll figure out how to keep the particle within the bounds of the window.