Skip to the content.

Text, Buttons, & Menus

🔗 Text

We skipped over rendering text yesterday, so let’s revisit that…

Drawing text in pygame is a lot like drawing images.

'''
Rendering text

Code copied and adapted from https://inventwithpython.com/pygame/chapter2.html
'''

import sys
import pygame

pygame.init()

FPS = 60
clock = pygame.time.Clock()

screen_width = 400
screen_height = 300
display_bg_color = [255,255,255]

display_surface = pygame.display.set_mode([screen_width, screen_height])
pygame.display.set_caption('This window will have some text in it. Woo?')

# Font documentation: https://www.pygame.org/docs/ref/font.html

# use the pygame.font.get_fonts() function to get a list of available fonts on your
# system
print('Available fonts:', pygame.font.get_fonts())

# To render text with pygame, first we need to create a font
# - when we create a font, we tell it which font-style to use and what font size
#   we want.
# - CHALLENGE: try changing the font size and the font type (use get_fonts() to
#              figure out what is possible)
font = pygame.font.SysFont('impact', 32)

# Next, we can render the font onto a surface (which we'll eventually draw on the
# display_display surface)
# - To render text, we need to specify: (1) the string we want to write, (2) use
#   anti-aliasing? (smoothing technique), (3) what color we want the text to be?,
#   and (4) what background color we want? (if blank, no background color)
hello_color = [0,0,0]
hello_bg = [0,255,0]
hello_surface = font.render('Hello World!', True, hello_color, hello_bg)

# Where do we want to draw the text?
hello_x = screen_width / 2
hello_y = screen_height / 2

# CHALLENGE: add some more text to the screen, try rendering a few different fonts
#            at once
goodbye_color = [255,255,255]
goodbye_bg = [0,0,0]
goodbye_surface = font.render('Goodbye forever!', True, goodbye_color, goodbye_bg)
goodbye_x = 0
goodbye_y = 0

# CHALLENGE: just like the cat animation, can you make some text move around?
moving_text_color = [0,0,0]
moving_text_surface = font.render('MOVING', True, moving_text_color)
moving_text_bounding_rect = moving_text_surface.get_rect()
moving_text_bounding_rect.x = screen_width - moving_text_surface.get_rect().width
moving_text_bounding_rect.y = 0
moving_text_dir = 'southwest'

# rot_degrees = 1
while True:
    # fill the background
    display_surface.fill(display_bg_color)

    # blit the text onto the screen
    # - The top left corner of the text should be at hello_x, hello_y
    display_surface.blit(hello_surface, [hello_x, hello_y])

    # blit goodbye to the screen
    display_surface.blit(goodbye_surface, [goodbye_x, goodbye_y])

    # Animate the moving text
    if moving_text_dir == 'southwest':
        moving_text_bounding_rect.x -= 1 # Move text to the left by 1
        moving_text_bounding_rect.y += 1 # Move text down by 1
        if (moving_text_bounding_rect.bottomleft[1] >= screen_height) or (moving_text_bounding_rect.x <= 0):
            moving_text_dir = 'northeast'
    elif moving_text_dir == 'northeast':
        moving_text_bounding_rect.x += 1 # Move text to the right by 1
        moving_text_bounding_rect.y -= 1 # Move text up by 1
        if (moving_text_bounding_rect.topright[1] <= 0) or (moving_text_bounding_rect.x >= screen_width):
            moving_text_dir = 'southwest'

    # blit the moving text
    display_surface.blit(moving_text_surface, moving_text_bounding_rect)

    # Events!
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    # Update the display
    pygame.display.update()
    clock.tick(FPS)

|>> download text_finished.py

CHALLENGE: Make each of the three texts a different font type and font size.

🔗 Buttons

Buttons in pygame are very simple: they’re rectangles that we draw on the screen, and we detect button presses by detecting collisions between the mouse pointer and the button rectangle when the player clicks. We can also use an image as a button. You’ll need to download red-button.png into the folder containing your Python file for the code below to work.

