Learning Goals
3 minBy the end of this lesson you can:
- Spawn 100 Actors in a list using a single
forloop and keep them all moving withdt. - Explain why one flat loop over 100 actors is fast and a nested loop would be slow.
- Verify your game runs at a stable 60 FPS using Pygame Zero's built-in FPS display.
Warm-Up · O(n) vs O(n²)
5 minTwo approaches to process 100 sprites. Which one does more work?
# Approach A — one flat loop for sprite in sprites: # 100 steps sprite.x += sprite.speed * dt # Approach B — nested loop (don't do this!) for i in sprites: for j in sprites: # 100 × 100 = 10 000 steps pass
Show the answer
Approach A does 100 steps per frame. Approach B does 10 000 steps — 100× more work. With 200 sprites, A still does 200 steps; B jumps to 40 000. The nested approach is O(n²); the flat loop is O(n). For sprite movement, Approach A is always correct.
Quick Recap · Skills You Will Need
5 minBefore the timed challenge, here is a checklist of every technique you will combine:
- List building —
for i in range(100): sprites.append(Actor(...)) - Custom attributes —
s.speed = random.randint(...),s.vx = ... - Time-based movement —
s.x += s.speed * dtinsideupdate(dt) - Wrapping — reset
s.x = 0whens.x > WIDTH - Drawing all actors — one
for s in sprites: s.draw()loop indraw() - FPS display —
screen.draw.text(str(clock.get_fps()[:5]), ...)for a live readout
If any of those feel shaky, revisit PZ-09 or PZ-11 before continuing.
The Challenge · 100 Sprites, Smooth & Steady
25 minYour brief
Save a new file called hundred.py. Without looking at the solution below, write a game that:
- Spawns exactly 100 actors at random positions and random speeds between 40 and 300 px/s.
- Moves all of them to the right using
dt; wraps each one when it crossesWIDTH. - Draws all of them in
draw(). - Shows a sprite count and a rough FPS value on screen so you can confirm it is running smoothly.
- Uses no nested loops anywhere.
Set a timer for 20 minutes. Work from memory. When the timer ends, check the reveal below.
What good output looks like
The solution
Show the solution (open only after your 20 minutes)
# hundred.py — 100 actors, one flat loop import pgzrun import random WIDTH = 600 HEIGHT = 400 TITLE = "100 Sprites" sprites = [] for i in range(100): s = Actor("alien") s.x = random.randint(0, WIDTH) s.y = random.randint(20, HEIGHT - 20) s.speed = random.randint(40, 300) sprites.append(s) def draw(): screen.fill((15, 23, 42)) for s in sprites: s.draw() screen.draw.text( f"{len(sprites)} sprites", topleft=(10, 10), fontsize=20, color=(251, 191, 36), ) def update(dt): for s in sprites: s.x += s.speed * dt if s.x > WIDTH: s.x = 0 pgzrun.go()
The code is almost identical to the Cat Parade — only the count changed. That is the power of the list pattern: scaling up is free.
Try It Yourself
10 minChange range(100) to range(500). Does it still run smoothly? Note the FPS. At what point does it start to drop?
Hint
for i in range(500): # try 500, then 1000 s = Actor("alien") ...
Give each sprite a random s.vx (positive or negative, range -200 to 200) and s.vy (same). Move using both. Wrap on all four edges.
Hint
s.vx = random.randint(-200, 200) s.vy = random.randint(-200, 200) def update(dt): for s in sprites: s.x += s.vx * dt s.y += s.vy * dt if s.x > WIDTH: s.x = 0 if s.x < 0: s.x = WIDTH if s.y > HEIGHT: s.y = 0 if s.y < 0: s.y = HEIGHT
Mini-Challenge · Collision Counter
8 minAdd a special "target" actor placed at the centre. Every frame, count how many of the 100 sprites are within 30 px of the target. Display that count. This combines today's list loop with the .distance_to() Actor method.
It works if…
the on-screen counter rises and falls as sprites pass near the centre target
Show one possible solution
target = Actor("alien") target.pos = (WIDTH // 2, HEIGHT // 2) def draw(): screen.fill((15, 23, 42)) for s in sprites: s.draw() target.draw() nearby = sum(1 for s in sprites if s.distance_to(target) < 30) screen.draw.text(f"Near target: {nearby}", topleft=(10, 10), fontsize=22, color="gold") def update(dt): for s in sprites: s.x += s.speed * dt if s.x > WIDTH: s.x = 0
The key addition is one line: sum(1 for s in sprites if s.distance_to(target) < 30). This is a generator expression — a compact loop that counts matching sprites.
Recap
3 minA single flat loop over a list of 100 actors does 100 steps per frame — fast enough for 60 FPS. A nested loop would do 10 000 steps — avoid it for simple sprite processing. The list + dt pattern scales from 10 sprites to 1000 with no structural changes.
Vocabulary Card
- O(n) — linear time
- Work grows in proportion to the number of items. One flat loop over n sprites = n steps.
- O(n²) — quadratic time
- Work grows as n × n. A nested loop over n sprites = n² steps — avoid for large lists.
- FPS (frames per second)
- How many times the game redraws per second. 60 FPS feels smooth; below 30 FPS starts to stutter.
- actor.distance_to(other)
- Returns the pixel distance between two Actors. Useful for proximity checks without a nested loop.
Homework
4 minWrite blizzard.py — 80 small actors falling downward (vy only, no horizontal movement) at random speeds between 50 and 250 px/s. Wrap them back to the top (y = 0) when they pass HEIGHT. Show the count on screen. Bring a screenshot to the next class.
Stretch. Give each actor a random starting x and a tiny random horizontal drift (vx = random.randint(-20, 20)) so they sway slightly as they fall.
Sample · blizzard.py
# blizzard.py — 80 falling actors import pgzrun import random WIDTH = 600 HEIGHT = 400 TITLE = "Blizzard" flakes = [] for i in range(80): f = Actor("alien") f.x = random.randint(0, WIDTH) f.y = random.randint(0, HEIGHT) f.vy = random.randint(50, 250) flakes.append(f) def draw(): screen.fill("midnightblue") for f in flakes: f.draw() screen.draw.text(f"{len(flakes)} flakes", topleft=(10, 10), fontsize=22, color="white") def update(dt): for f in flakes: f.y += f.vy * dt if f.y > HEIGHT: f.y = 0 pgzrun.go()
The structure is identical to the Cat Parade — only the axis of movement changes (vy instead of speed on x). That is the point: the list pattern is reusable.