Ryans Tutorials
More great tutorials at RyansTutorials

PyGame Buttons

and Mouse Movement!

Introduction

In this section we will look at how you can utilise the mouse in your games and create buttons to craft more intuitive interfaces and game interactions.

Getting Set Up

Tom, our heroic character, standingBefore we begin, let's create a new file (call it mouse.py) and copy in the template code from the previous section

Once you've saved your file with the template code in it, run the file to make sure you've copied it in ok.

We will also need some images to play with. Create a directory called images in the same directory as your Python file and place these images into this directory. Right click and download these images.

  • Tom - our heroic hero.

(Right click and "Save image as ...", or similar)

It is possible to access and load the images from anywhere on the system but if you place them in the same location as the Python (preferably in a folder called "images" to keep things neat) file it makes things a little easier. It also makes things easier when you want to package up and share your game later on.

We will place Tom in the middle of the window so that we can manipulate him via the mouse. Place the following code near the beginning of the main function :

  •   looping = True
  •  
  •   tom = pygame.image.load('images/tom_standing.png').convert_alpha()
  •   tom = pygame.transform.scale(tom, (50, 95))
  •   tomX = WINDOW_WIDTH // 2 - 25
  •   tomY = WINDOW_HEIGHT //2 - 48

And then render Tom :

  •     WINDOW.fill(BACKGROUND)
  •     WINDOW.blit(tom, (tomX, tomY))
  •     pygame.display.update()

Tom in a windowIf you save and run the program you should get a window with Tom in the middle.

Where is the cursor?

Let's start by figuring out the location of the mouse within the window. We can do this rather easily.

  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •     mouse = pygame.mouse.get_pos()

And now we will print the values out so we can see what they look like :

  •     # Processing
  •     print (mouse)

This will print out a constant stream of values to the screen. Move the mouse around a bit and observe how the values change. See what happens if you move the cursor outside the window as well. Once you are done, you may comment out this line.

The values are in what is called a tuple which is essentially a list (or array) in which the items cannot be changed once they are set. You access the items using numerical index (starting from 0) just the same as a list.

If we wanted to print the values a bit more nicely we could replace the line we just put in with the following :

  •     # Processing
  •     print ('X coordinate : ', mouse[0], ' Y coordinate : ', mouse[1])

Reacting to Mouse Movement

Now that we can identify the location of the cursor we can add some interactivity. Let's make Tom chase the cursor.

  •     # Processing
  •     # print ('X coordinate : ', mouse[0], ' Y coordinate : ', mouse[1])
  •     if tomX < mouse[0] :
  •       tomX = tomX + 1
  •     if tomX > mouse[0] :
  •       tomX = tomX - 1
  •     if tomY < mouse[1] :
  •       tomY = tomY + 1
  •     if tomY > mouse[1] :
  •       tomY = tomY - 1

