Mouse interactions

18 Sep 2010 Code on Github


In the previous tutorial, we completed a simulation of a particle's trajectory. Unfortunately, once we'd added drag and friction, the simulation came to a halt pretty quickly. In order to try out different starting conditions, we have to edit the code and restart the simulation. It would be much simpler if we could reach into our virtual world and interact with it directly. In this tutorial, we will:

  • Add the ability to select particles with a mouse click
  • Add the ability to move particles with the mouse
  • Add the ability to throw particles with the mouse

As usual the complete code can be found by clicking the Github link at the top of this post. By the end of this tutorial, you too will be able to fling the particles about like this:

Getting the mouse position

Firstly, set the number_of_particles to 3, which will give us a few to choose between without cluttering the screen.

In order to test whether the user has selected a particle, we need to know where they have clicked and we do this by using pygame.mouse.get_pos(). As the name suggests, this function returns the position of the mouse in the Pygame window as an x, y coordinate. For now we only need to know the position of the mouse when the user clicks. We test this by monitoring Pygame events as we did in tutorial 1 using pygame.event.get(). Since we are already calling this function, looking for QUIT events, we might as well search for MOUSEBUTTONDOWN events at the same time.

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        running = False
    if event.type == pygame.MOUSEBUTTONDOWN:
        (mouseX, mouseY) = pygame.mouse.get_pos()
        print mouseX, mouseY

Now, whenever we click in the Pygame window, the position of the mouse is printed to the command line. Note that the MOUSEBUTTONDOWN event is only found once per mouse click; it is not repeatedly found if the mouse is held down.

Selecting particles

Rather than print the position of the mouse, we want to test whether the mouse is within the boundary of a particle. Since the particles are circular, this is very straightforward: we test all the particles to see whether the distance from the mouse to particle's centre is less than that particle's size. We can do this with our old friend, math.hypot().

def findParticle(particles, x, y):
    for p in particles:
        if math.hypot(p.x-x, p.y-y) <= p.size:
            return p
    return None

The return None command is not strictly necessary as Python will return None if it reaches the end of a function without finding a return command. Also note, that if the function finds that the first particle in the array is selected, it will return that particle without bothering to check the other particles.

We can test our findParticle() function by replacing the print function with:

selected_particle = findParticle(my_particles, mouseX, mouseY)
if selected_particle:
    selected_particle.colour = (255,0,0)

If you click on a particle now, its colour should change to red. You can remove this code now if you want.

When we have selected a particle, we want it to stop moving, so change the code that makes particle move to:

for particle in my_particles:
    if particle != selected_particle:

And add selected_particle = None before the while loop starts so the variable exists before a mouse click. We also need to cancel the selection once the mouse button is released, so below the code checking for MOUSEBUTTONDOWN events, add:

elif event.type == pygame.MOUSEBUTTONUP:
    selected_particle = None

Dragging particles

We can now select a particle and stop it, but we want to be able to drag it somewhere. We do this by making the x, y coordinates of a selected particle (if there is one) equal the coordinates of the mouse.

if selected_particle:
    (mouseX, mouseY) = pygame.mouse.get_pos()
    selected_particle.x = mouseX
    selected_particle.y = mouseY

We can now pick up and drag the particles. However, when we let go, the particles drop to the ground in a disappointing sort of way. Instinctively, we expect the particle to be released with a speed equal to the speed the mouse was moving at the time. For this we need to measure the difference between the position of the mouse and the particle and creating a vector that joins them. We can then setting the particle's angle and speed to be this vector. Replace, the above code with the below:

if selected_particle:
   (mouseX, mouseY) = pygame.mouse.get_pos()
    dx = mouseX - selected_particle.x
    dy = mouseY - selected_particle.y
    selected_particle.angle = math.atan2(dy, dx) + 0.5*math.pi
    selected_particle.speed = math.hypot(dx, dy) * 0.1

I've found that setting the particle's speed to 0.1 time the actual length of the vector works best. This means it will actually take ten units of time for the particle to catch up with the mouse. Note also that the angle is the arctangent plus half pi. Just trust me it is. I'll try to draw a diagram explaining why later. We now need to ensure the selected particle moves, so change the code back to move all the particle each turn (sorry).

So now we have a simulation that we can interact with. It's quite fun to fling the particles about and I think this simulation could easily be adapted into some sort of game. However, the particles still don't interact with one another. In the next tutorial, I hope to remedy this.

Comments (6)

Anonymous on 11 Jun 2013, 2:32 p.m.

All of pygame's mouse events, MOUSEBUTTONDOWN, MOUSEBUTTONUP, and MOUSEMOTION (maybe more) come with attributes .pos for the mouse position (which would be more efficient than a function call at that specific point in code) and .relpos (relative position from last MouseEvent)(which may be useful in calculating the angle of fling).

dave on 7 Aug 2014, 12:20 p.m.

Hi Peter

Great tutorial! I am enjoying going thru it.

I think the fling works better on the mouse up event, ie, you should put this block of code in the MOUSEBUTTONUP block instead

dx = mouseX - selected_particle.x
dy = mouseY - selected_particle.y
selected_particle.angle = 0.5*math.pi + math.atan2(dy, dx)
selected_particle.speed = math.hypot(dx, dy) * 0.1

Subrata on 2 Sep 2014, 4:17 a.m.

Hi Peter,

I am confused on how our dy,dx code is handling the move function with gravity in it.

Can you please explain why gravity stops acting while we are holding a circle?


Emmanuel on 15 Oct 2015, 5:35 a.m.

Thanks much for the tutorial, Peter. Still one of the best I've found on the web! I made some adjustments up to this point that preclude the need to add Pi/2 on line 99. Angles in my rendition are organized like the unit circle (Pi is left, etc.). Thought you might appreciate it. Check it out

Aditya on 9 Jan 2017, 4:37 a.m.


I have a problem. The particles sometimes get stuck to the right wall and fall down slowly as if a highly viscous thing is dripping from the wall.

And when the the particles are on the ground they vibrate.

I don't know why is this happening. It would be great if someone could help

Thank You

[email protected] on 30 Apr 2017, 11:44 p.m.

Just came here to thank you for very informative tutorial, you rock!