When creating a simulation you will often want to make it bounded, to avoid simulating an infinite region. In this simulation, a realistic, physical solution is to add virtual walls that particles bounce off. In this tutorial, we will:

  • Test whether particles have moved out of the window
  • Move such particles to where they would have bounced
  • Change their direction appropriately

Another solution would be to bend a 2D simulation into virtual torus (ring doughnut shape). We do this my making particles that leave one side of the simulation appear on the opposite side.

Exceeding boundaries

The first thing our bounce() function needs to do is test whether a particle has gone past a boundary. The four boundaries are at:

  • x=0 (the left wall)
  • x=width (the right wall)
  • y=0 (the ceiling)
  • y=height (the floor)

Since this simulation has discrete steps of time, we unlikely to catch a particle at the exactly point that it 'hits' a boundary, but rather at a point when it has travelled a bit beyond the boundary. If the speed of the particle is low, then the particle is unlikely to have gone much beyond the boundary (the maximum distance it will have exceeded the boundaries is, in fact, equal to its speed). We could chose to ignore the discrepancy and simply (especially if our simulation is becoming too computationally intense), and simply reflect the particle angle, but we might as well be accurate for now.

Therefore, if a particle exceeds a boundary, we first calculate by how much it has exceeded it (which I'll call d). For example, if the particle's x value is greater than the window's width minus the particle's size (it has gone beyond the right wall), then we calculate d:

d = self.x - (width - self.size)

We then reflect the position in the boundary (i.e. bounce it) by setting the x coordinate as below:

self.x = (width - self.size) - d

This simplifies to:

self.x = 2*(width - self.size) - self.x

So we don't actually need to calculate variable d. Replacing width with 0 calculates the x position when the particle crosses the left wall. The y coordinates can be calculated in a similar way.

Graph of bouncing

The most important feature of bouncing is reflecting its angle in the boundary, which is where our use of vectors starts to become useful (though its more useful when the boundaries aren't straight). A vertical boundary is has an angle of 0, while a horizontal boundary has an angle of pi. We therefore reflect vertically bouncing particles by subtracting their current angle from 0 and from pi in the case of horizontal bouncing.

The final bounce function should look like this:

def bounce(self):
    if self.x > width - self.size:
        self.x = 2*(width - self.size) - self.x
        self.angle = - self.angle

    elif self.x < self.size:
        self.x = 2*self.size - self.x
        self.angle = - self.angle

    if self.y > height - self.size:
        self.y = 2*(height - self.size) - self.y
        self.angle = math.pi - self.angle

    elif self.y < self.size:
        self.y = 2*self.size - self.y
        self.angle = math.pi - self.angle

Finally, don't forget to call the bounce() function after calling the move() function and before calling the display() function.

    for particle in my_particles:

Before running the simulation, I would also increase the width and height to 400 to make things easier to see. When you run this simulation, you should now see ten particles bouncing around at different speeds, but completely ignoring one another. In a later tutorial, we’ll allow the particles to interact with one another.

particle_tutorial_5.txt1.9 KB



A superb set of Tutorials. I haven't written any code since Sinclair Basic on the ZX Spectrum. My daughter has expressed an interest in learning to code games and whilst looking at alternative languages I came across your site so I thought Python, Pygame and your physics  Tutorials might be a good place to start.

We are using Python 3.2 and have been working through your Tutorials over the couple of evenings. In Tutorial 5 we receive a syntax error in the def addVectors line which appears to be the same syntax error that arose on the second Tutorial relating to 'tuple unpacking'. Whilst not having a clue what tuple unpacking is, we have used the same principle you suggest there and recoded as follows;

def addVectors(vector1, vector2):
    angle1, length1 = vector1
    angle2, lengthe2 = vector2

This appears to work fine.

Thanks again for sharing your work.




Hi Gary,

Thanks for your comment. I also started out programming Basic on the ZX Spectrum. I think programming is a great skill to learn, and coding games is probably the most fun (even if your games are necessarily much simpler than the ones you can buy). I definitely recommend Python as a language to learn programming. You might also want look at which goes into a lot more detail.

Hopefully the physics simulation is also a good way to get a more intuitive understanding of physics and mathematics. I certainly gained a much better understanding of what trigonometry was and why it had been invented after working out how to code this simulation.

As for tuple unpacking, a tuple is like a list you can't modify and is convenient for "packing" variables, such as angle and length into a single variable, vector. It seems that in Python 3 they removed the ability to unpack the variables in the parameter list. The way you've solved the problem is correct. One of these days, I'll make the switch to Python 3. I hope it's not too difficult to rewrite what I've done here.

All the best to you and your daughter and good luck with the programming.


i was wondering about the bounce() method.
do we need to set the "x" value? we could simply could change the direction of the vector when it matches any boundary condition?
Suppose, for right wall the following code works pretty well :

        if self.x > width - self.size:
self.angle = - self.angle

Thanks a barrel for your wonderful tutorials :)
just love them :D

Hi Pallab, you can write the code that way if you like, and I often do as it's simpler and realistically doesn't make much difference. In fact, if you remove the code that sets the x and y values, you can reduce the bounce code to two conditionals that check whether either x or y is beyond a boundary.

Hello Peter! I want to thank you for helping me make my future dreams! Great set of tutorials!

Awesome Tutorial.....
nice work...


    I am not sure if this is normal, but even when I set speed as being random.uniform(0, .1) the particles are moving incredibly fast. The result is that I am frankly unable to tell if they are giving any regard at all to the bounds. I admit that I made a few changes to the script you seem to have so far, but I think mine should work just as well and do the same thing.  I am pasting it so you can take a look. Please note that I am using python 3.3, and some of the sytax was changed with respect to that.

import pygame
import math
import random
(width, height) = (400, 400)
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Hello World")
background_colour = (255,255,255)

class Particle:
    def __init__(self, x, y, size, speed, angle):
        self.x = x
        self.y = y
        self.size = size
        self.speed = speed
        self.angle = angle
        self.colour = (0, 0, 255)
        self.thickness = 1
    def display(self):, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)
    def move(self):
        self.x += math.sin(self.angle) * self.speed
        self.y -= math.cos(self.angle) * self.speed
    def bounce(self):
        if self.x > width - self.size:
            self.x = 2*(width - self.size) - self.x
            self.angle = - self.angle
        elif self.x < self.size:
            self.x = 2*self.size - self.x
            self.angle = - self.angle
        if self.y > height - self.size:
            self.y = 2*(height - self.size) - self.y
            self.angle = math.pi - self.angle
        elif self.y < self.size:
            self.y = 2*self.size - self.y
            self.angle = math.pi - self.angle

