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