What we have done is comment out the line printing the coordinates (now that we know that is works we don't need it any more). Then we added four if statements to check if Tom is above, below, to the left, or to the right of the cursor and to update his coordinates accordingly.

Tom chasing the cursorTom now chases the cursor but there is a small flaw. If you move the mouse outside the window, Tom will keep moving to whereever that last location was. It would be nice if Tom stopped moving when there wasn't a cursor to chase. We do this by checking if the mouse is focused on the window or not.

  •     # Processing
  •     # print ('X coordinate : ', mouse[0], ' Y coordinate : ', mouse[1])
  •     if pygame.mouse.get_focused() == 1 :
  •       if tomX < mouse[0] :
  •         tomX = tomX + 1
  •       if tomX > mouse[0] :
  •         tomX = tomX - 1
  •       if tomY < mouse[1] :
  •         tomY = tomY + 1
  •       if tomY > mouse[1] :
  •         tomY = tomY - 1

Mouse Clicks

Detecting mouse clicks is just as easy. Reacting to mouse clicks is a little bit trickier. It is possible to detect and respond to both the mouse button being pressed and the mouse button being released.

  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •       if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
  •         print ('mouse button down')
  •       if event.type == pygame.MOUSEBUTTONUP and event.button == 1 :
  •         print ('mouse button up')
  •  
  •     mouse = pygame.mouse.get_pos()

With this method we can detect both when the mouse button has been clicked and when it has been released. This can be useful to show when the button has been pressed down but only act when it has been released. Alternatively, we may want to perform an action for as long as the button is pressed and only stop when it is released.

You'll notice that at the end of the if statements we have event.button == 1. This indicates that it is the left mouse button we are looking at. If you change this to 2 it will instead look at the middle button (usually the scroll wheel) and 3 will make it look at the right mouse button.

It is also possible to get the state of all buttons using the command pygame.mouse.get_pressed() however this will only tell us if the button is up or down at the time of calling. It doesn't give us as fine grained an indication of the state of the buttons.

Now let's do something with these clicks. We'll start by pausing Tom when the left mouse button is pressed.

  •   tom = pygame.transform.scale(tom, (50, 95))
  •   tomX = WINDOW_WIDTH // 2 - 25
  •   tomY = WINDOW_HEIGHT //2 - 48
  •  
  •   mouseDown = False

And change the statement within the if statement for the MOUSEBUTTONDOWN event so that instead of printing a message it sets the variable mouseDown to True. Also do the opposite when MOUSEBUTTONUP event is fired.

  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •       if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
  •         mouseDown = True
  •       if event.type == pygame.MOUSEBUTTONUP and event.button == 1 :
  •         mouseDown = False
  •  
  •     mouse = pygame.mouse.get_pos()

This first bit creates a variable that stores if the button is currently down or not. Next we will add a condition to the if statement around the movement code for Tom so that it only runs when this variable is False.

  •     # Processing
  •     # print ('X coordinate : ', mouse[0], ' Y coordinate : ', mouse[1])
  •     if pygame.mouse.get_focused() == 1 and mouseDown == False :
  •       if tomX < mouse[0] :
  •         tomX = tomX + 1
  •       if tomX > mouse[0] :
  •         tomX = tomX - 1
  •       if tomY < mouse[1] :
  •         tomY = tomY + 1
  •       if tomY > mouse[1] :
  •         tomY = tomY - 1

If you save and run your code now you should see that Tom will chase your cursor but stop whenever you have your left mouse button pressed. Now le't code in the next step which is when the button is released.

  •   tom = pygame.transform.scale(tom, (50, 95))
  •   tomX = WINDOW_WIDTH // 2 - 25
  •   tomY = WINDOW_HEIGHT //2 - 48
  •  
  •   mouseDown = False
  •   mouseReleased = False
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •  
  •       if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
  •         mouseDown = True
  •         mouseReleased = False
  •       if event.type == pygame.MOUSEBUTTONUP and event.button == 1 :
  •         mouseDown = False
  •         mouseReleased = True
  •  
  •     mouse = pygame.mouse.get_pos()

And another if statement to act upon when the mouse button is released.

  •       if tomX < mouse[0] :
  •         tomX = tomX + 1
  •       if tomX > mouse[0] :
  •         tomX = tomX - 1
  •       if tomY < mouse[1] :
  •         tomY = tomY + 1
  •       if tomY > mouse[1] :
  •         tomY = tomY - 1
  •     if mouseReleased == True :
  •       tomX = WINDOW_WIDTH // 2 - 25
  •       tomY = WINDOW_HEIGHT //2 - 48
  •       mouseReleased = False

Now if you save and run your code, when you release the mouse button Tom should reset back to the middle of the window.

Buttons

Being able to detect mouse clicks gives us the final bit we need in order to make buttons within our games. I will demonstrate a simple working button here but utilising your knowledge of drawing and images you can easily take this further and create more elaborate buttons.

  • import pygame, sys, random
  • from pygame.locals import *
  • pygame.init()
  •  
  • # Colours
  • BACKGROUND = (255, 255, 255)
  • BUTTON_NORMAL = (255, 100, 100)
  • BUTTON_HOVER = (100, 255, 100)
  • BUTTON_CLICKED = (100, 100, 255)
  •  
  • # Game Setup
  • FPS = 60
  • fpsClock = pygame.time.Clock()
  • WINDOW_WIDTH = 400
  • WINDOW_HEIGHT = 300
  •  
  • WINDOW = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
  • pygame.display.set_caption('My Game!')
  •  
  • # The main function that controls the game
  • def main () :
  •   looping = True
  •  
  •   button = pygame.Rect(50, 50, 200, 100)
  •   mouseClicked = False
  •  
  •   # The main game loop
  •   while looping :
  •     state = 'normal'
  •  
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •       if event.type == pygame.MOUSEBUTTONDOWN :
  •         mouseClicked = True
  •       if event.type == pygame.MOUSEBUTTONUP :
  •         mouseClicked = False
  •  
  •     mouse = pygame.mouse.get_pos()
  •  
  •     # Processing
  •     # This section will be built out later
  •     if button.collidepoint(mouse) and mouseClicked :
  •       state = 'clicked'
  •     elif button.collidepoint(mouse) :
  •       state = 'hover'
  •  
  •     # Render elements of the game
  •     WINDOW.fill(BACKGROUND)
  •     if state == 'hover' :
  •       pygame.draw.rect(WINDOW, BUTTON_HOVER, button)
  •     elif state == 'clicked' :
  •       pygame.draw.rect(WINDOW, BUTTON_CLICKED, button)
  •     else :
  •       pygame.draw.rect(WINDOW, BUTTON_NORMAL, button)
  •  
  •     pygame.display.update()
  •     fpsClock.tick(FPS)
  •  
  • main()

A very basic buttonThe button is just a rectangle that changes colour based upon the state (normal, hover, click) but it illustrates the mechanism.

In Action

Now let's put this all together into a simple game. The game is going to either show the text 'Left' or 'Right' randomly on the screen and you have to react as fast as you can to click the opposite button.

  • import pygame, sys, random
  • from pygame.locals import *
  • pygame.init()
  •  
  • # Colours
  • BACKGROUND = (255, 255, 255)
  • BUTTON_NORMAL = (255, 100, 100)
  • BUTTON_HOVER = (100, 255, 100)
  • BUTTON_CLICKED = (100, 100, 255)
  • TEXTCOLOUR = (0, 0, 0)
  •  
  • # Game Setup
  • FPS = 60
  • fpsClock = pygame.time.Clock()
  • WINDOW_WIDTH = 400
  • WINDOW_HEIGHT = 300
  •  
  • WINDOW = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
  • pygame.display.set_caption('My Game!')
  •  
  • # Fonts
  • fontObj = pygame.font.Font(None, 32)
  • startText = fontObj.render('Start', True, TEXTCOLOUR, None)
  • leftText = fontObj.render('Left', True, TEXTCOLOUR, None)
  • rightText = fontObj.render('Right', True, TEXTCOLOUR, None)
  •  
  • # The main function that controls the game
  • def main () :
  •   looping = True
  •   gameState = 'menu'
  •   toHit = random.randint(0,1) # 0 = Left, 1 = Right
  •   timer = 0
  •   timeTaken = 0
  •   timeToWait = 0
  •  
  •   startButton = pygame.Rect(150, 40, 100, 50)
  •   leftButton = pygame.Rect(50, 190, 100, 50)
  •   rightButton = pygame.Rect(250, 190, 100, 50)
  •   mouseClicked = False
  •  
  •   # The main game loop
  •   while looping :
  •     startState = 'normal'
  •     leftState = 'normal'
  •     rightState = 'normal'
  •  
  •     # Get inputs
  •     for event in pygame.event.get() :
  •       if event.type == QUIT :
  •         pygame.quit()
  •         sys.exit()
  •       if event.type == pygame.MOUSEBUTTONDOWN :
  •         mouseClicked = True
  •       if event.type == pygame.MOUSEBUTTONUP :
  •         mouseClicked = False
  •  
  •     mouse = pygame.mouse.get_pos()
  •  
  •     # Processing
  •     # This section will be built out later
  •     if startButton.collidepoint(mouse) and mouseClicked :
  •       startState = 'clicked'
  •       gameState = 'inGame'
  •       timer = time.time() + random.randint(2, 5)
  •     elif startButton.collidepoint(mouse) :
  •       startState = 'hover'
  •  
  •     if leftButton.collidepoint(mouse) and mouseClicked :
  •       leftState = 'clicked'
  •       if gameState == 'inGame' and toHit == 0 and time.time() > timer:
  •         timeTaken = time.time() - timer
  •         print (f"Time Taken : {timeTaken}")
  •         toHit = random.randint(0,1) # 0 = Left, 1 = Right
  •         timer = time.time() + random.randint (2, 5)
  •     elif leftButton.collidepoint(mouse) :
  •       leftState = 'hover'
  •  
  •     if rightButton.collidepoint(mouse) and mouseClicked :
  •       rightState = 'clicked'
  •       if gameState == 'inGame' and toHit == 1 and time.time() > timer:
  •         timeTaken = time.time() - timer
  •         print (f"Time Taken : {timeTaken}")
  •         toHit = random.randint(0,1) # 0 = Left, 1 = Right
  •         timer = time.time() + random.randint (2, 5)
  •     elif rightButton.collidepoint(mouse) :
  •       rightState = 'hover'
  •  
  •     # Render elements of the game
  •     WINDOW.fill(BACKGROUND)
  •     if startState == 'hover' and gameState == 'menu' :
  •       pygame.draw.rect(WINDOW, BUTTON_HOVER, startButton)
  •       WINDOW.blit(startText, (175, 55))
  •     elif startState == 'clicked' and gameState == 'menu' :
  •       pygame.draw.rect(WINDOW, BUTTON_CLICKED, startButton)
  •       WINDOW.blit(startText, (175, 55))
  •     elif startState == 'normal' and gameState == 'menu' :
  •       pygame.draw.rect(WINDOW, BUTTON_NORMAL, startButton)
  •       WINDOW.blit(startText, (175, 55))
  •  
  •     if gameState == 'inGame' and time.time() > timer:
  •       if toHit == 0 :
  •         WINDOW.blit(leftText, (175, 65))
  •       else :
  •         WINDOW.blit(rightText, (175, 65))
  •  
  •     if leftState == 'hover' :
  •       pygame.draw.rect(WINDOW, BUTTON_HOVER, leftButton)
  •       WINDOW.blit(leftText, (80, 205))
  •     elif leftState == 'clicked' :
  •       pygame.draw.rect(WINDOW, BUTTON_CLICKED, leftButton)
  •       WINDOW.blit(leftText, (80, 205))
  •     elif leftState == 'normal' :
  •       pygame.draw.rect(WINDOW, BUTTON_NORMAL, leftButton)
  •       WINDOW.blit(leftText, (80, 205))
  •  
  •     if rightState == 'hover' :
  •       pygame.draw.rect(WINDOW, BUTTON_HOVER, rightButton)
  •       WINDOW.blit(rightText, (270, 205))
  •     elif rightState == 'clicked' :
  •       pygame.draw.rect(WINDOW, BUTTON_CLICKED, rightButton)
  •       WINDOW.blit(rightText, (270, 205))
  •     elif rightState == 'normal' :
  •       pygame.draw.rect(WINDOW, BUTTON_NORMAL, rightButton)
  •       WINDOW.blit(rightText, (270, 205))
  •  
  •     pygame.display.update()
  •     fpsClock.tick(FPS)
  •  
  • main()

Basic reaction gameIf you save and run the game you should have a simple reaction timer game.

There is a lot of duplication of similar code in this program for the different buttons etc. Ideally functions and arrays should be used to make the code shorter and more efficient (as well as modular which makes it easier to modify and expand). I haven't done so here simply to make it easier for newer coders to follow but a good challenge would be to refactor this code using functions.

Activities

Even though our program is fairly basic we can still tinker with a few elements to make sure we understand them.

Have a go at the following :

1. Can you improve the look of the buttons?

2. Can you indicate the speed of reaction visually instead of printing it to the terminal?

3. Can you make it so that the player has 10 goes then the start button reappears and an average reaction time is printed?

4. Can you enhance the game by adding 'Up' and 'Down' to the available options?