Mouse interactions

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 following the link at the bottom 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 chose 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.

particle_tutorial_7.txt3.23 KB


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).

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

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?


Post new comment

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