number_of_particles = 10
my_particles = []

for n in range(number_of_particles): # this loop adds particles with random sizes, speeds, etc
    size = random.randint(10, 20)
    x = random.randint(size, width - size)
    y = random.randint(size, height - size)
    a = random.uniform(0, .1) # speed
    b = random.uniform(0, 2 * math.pi) #angle
    my_particles.append(Particle(x, y, size, a, b))

running = True
    while running:
        for particle in my_particles:

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

Love your tutorials!

I have a question though... I don't quite understand is what 'math.atan2(dy, dx)' is doing exactly. Is it the same as going: 1 / (math.tan(dy/dx))?

Thanks again for the great tutorials.


What are the advantages of your method compared to this one :

Thanks for the answer.

Dear Peter,

Thank you for this impressive tutorial, I found it extremely helpful thus far. Currently, I'm on the Boundaries section where I was slightly confused. It seems as if the speed (how fast the particles move in real time) depend solely on how fast the clock cycles of the computer is (and thus how fast it runs the while statement). In this case, the particle speed becomes pixel/clock cycle rather than pixel/second.

Are my assumptions correct and if so, is the proposed fix I give the most efficient one? If my assumption above is correct then we would run into several issues such as variable speeds for different work stations (this would be bad for gaming). The fix I proposed was to have a cap for the speed, perhaps import that time library and run the loop only every time a set increment of time has passed. This forces the particle to a set speed no matter how fast your computer is. 

Also, could you talk a bit more about the math you used to "bounce" the particle?



Why keep as:

self.x = 2*(width - self.size) - self.x

Doesn't that just become:

self.x = width - self.size



Post new comment

The content of this field is kept private and will not be shown publicly.