[concept]Game Development with Pygame
Text & HUD Rendering
# theory
rendering text in Pygame
Pygame doesn't have a text box you can just type into. To show text on screen you render it onto a surface, then blit that surface onto the screen. It's a two-step process.
font = pygame.font.SysFont(None, 36) # None = default system font, 36 = size
text_surface = font.render("Hello!", True, (255, 255, 255)) # text, antialias, color
screen.blit(text_surface, (10, 10)) # draw it at position (10, 10)
That's the whole pattern. Everything else is just variations on it.
font options
System Fonts
# Use any font installed on the system
font = pygame.font.SysFont("arial", 28)
font = pygame.font.SysFont("courier", 20)
# None = pygame default font (always available)
font = pygame.font.SysFont(None, 36)
# Bold and italic
font = pygame.font.SysFont("arial", 28, bold=True, italic=True)
# See what fonts are available:
print(pygame.font.get_fonts())
Custom Font Files
# Load a .ttf file from your project folder
font = pygame.font.Font("assets/fonts/PressStart2P.ttf", 16)
Custom fonts make your game look unique. Free pixel fonts from Google Fonts or dafont.com work great for retro games.
render()
text_surface = font.render(text, antialias, color, background=None)
| Param | What it does |
|---|---|
| text | The string to display |
| antialias | True = smooth edges, False = pixelated |
| color | (R, G, B) text color |
| background | Optional (R, G, B) background color |
Anti-aliasing: Use True for most text. Use False for pixel art games where you want that crispy look.
# Smooth text
smooth = font.render("Score: 100", True, (255, 255, 255))
# Pixel-crisp text (retro style)
crispy = font.render("GAME OVER", False, (255, 0, 0))
# Text with background color (like a highlight)
highlighted = font.render("NEW HIGH SCORE!", True, (255, 255, 0), (0, 0, 100))
positioning text
font.render() returns a surface. Positioning it on the screen is a separate step.
Basic Positioning
# Top-left corner
screen.blit(text_surface, (10, 10))
# But what if you want it centered?
text_rect = text_surface.get_rect()
text_rect.center = (400, 300) # center of an 800x600 screen
screen.blit(text_surface, text_rect)
Alignment Shortcuts
rect = text_surface.get_rect()
# Center on screen
rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
# Top-center
rect.midtop = (SCREEN_WIDTH // 2, 10)
# Right-aligned
rect.topright = (SCREEN_WIDTH - 10, 10)
# Bottom-center (for UI at bottom of screen)
rect.midbottom = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 10)
building a HUD (heads-up display)
A HUD shows game info that updates every frame: score, lives, timer, level.
The Pattern
class HUD:
def __init__(self):
self.font = pygame.font.SysFont(None, 28)
self.score = 0
self.lives = 3
self.level = 1
def draw(self, screen):
# Score; top left
score_text = self.font.render(f"Score: {self.score}", True, (255, 255, 255))
screen.blit(score_text, (10, 10))
# Lives; top right
lives_text = self.font.render(f"Lives: {self.lives}", True, (255, 100, 100))
lives_rect = lives_text.get_rect(topright=(screen.get_width() - 10, 10))
screen.blit(lives_text, lives_rect)
# Level; top center
level_text = self.font.render(f"Level {self.level}", True, (200, 200, 200))
level_rect = level_text.get_rect(midtop=(screen.get_width() // 2, 10))
screen.blit(level_text, level_rect)
Using It
hud = HUD()
while running:
# ... game logic ...
if brick_destroyed:
hud.score += 10
if player_hit:
hud.lives -= 1
# Draw everything
screen.fill((0, 0, 0))
# ... draw game objects ...
hud.draw(screen) # HUD goes LAST (on top of everything)
pygame.display.flip()
Key insight: Draw the HUD last so it renders on top of everything else.
performance tip: cache rendered text
Re-rendering text every frame is wasteful if the text hasn't changed. Cache it:
class HUD:
def __init__(self):
self.font = pygame.font.SysFont(None, 28)
self._score = 0
self._cached_score_surface = None
@property
def score(self):
return self._score
@score.setter
def score(self, value):
self._score = value
# Only re-render when score actually changes
self._cached_score_surface = self.font.render(
f"Score: {value}", True, (255, 255, 255)
)
def draw(self, screen):
if self._cached_score_surface:
screen.blit(self._cached_score_surface, (10, 10))
For simple games this optimization doesn't matter much, but it's a good habit. Rendering text is one of the slower operations in Pygame.
timer display
start_time = pygame.time.get_ticks()
# In game loop:
elapsed_ms = pygame.time.get_ticks() - start_time
seconds = elapsed_ms // 1000
minutes = seconds // 60
display_seconds = seconds % 60
timer_text = font.render(f"{minutes:02d}:{display_seconds:02d}", True, WHITE)
The :02d format pads with zeros: "01:05" instead of "1:5".
# examples [2]
Use get_rect() with center to position text in the middle of the screen.
pygame needs a real window — copy this into a .py file and run it locally.
Right-align the score so it doesn't jump around as the number gets bigger.
pygame needs a real window — copy this into a .py file and run it locally.
# challenges [3]