'''
Buttons example
'''

import sys
import pygame

# Constants
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
FPS = 60

# colors
RED = [255, 0, 0]
BLUE = [0, 0, 255]
DARK_RED = [200, 10, 10]
BLACK = [0, 0, 0]
GREY = [230, 230, 230]

# initialize pygame
pygame.init()

screen = pygame.display.set_mode([SCREEN_WIDTH, SCREEN_HEIGHT]) # Setup the game screen surface
clock = pygame.time.Clock()                                     # make a clock to manage FPS

font = pygame.font.SysFont('impact', 32) # Here's the font we'll use to render text

# Rect button
rect_button = pygame.Rect([10, 10], [100, 30])
rect_button_color = RED
rect_clicks = 0 # click tracker

# Image button
button_img = pygame.image.load('media/red-button.png') # Make an image surface
button_img_rect = button_img.get_rect()          # Get a bounding+drawing rectangle for the surface
                                                 # (this is what we'll use to detect collisions w/mouse)
button_img_rect.x = 10    # Position the img rectangle
button_img_rect.y = 100
button_img_clicks = 0 # click tracker

# Our game loop
while True:
    screen.fill(GREY)

    # CHALLENGE: if mouse if hovering over the button, make the color darker/outline it
    # We'll need to know the mouse position

    ############################################################################
    # Draw the rectangle button
    pygame.draw.rect(screen, rect_button_color, rect_button)
    rect_clicks_surf = font.render(str(rect_clicks), True, BLACK)
    screen.blit(rect_clicks_surf, [rect_button.topright[0]+10, rect_button.topright[1]-5])
    ############################################################################

    ############################################################################
    # Draw the image button
    screen.blit(button_img, button_img_rect)
    img_button_clicks_surf = font.render(str(button_img_clicks), True, BLACK)
    screen.blit(img_button_clicks_surf, [button_img_rect.topright[0]+10, button_img_rect.topright[1]-5])
    ############################################################################

    # Event loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            # when the mouse is clicked, did the player press any buttons?
            mouse_pos = pygame.mouse.get_pos()
            if rect_button.collidepoint(mouse_pos):
                # rectangle button was clicked
                print("RECTANGLE BUTTON CLICK")
                rect_clicks += 1
            if button_img_rect.collidepoint(mouse_pos):
                print("IMG BUTTON CLICK")
                button_img_clicks += 1

    pygame.display.update()
    clock.tick(FPS)

|>> download buttons.py

CHALLENGE: Add another button that resets the click counts for the other two buttons.

  • Extra challenge: add a button that disables the other buttons (prevents them from being clicked)

Menus are just several buttons strung together. We can make a menu screen by having multiple game modes. In the example below, we have two game modes:

  1. a title screen mode and
  2. a menu screen mode. In the menu screen, the player uses the arrow keys to navigate (we could have used the mouse here instead), and presses enter to make a selection.
'''
Menu example
'''

import sys
import random
import pygame

# Constants
FPS = 60
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
BTN_PADDING = 10    # How much padding are we going to put around a button?
BTN_MARGIN = 10     # How much space do we want around button text?

# Colors
WHITE = [255, 255, 255]
GREY = [175, 175, 175]
BLACK = [0, 0, 5]
YELLOW = [255, 229, 153]
DARKER_YELLOW = [255, 217, 102]

pygame.init() # As always, initialize pygame

screen = pygame.display.set_mode([SCREEN_WIDTH, SCREEN_HEIGHT])
clock = pygame.time.Clock()
menu_font = pygame.font.SysFont('impact', 32)   # Here's our button font
print("Loaded the font!")
screen_mode = 'title'   # Modes: title, menu

menu_btn_color = YELLOW
menu_btn_hover_color = DARKER_YELLOW

