Module 11 of 20 Phase: Audio & UI

Building a Real User Interface
Menus, HUDs & Buttons

Fonts, clickable buttons, sliders, menus, pause screens and a full in-game HUD — everything that turns a Pygame prototype into something players can actually navigate.

🖱️
12 Lessons
This module
~2.5 hrs
Estimated time
📊
Intermediate
Difficulty
🐍
pygame.font
Primary module
Section 1

Fonts & Text Rendering

Text in Pygame is not drawn directly — you render a font into a Surface, then blit() that surface like any image. Rendering happens once and should be cached, not repeated every frame unless the text actually changes.

text_basics.py
pygame.font.init()
font_lg = pygame.font.Font(None, 48)          # default font, 48px
font_sm = pygame.font.SysFont("arial", 20)    # a system font by name

text_surf = font_lg.render("Game Over", True, (255, 255, 255))  # antialias=True
text_rect = text_surf.get_rect(center=(400, 300))
screen.blit(text_surf, text_rect)
Section 2

Clickable Buttons & Sliders

A button is just a pygame.Rect you check .collidepoint(mouse_pos) against on a mouse click. A slider is the same idea with extra math — dragging a handle along a track maps its x-position to a numeric value.

button.py
class Button:
    def __init__(self, rect, text, font, on_click):
        self.rect = pygame.Rect(rect)
        self.text = text
        self.font = font
        self.on_click = on_click
        self.hovered = False

    def handle_event(self, event):
        if event.type == pygame.MOUSEMOTION:
            self.hovered = self.rect.collidepoint(event.pos)
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            if self.rect.collidepoint(event.pos):
                self.on_click()

    def draw(self, screen):
        color = (52, 211, 153) if self.hovered else (30, 41, 59)
        pygame.draw.rect(screen, color, self.rect, border_radius=8)
        label = self.font.render(self.text, True, (255, 255, 255))
        screen.blit(label, label.get_rect(center=self.rect.center))
Section 3

Menus, Pause Screens & Settings

A menu is simply a screen full of buttons rendered instead of your gameplay. A pause screen usually overlays a semi-transparent rectangle over the frozen game (freeze updates but keep drawing the last frame underneath):

🏠
Main Menu
Play / Settings / Quit buttons, usually the game's very first screen (see Module 18's Scene pattern).
⏸️
Pause Screen
Stop calling gameplay update() while ESC is toggled, but keep drawing — overlay a translucent dark rect + "Paused" text on top.
⚙️
Settings
Volume sliders, key-rebind list, fullscreen toggle — typically saved to a JSON file (Module 17).
📋
Inventory UI
A grid of item slots (list of Rects), each rendering an item icon + stack count if the corresponding inventory index isn't empty.
pause_overlay.py
def draw_pause_overlay(screen, font):
    overlay = pygame.Surface(screen.get_size(), pygame.SRCALPHA)
    overlay.fill((0, 0, 0, 160))          # semi-transparent black
    screen.blit(overlay, (0, 0))
    text = font.render("PAUSED — Press ESC to resume", True, (255, 255, 255))
    screen.blit(text, text.get_rect(center=screen.get_rect().center))
Section 4

HUD: Health Bars, Score & Timer

The Heads-Up Display is drawn every frame ON TOP of the gameplay world — it never scrolls with the camera. A health bar is just two overlapping rectangles: a full-width background and a foreground scaled to the current health percentage.

hud.py
def draw_health_bar(screen, x, y, w, h, current_hp, max_hp):
    pct = max(0, current_hp / max_hp)
    pygame.draw.rect(screen, (60, 20, 20), (x, y, w, h), border_radius=4)
    pygame.draw.rect(screen, (220, 50, 50), (x, y, int(w * pct), h), border_radius=4)
    pygame.draw.rect(screen, (255, 255, 255), (x, y, w, h), 2, border_radius=4)

def draw_score_and_timer(screen, font, score, seconds_left):
    score_surf = font.render(f"Score: {score}", True, (255, 255, 255))
    time_surf  = font.render(f"Time: {int(seconds_left)}s", True, (255, 255, 255))
    screen.blit(score_surf, (20, 20))
    screen.blit(time_surf, (20, 50))
Section 5

HUD Layout: Screen-Space Placement

A HUD is anchored to the screen, not the game world — it renders at the same fixed pixel coordinates every single frame, no matter where the camera is currently looking. The diagram below maps where each element from hud.py typically lives.

SCREEN — fixed, never scrolls Health Bar top-left Timer top-center Score top-right Game World (scrolls with camera) HUD is drawn on top, last, every frame
Fixed screen-space HUD elements — health top-left, timer top-center, score top-right — sit above the scrolling game world and never move with the camera.

🧠 Quick Knowledge Check

1. How does Pygame put text on the screen?
2. How do you detect a click on a button in Pygame?
3. What's a common approach to implementing a pause screen?
4. In the health bar example, what does `pct = current_hp / max_hp` represent?
5. Does the HUD typically move with the game's scrolling camera?
Finished Module 11?

Mark it complete to track your progress through Complete Pygame Mastery 2026.

🗒 Cheat Sheet 📝 Worksheet