Learning Goals
3 minBy the end of this lesson you can:
- Implement a shake timer that offsets every draw call by a small random amount for a fixed number of frames.
- Implement a hit-flash by briefly drawing a white filled rectangle over a sprite's bounding box.
- Combine both effects in a single mini-game so that an impact triggers shake and flash at the same time.
Warm-Up · Jitter Maths
5 minIn PZ-43 you spawned particles every frame. Today you tweak every draw position. First, warm up with random.randint. What does this print?
import random for _ in range(5): offset = random.randint(-8, 8) print(offset)
Show the answer
Five different integers, each somewhere between −8 and 8 (inclusive). The exact values change every run — that unpredictability is exactly what makes shake look real.
Screen shake = draw everything slightly off-centre for a short time. Hit flash = briefly paint a white box where the sprite lives. Both effects use timers you count down each frame.
New Concept · Shake Timers & Flash Timers
12 minThink of screen shake like a hand-held camera bump. A film editor adds a brief jitter — three or four frames — then the camera steadies. In Pygame Zero you mimic this by keeping a shake timer: a counter that starts at a positive number and counts down to zero each update(). While it is above zero, add a small random offset to every position you draw.
Shake timer pattern
import pgzrun import random WIDTH = 480 HEIGHT = 320 shake_timer = 0 # counts down; 0 = no shake def trigger_shake(frames=12): global shake_timer shake_timer = frames def update(): global shake_timer if shake_timer > 0: shake_timer -= 1 pgzrun.go()
When something hits, call trigger_shake(). Each frame update() ticks the timer down. Easy.
Using the offset in draw()
Inside draw(), pick a random offset only while the timer is alive, then shift every drawing call by that amount.
def draw(): if shake_timer > 0: ox = random.randint(-6, 6) oy = random.randint(-6, 6) else: ox, oy = 0, 0 screen.fill("black") # pass ox, oy into every position you draw screen.draw.circle((240 + ox, 160 + oy), 40, "white")
Hit-flash pattern
A hit flash is even simpler: keep a flash timer per sprite. While it is above zero, draw a semi-transparent white rectangle over the sprite instead of (or on top of) the normal sprite image.
flash_timer = 0 def trigger_flash(frames=8): global flash_timer flash_timer = frames def draw(): # ... draw normal game ... if flash_timer > 0: r = Rect((player.x - 24, player.y - 24), (48, 48)) screen.draw.filled_rect(r, (255, 255, 255))
Worked Example · Shake & Flash Catcher
12 minThe story
Hafiz builds a catcher. A red ball falls from the top. Click it to destroy it — and feel the hit. Save as shake_flash.py. Place a stand-in image or draw a filled circle for the "ball".
# shake_flash.py — screen shake + hit flash demo import pgzrun import random WIDTH = 480 HEIGHT = 320 TITLE = "Shake & Flash" ball_x = 240 ball_y = 20 ball_speed = 3 shake_timer = 0 flash_timer = 0 score = 0 ox, oy = 0, 0 # shake offset, recomputed each frame
def update(): global ball_y, ball_x, shake_timer, flash_timer, ox, oy ball_y += ball_speed if ball_y > HEIGHT + 30: ball_y = -30 ball_x = random.randint(30, WIDTH - 30) if shake_timer > 0: shake_timer -= 1 ox = random.randint(-7, 7) oy = random.randint(-7, 7) else: ox, oy = 0, 0 if flash_timer > 0: flash_timer -= 1
def on_mouse_down(pos, button): global score, shake_timer, flash_timer, ball_y, ball_x bx, by = ball_x + ox, ball_y + oy dist = ((pos[0] - bx) ** 2 + (pos[1] - by) ** 2) ** 0.5 if dist < 28: score += 1 shake_timer = 14 flash_timer = 8 ball_y = -30 ball_x = random.randint(30, WIDTH - 30)
def draw(): screen.fill("black") # draw ball at shaken position bx, by = ball_x + ox, ball_y + oy screen.draw.filled_circle((bx, by), 26, "crimson") # hit flash overlay if flash_timer > 0: r = Rect((bx - 28, by - 28), (56, 56)) screen.draw.filled_rect(r, (255, 255, 255)) screen.draw.text(f"Score: {score}", topleft=(10, 10), fontsize=26, color="white") pgzrun.go()
ox and oy are set once in update() and read in draw(). Every element shifts by the same amount — the whole scene moves together, just like a real camera bump.
Try It Yourself
13 minChange the shake magnitude from 7 pixels to 15 pixels and the duration from 14 frames to 20 frames. Note how it feels different. Then try 3 pixels and 5 frames. Which feels best?
Hint
# in update(): ox = random.randint(-15, 15) # was -7, 7 oy = random.randint(-15, 15) # in on_mouse_down(): shake_timer = 20 # was 14
Instead of a plain white flash, make the overlay colour cycle between white and yellow each frame. Use flash_timer % 2 to alternate.
Hint
if flash_timer > 0: flash_color = (255, 255, 255) if flash_timer % 2 == 0 else (255, 220, 0) r = Rect((bx - 28, by - 28), (56, 56)) screen.draw.filled_rect(r, flash_color)
Mini-Challenge · Double Impact
8 minPriya wants her game to feel extra dramatic when the player's score hits a multiple of 5. Take the worked-example game and add a big shake (25 frames, ±12 px) that fires only on multiples-of-5 scores, while the normal click still uses the small shake (14 frames, ±7 px).
Combine today's shake timer with the modulo operator (%) from Level 1.
It works if…
clicking the 5th ball triggers a noticeably bigger screen shake than the other clicks
Show one possible solution
def on_mouse_down(pos, button): global score, shake_timer, flash_timer, ball_y, ball_x bx, by = ball_x + ox, ball_y + oy dist = ((pos[0] - bx) ** 2 + (pos[1] - by) ** 2) ** 0.5 if dist < 28: score += 1 if score % 5 == 0: shake_timer = 25 # big shake on milestone else: shake_timer = 14 flash_timer = 8 ball_y = -30 ball_x = random.randint(30, WIDTH - 30)
The only change is the if score % 5 == 0 branch that picks a larger timer. Everything else stays the same.
Recap
3 minScreen shake works by computing a random pixel offset each frame while a timer counts down, then adding that offset to every position you draw. Hit flash works by drawing a coloured rectangle over the sprite's bounding box while a second timer counts down. Both effects are cheap and powerful game-feel improvements.
Vocabulary Card
- shake timer
- A variable that counts down from a positive number. While > 0, each draw is offset by a small random amount.
- shake offset
- A pair
(ox, oy)of random integers computed once per frame inupdate()and reused indraw()so the whole scene shifts together. - hit flash
- A brief filled rectangle drawn over a sprite to give visual feedback when it is hit. Controlled by its own countdown timer.
- game feel (juice)
- The collection of small visual and audio reactions that make actions feel satisfying and weighty.
Homework
4 minTake any game you built earlier in the module (e.g. the gravity catcher from PZ-37 or the asteroids game from PZ-34) and add:
- A shake timer that fires when the player loses a life or an enemy is destroyed.
- A hit flash on the affected sprite.
Save it as juicy_game.py and bring a screenshot or a short screen-recording to the next class.
Sample · juicy_game.py (shake + flash additions)
# Added to any existing game — the two new globals: shake_timer = 0 flash_timer = 0 ox, oy = 0, 0 # In update(), add: if shake_timer > 0: shake_timer -= 1 ox = random.randint(-7, 7) oy = random.randint(-7, 7) else: ox, oy = 0, 0 if flash_timer > 0: flash_timer -= 1 # When a hit occurs, add: shake_timer = 14 flash_timer = 8 # In draw(), shift every position by ox, oy and add flash overlay
The pattern is always the same: two timers, one offset pair, one overlay. Slot them into whichever game you choose.