################################################################################
# Title screen components
################################################################################
title_screen_bg_color = BLACK
# === Title text ===
title_font = pygame.font.SysFont('impact', 128)
title_surface = title_font.render('THE MENU GAME', True, WHITE)
title_rect = title_surface.get_rect()
title_rect.x = (SCREEN_WIDTH / 2) - (title_rect.width / 2) # Put rect in middle of screen (perfectly in middle x)
title_rect.y = (SCREEN_HEIGHT / 2) - (title_rect.height)   # Put rect in middle of the screen (sitting on top of horizontal midline)

# === Open menu button ===
menu_btn_txt_surface = menu_font.render('open menu', True, BLACK)

# setup open menu button background
menu_btn_bg_rect = menu_btn_txt_surface.get_rect()
menu_btn_bg_rect.width += 2 * BTN_MARGIN  # Add some margins to the button
menu_btn_bg_rect.height += 2 * BTN_MARGIN # Add margin to the button
menu_btn_bg_rect.x = title_rect.midbottom[0] - (menu_btn_bg_rect.width / 2)
menu_btn_bg_rect.y = title_rect.midbottom[1] + BTN_PADDING

# setup text rectangle (used to determine where we'll draw text)
menu_btn_txt_rect = menu_btn_txt_surface.get_rect()
menu_btn_txt_rect.x = title_rect.midbottom[0] - (menu_btn_txt_rect.width / 2)
menu_btn_txt_rect.y = title_rect.midbottom[1] + BTN_PADDING + BTN_MARGIN

################################################################################
# Menu screen components
################################################################################
menu_screen_bg_color = GREY

menu_screen_buttons = ['resume', 'random', 'quit'] # Available buttons
cur_menu_btn_id = 0                                # What button are we currently on?
btn_color = YELLOW

# === Resume button ===
# Render resume btn text onto surface
resume_btn_txt_surface = menu_font.render('Resume', True, BLACK)
# Setup resume button background
resume_btn_bg_rect = resume_btn_txt_surface.get_rect()
resume_btn_bg_rect.width += 2 * BTN_MARGIN
resume_btn_bg_rect.height += 2 * BTN_MARGIN
resume_btn_bg_rect.x = (SCREEN_WIDTH / 2) - (0.5 * resume_btn_bg_rect.width)
resume_btn_bg_rect.y = 50
# Setup the resume button text
resume_btn_txt_rect = resume_btn_txt_surface.get_rect()
resume_btn_txt_rect.x = resume_btn_bg_rect.x + BTN_MARGIN
resume_btn_txt_rect.y = resume_btn_bg_rect.y + BTN_MARGIN

# === Random button ===
# Render random btn text onto surface
random_btn_txt_surface = menu_font.render('???', True, BLACK)
# Setup random button background
random_btn_bg_rect = random_btn_txt_surface.get_rect()
random_btn_bg_rect.width += 2 * BTN_MARGIN
random_btn_bg_rect.height += 2 * BTN_MARGIN
random_btn_bg_rect.x = (SCREEN_WIDTH / 2) - (0.5 * random_btn_bg_rect.width)
random_btn_bg_rect.y = resume_btn_bg_rect.y + (resume_btn_bg_rect.height + BTN_PADDING)
# Setup the random button text
random_btn_txt_rect = random_btn_txt_surface.get_rect()
random_btn_txt_rect.x = random_btn_bg_rect.x + BTN_MARGIN
random_btn_txt_rect.y = random_btn_bg_rect.y + BTN_MARGIN

# === Quit button ===
# Render quit btn text onto surface
quit_btn_txt_surface = menu_font.render('Quit', True, BLACK)
# Setup quit button background
quit_btn_bg_rect = quit_btn_txt_surface.get_rect()
quit_btn_bg_rect.width += 2 * BTN_MARGIN
quit_btn_bg_rect.height += 2 * BTN_MARGIN
quit_btn_bg_rect.x = (SCREEN_WIDTH / 2) - (0.5 * quit_btn_bg_rect.width)
quit_btn_bg_rect.y = random_btn_bg_rect.y + (resume_btn_bg_rect.height + BTN_PADDING)
# Setup the quit button text
quit_btn_txt_rect = quit_btn_txt_surface.get_rect()
quit_btn_txt_rect.x = quit_btn_bg_rect.x + BTN_MARGIN
quit_btn_txt_rect.y = quit_btn_bg_rect.y + BTN_MARGIN
################################################################################

