Movement


Feb. 3, 2010 Code on Github

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.

Comments (19)

nf3 on Jan. 28, 2011, 12:13 p.m.

hi again:

a small error found:

in code snippet about 'Random movement', line 33 lacks the seq parenthesis so:

33 particle = Particle(x, y, size)

should be:

33 particle = Particle((x, y), size)

Peter on Jan. 28, 2011, 2:06 p.m.

Thanks, I've changed it. It's nice to know that someone's reading this tutorial.

Joe Nash on March 1, 2012, 4:26 p.m.

Iirc, your conversion from degree to radians is the wrong way around. 1 degree = pi/180 radians, and 1 radian = 180/pi degrees. You say that 1 degree = 180/pi radians, which would make 1 degree 57.3 radians.

Peter on March 1, 2012, 5:31 p.m.

Thanks Joe, you're absolutely right. I've corrected the text.

colin on Oct. 20, 2012, 10:20 a.m.

Hi Peter, I'm a complete beginner with a recently acquired raspberry pi and trying to learn python & pygame. I'm still getting used to the RPi so at the moment I am using a VirtualBoxVM (on a Win XP host) to run a Linux Debian OS with Pygame installed. Your tuts are really great and very easy to follow - so many thanks! I have encountered one issue though on Tut 4, Movement. The code runs fine and does everything it is supposed to with one exception. The console display will only update when my mouse pointer is placed over the console window and moved. In the case of Tut No. 4, the console display initially shows the screen with 10 static circles, then as I move my mouse (without any buttons pressed) over the console window, the circles begin to move. Stop moving the mouse and the circles stop. Its as though the program is somehow linked to detecting a mouse movement and only steps through the move() and display() functions when it detects this. I have tried various forums and google to see if this is a commom problem but nothing found - any ideas?

Peter on Oct. 22, 2012, 11:33 p.m.

Hi Colin, I think I've had that issue before. The problem is probably with the indentation. Ensure that screen.fill() and the for particle in my_particles loop are outside of the loop searching for events. Otherwise they will only be called if there is an event, such as the mouse moving over the screen.

Colin on Oct. 23, 2012, 10:29 p.m.

Many thanks Peter - perfect diagnosis! I had indeed got the screen.fill and for particle in my_particles indented under the for event loop. Two quick changes to my code and I now have circles floating without the aid of any mouse support. Amazing how obvious things are once they are pointed out (note to self: don't just check you have the same code as the example but check it is in the same place!) With that sorted, I'm off to crack gravity. Thanks again, really enjoying this set of tutorials, are you planning to do any more?

Peter on Oct. 24, 2012, 11 a.m.

I was only able to diagnose the problem because I've had it myself more than once. It is the one problem I have with Python's indentation - confusing bugs can occur when copy-and-pasting sections of code. It's even more of a problem if the code switches from spaces to tabs as then indentation can be wrong even if it looks right.

I'm not sure about making more tutorials in this series. I don't know what else to add. I have a couple of ideas, I might try. I could move it into the third dimension which would require a complete rewrite (something I've started), or maybe add joints to create a sort of skeleton to create stick figures that can move in a fluid way.

Good luck with the rest of the tutorials and beyond.

matt on May 1, 2013, 10:15 a.m.

Hi, for the line particle = Particle((x,y), size)

I get an error telling me name 'Particle' is not defined. Any ideas why this is?

Thanks,matt

Peter on May 2, 2013, 12:57 p.m.

Hi Matt,

It's difficult to say for sure without seeing your code. Do you definitely have the Particle class above that line and it's definitely spelt correct? The only other thing I can think of is that there might be some problem with the indentation somewhere so the Particle class is out of scope.

Fred on July 4, 2013, 2:35 p.m.

hey peter thanks for these tutorials! Can you make a tutorial on rotating objects??

Charlse on Jan. 16, 2014, 1:59 p.m.

HI, i really enjoy these tutorials. Thanks

I don't think it matters much, but shouldn't the line 22 "self.x += math.sin(self.angle) * self.speed" use cos instead of sin? since sin is o/h and once we mutiply it by the speed(the hyptenuse) it would give as the chage in y, not the change in x. Similarly line 23 should have used sin instead of cos, using the same reasoning.

John on Oct. 3, 2014, 12:28 a.m.

Hi,

First of all, thank you for creating this tutorial site. It helps me see python programming in different light. My I.Q has increased by one point.

Anyway, I wanted to share something with you. I improved your code a bit to slow down the screen update. Without the modification, the circles would run off the screen before I had a chance to see what happen. I am using a fast computer with Python 3.3 which is why.

So I added a timer to slow things down a bit.

running = True
clock = pygame.time.Clock() <-- add this

while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

screen.fill(background_colour)

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

clock.tick(60) <-- add this
pygame.display.flip()

pygame.quit() <-- optional but useful

Don't set the clock.tick( ) more than 60, you won't be able to see what happen. Too fast for your eyes to see. You can set it to 20 for slowness.

Tanuj Rastogi on Jan. 23, 2015, 5:43 p.m.

Hi. This is quite an interesting tutorial. I appriciate you making this.

I do have a one small question though: You have measured the angle starting from negative y-axis and running clock-wise. I do find this quite counter-intutive, even though I know its not wrong to do so. Do you have any rationale/reason for choosing this convention? Wouldn't starting from +ve x-axis and running clock-wise be more convinient (as it is sync with regular convention used in cartesian plane).

Sorry if my question feels like a bit of nitpicking, that isn't my intenstion.

Matthew on Feb. 10, 2015, 1:29 a.m.

Hi Peter,

Great site. I've been having trouble with the trig for my own programs, and many of the sites I've found are over-complicated or are written for programmers and math majors. Unlike them, I'm not a programmer or a math person but I do enjoy python as a hobby. So far, your site has been much more of a benefit to my undertsanding of python (and trig!) due to it's ease and simplistic examples.

Quick question, though. If you wanted to set the angle of a particle to some non-random number (like have a single particle move towards where the mouse was clicked), how would you accomplish this? I'm not sure I understand how to represent the self.angle using delta x and delta y based on a range of zero to 2*pi.

It's quite easy to find the current position (x, y) of the center of the particle and to find the coords of where a mouse click occurs and then to calculate the tangent of that angle. But how do you translate that into a directional vector and move a particle along a line at that angle?

Thanks, again.

Matthew

Anonymous on Nov. 25, 2017, 2:06 p.m.

This tutorial is so perfectly targeted on where I want to be learning right now. At the point at which you say that for a long time you worked with a simple dx and dy variable, but then you graduated beyond that... well, that's where I stand -- I know I need to graduate beyond this. You should know that you made a real, true resource here...

Chris on Jan. 27, 2018, 6:01 p.m.

Hi Peter,

Wonderful tutorials! I realize these are a bit old, I hope you still read comments! I am having trouble because at this stage of the tutorial, pygame.draw.circle complains that it expects integer values for x and y, but gets a float. Do you know if pygame has changed in this respect? I have gotten around this by wrapping the expression that updates the x and y values in move function in int(), but I wonder if there is a better way.

Chris on Jan. 27, 2018, 6:04 p.m.

Whoops, never mind my last question-- I see you put the int() in the display method. Thanks again!

Sloke on May 26, 2018, 9:50 a.m.

Hello,

I think that it would be better if you mention that 0 radians angle is upward in computers. I learned it the hard way. :p

By the way, Great Tutorial. Thank you very very much for this. :D

Leave a comment

cancel reply