Learning Goals
3 minBy the end of this lesson you can:
- Display a score number and a lives counter using
screen.draw.textwith f-strings. - Draw a health bar as two overlapping
filled_rectcalls — one dark background, one coloured fill. - Update the health bar width in real time so it shrinks as health decreases.
Warm-Up · Rectangle Maths
5 minA health bar maps a value to a pixel width. Predict what this prints:
MAX_HEALTH = 100 BAR_MAX_WIDTH = 200 health = 75 bar_width = int((health / MAX_HEALTH) * BAR_MAX_WIDTH) print(bar_width)
Show the answer
Output
150
75 out of 100 is 75 %. 75 % of 200 pixels = 150. That is the formula behind every health bar in every video game.
New Concept · Drawing the HUD
12 minHUD stands for heads-up display — the part of the screen that shows game status, not the game world. Think of it as a layer of labels drawn on top of everything else.
Score and lives as text
score = 240 lives = 3 def draw(): screen.fill("black") screen.draw.text(f"Score: {score}", topleft=(8, 8), fontsize=22, color="white") screen.draw.text(f"Lives: {lives}", topleft=(8, 34), fontsize=22, color="white")
Draw these last, after all game objects. That way the text always sits on top.
A health bar with two rects
A health bar needs two rectangles stacked:
- Background rect — full width, dark colour. Drawn first.
- Fill rect — shrinks as health drops. Drawn on top.
MAX_HEALTH = 100 BAR_W = 160 BAR_H = 14 BAR_X = 8 BAR_Y = 60 health = 75 def draw(): bar_fill = int((health / MAX_HEALTH) * BAR_W) bg_rect = Rect((BAR_X, BAR_Y), (BAR_W, BAR_H)) fill_rect = Rect((BAR_X, BAR_Y), (bar_fill, BAR_H)) screen.draw.filled_rect(bg_rect, (60, 0, 0)) # dark red background screen.draw.filled_rect(fill_rect, "red") # bright red fill
Worked Example · Full HUD Demo
12 minAisyah builds a demo where pressing H deals 10 damage and pressing S scores 50 points. Save as hud_demo.py:
# hud_demo.py — score, lives and health bar import pgzrun WIDTH = 480 HEIGHT = 320 TITLE = "HUD Demo" MAX_HEALTH = 100 BAR_W = 160 BAR_H = 14 BAR_X = 8 BAR_Y = 60 score = 0 lives = 3 health = 100 def on_key_down(key): global score, health if key == keys.S: score += 50 if key == keys.H: health = max(0, health - 10) def health_colour(h): if h > 60: return "limegreen" elif h > 30: return "orange" return "red" def draw(): screen.fill("darkslategrey") bar_fill = int((health / MAX_HEALTH) * BAR_W) bg = Rect((BAR_X, BAR_Y), (BAR_W, BAR_H)) fg = Rect((BAR_X, BAR_Y), (bar_fill, BAR_H)) screen.draw.filled_rect(bg, (40, 40, 40)) screen.draw.filled_rect(fg, health_colour(health)) screen.draw.text(f"Score: {score}", topleft=(8, 8), fontsize=22, color="white") screen.draw.text(f"Lives: {lives}", topleft=(200, 8), fontsize=22, color="white") screen.draw.text(f"HP: {health}", topleft=(BAR_X, BAR_Y + BAR_H + 4), fontsize=16, color="lightgrey") screen.draw.text("S = +50 pts | H = -10 hp", center=(WIDTH // 2, HEIGHT - 20), fontsize=16, color="grey") pgzrun.go()
Try It Yourself
13 minWhen health reaches 0, subtract one life and restore health to 100. If lives is also 0, print "Game Over" to the terminal. Add this check inside on_key_down after reducing health.
Hint
if health <= 0: lives -= 1 health = 100 if lives <= 0: print("Game Over")
Instead of the text "Lives: 3", draw small filled circles (radius 8) in a row — one per remaining life — in the top-right corner. Hint: loop over range(lives) and space the circles 22 pixels apart.
Hint
for i in range(lives): cx = WIDTH - 20 - i * 22 screen.draw.filled_circle((cx, 20), 8, "tomato")
Mini-Challenge · Debug Kavitha's Health Bar
8 minKavitha's health bar sometimes draws with a negative width, which crashes Pygame Zero. Find the two bugs:
# kavitha_hud.py — buggy
import pgzrun
WIDTH = 400
HEIGHT = 300
MAX_HEALTH = 100
BAR_W = 150
health = 100
def on_key_down(key):
global health
if key == keys.H:
health -= 20 # Bug 1: no lower bound clamp
def draw():
screen.fill("black")
bar_fill = (health / MAX_HEALTH) * BAR_W # Bug 2: not converted to int
fg = Rect((10, 20), (bar_fill, 14))
screen.draw.filled_rect(fg, "red")
pgzrun.go()Show the fixes
# kavitha_hud.py — fixed import pgzrun WIDTH = 400 HEIGHT = 300 MAX_HEALTH = 100 BAR_W = 150 health = 100 def on_key_down(key): global health if key == keys.H: health = max(0, health - 20) # Fix 1: clamp at 0 def draw(): screen.fill("black") bar_fill = int((health / MAX_HEALTH) * BAR_W) # Fix 2: wrap in int() fg = Rect((10, 20), (bar_fill, 14)) screen.draw.filled_rect(fg, "red") pgzrun.go()
Bug 1: health could go negative, making bar_fill negative — a Rect with a negative width crashes. Bug 2: Rect requires integer dimensions; a float like 112.5 raises a TypeError.
It works if…
the bar shrinks to zero and stays there without crashing when H is pressed repeatedly
Recap
3 minDraw score and lives with screen.draw.text and f-strings. Draw a health bar with twofilled_rect calls: a dark background at full width, then a coloured fill whose width is int((health / max) * bar_width). Always clamp health to 0 with max(0, ...)so the bar never goes negative. Draw the HUD last so it sits on top of everything.
Vocabulary Card
- HUD
- Heads-up display — on-screen labels (score, lives, health) that overlay the game world.
- health bar
- Two overlapping filled rects: a dark background at full width, a coloured fill whose width scales with the current health value.
- max(0, value)
- Clamps a value so it never drops below 0. Prevents negative-width rectangles.
- int()
- Converts a float to an integer. Required because
Rectdimensions must be whole pixels.
Homework
4 minAdd a second health bar to the HUD demo — an enemy bar drawn in the top-right corner, starting at full health and decreasing by 15 each time the player presses E. Use a different colour (e.g. purple). Display "Enemy defeated!" in gold when it reaches 0. Save as hud_two_bars.py and bring a screenshot to the next class.
Sample · hud_two_bars.py
# hud_two_bars.py — player + enemy health bars import pgzrun WIDTH = 480 HEIGHT = 320 TITLE = "Two Health Bars" MAX_HP = 100 BAR_W = 150 BAR_H = 14 player_hp = 100 enemy_hp = 100 def on_key_down(key): global player_hp, enemy_hp if key == keys.H: player_hp = max(0, player_hp - 10) if key == keys.E: enemy_hp = max(0, enemy_hp - 15) def draw_bar(x, y, current, maximum, colour): fill = int((current / maximum) * BAR_W) screen.draw.filled_rect(Rect((x, y), (BAR_W, BAR_H)), (40, 40, 40)) screen.draw.filled_rect(Rect((x, y), (fill, BAR_H)), colour) def draw(): screen.fill("black") draw_bar(8, 8, player_hp, MAX_HP, "limegreen") screen.draw.text("Player", topleft=(8, 26), fontsize=14, color="white") draw_bar(WIDTH - BAR_W - 8, 8, enemy_hp, MAX_HP, "mediumpurple") screen.draw.text("Enemy", topleft=(WIDTH - BAR_W - 8, 26), fontsize=14, color="white") if enemy_hp <= 0: screen.draw.text("Enemy defeated!", center=(WIDTH // 2, HEIGHT // 2), fontsize=36, color="gold") pgzrun.go()
Extracting draw_bar() as a helper avoids repeating the two-rect logic. Pass x, y, current HP, max HP, and colour as parameters.