# Game loop
while True:
    # We need to show different things depending on whether or not we're in 'title'
    # or 'menu' mode
    if screen_mode == 'title':
        # ==== TITLE SCREEN MODE ====
        screen.fill(title_screen_bg_color)
        # Draw the title screen
        # Render the title text in the middle of the screen
        screen.blit(title_surface, title_rect)
        # Draw the menu button, first: the text
        pygame.draw.rect(screen, menu_btn_color, menu_btn_bg_rect)
        screen.blit(menu_btn_txt_surface, menu_btn_txt_rect)

    elif screen_mode == 'menu':
        # === MENU SCREEN MODE ===
        screen.fill(menu_screen_bg_color)
        # Draw button rectangles!
        # - If button is active, color background with hover color and an outline
        # - otherwise, draw with normal color and no outline
        if menu_screen_buttons[cur_menu_btn_id] == 'resume':
            pygame.draw.rect(screen, menu_btn_hover_color, resume_btn_bg_rect)
            pygame.draw.rect(screen, BLACK, resume_btn_bg_rect, 5)
        else:
            pygame.draw.rect(screen, menu_btn_color, resume_btn_bg_rect)

        if menu_screen_buttons[cur_menu_btn_id] == 'random':
            pygame.draw.rect(screen, menu_btn_hover_color, random_btn_bg_rect)
            pygame.draw.rect(screen, BLACK, random_btn_bg_rect, 5)
        else:
            pygame.draw.rect(screen, menu_btn_color, random_btn_bg_rect)

        if menu_screen_buttons[cur_menu_btn_id] == 'quit':
            pygame.draw.rect(screen, menu_btn_hover_color, quit_btn_bg_rect)
            pygame.draw.rect(screen, BLACK, quit_btn_bg_rect, 5)
        else:
            pygame.draw.rect(screen, menu_btn_color, quit_btn_bg_rect)

        # Layer button text over button backgrounds
        screen.blit(resume_btn_txt_surface, resume_btn_txt_rect)
        screen.blit(random_btn_txt_surface, random_btn_txt_rect)
        screen.blit(quit_btn_txt_surface, quit_btn_txt_rect)

    else:
        # ==== ???? MODE ====
        print("AAAH UNRECOGNIZED SCREEN MODE! Exiting")
        pygame.quit()
        sys.exit()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        # ===== TITLE MODE EVENTS =====
        if screen_mode == 'title' and event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            if menu_btn_bg_rect.collidepoint(mouse_pos):
                screen_mode = 'menu'
                cur_menu_btn_id = 0

        # ===== MENU MODE EVENTS =====
        if screen_mode == 'menu' and event.type == pygame.KEYDOWN:
            if event.key == pygame.K_DOWN:
                # player presses down arrow
                cur_menu_btn_id = (cur_menu_btn_id + 1) % len(menu_screen_buttons)
            if event.key == pygame.K_UP:
                # player presses up arrow
                cur_menu_btn_id = (cur_menu_btn_id - 1) % len(menu_screen_buttons)
            if event.key == pygame.K_RETURN:
                # Player presses return (selects current option)
                if menu_screen_buttons[cur_menu_btn_id] == 'resume':
                    # if on resume button, go back to title screen
                    screen_mode = 'title'
                elif menu_screen_buttons[cur_menu_btn_id] == 'random':
                    # if on random ('???') button, randomize background color
                    menu_screen_bg_color = [random.randint(0, 255),
                                            random.randint(0, 255),
                                            random.randint(0, 255)]
                elif menu_screen_buttons[cur_menu_btn_id] == 'quit':
                    # if on quit button, quit the game
                    pygame.quit()
                    sys.exit()
    clock.tick(FPS)
    pygame.display.update()

|>> download menu.py

CHALLENGE: Add another option to the menu screen that changes the title screen’s background