Managing Multiple Obstacles
Introduction
Welcome to Creating the Core Game Mechanics with Pygame! In the previous course, we built the skeleton of our dodge game: an 800 × 600 window, a game loop that handles quitting, a dark blue background, a single red obstacle that falls and recycles, and a green player that responds to the arrow keys. That scene works, but it is not yet a game: one obstacle is easy to sidestep forever.
This is the first of four lessons in this course, and our job here is to make the screen feel alive with several obstacles falling at once, each starting at its own random position and recycling forever. Here is roughly where we left off:
Three tools will improve that setup: an OBSTACLE_SIZE constant for the shared dimensions, a Python list to hold many Rects, and a helper function that builds them for us.
Replacing Repeated Dimensions with a Constant
Every obstacle is a 40 × 40 square. Instead of repeating the number 40 wherever an obstacle is created or positioned, we define it once with the other constants:
Now, changing the obstacle size later only requires editing one value. The same constant can control both dimensions and the horizontal spawning boundary.
Why a List Beats Copy-Pasting Variables
Imagine we wanted five obstacles and reached for the most obvious solution: five variables named obstacle1 through obstacle5. Every single piece of obstacle logic would then need to be written five times over:
- five lines to create the
Rects; - five lines to move them down each frame;
- five recycle checks with five pairs of position resets;
- five calls to
pygame.draw.rect.
That creates many near-identical lines. If we later change the obstacle size or behavior, we must edit every copy and hope we do not miss any. A list solves this neatly: we store all the Rects in one place, then write each rule once and let a for loop apply it to every item.
Writing the spawn_obstacles Helper Function
Creating those Rects is a standalone task, so we give it its own function. It sits outside main(), at the top level of the file:
Let us walk through it:
new_obstacles = []initializes an emptylist.for _ in range(count)runs exactlycounttimes. The underscore shows that we do not need the loop's index value.random.randint(0, WIDTH - OBSTACLE_SIZE)picks the left edge. We subtract the obstacle width because aRectgrows to the right from itsx.random.randint(-HEIGHT, 0)places each obstacle in the invisible strip above the window.pygame.Rect(x, y, OBSTACLE_SIZE, OBSTACLE_SIZE)uses the shared constant for both dimensions.return new_obstacleshands the finishedlistback to the caller.
Keeping this logic outside the game loop means we can call it whenever we need a fresh batch of obstacles, including when a future version of the game restarts.
Calling the Helper and Setting Up Game Variables
We use the helper inside main(), replacing the old single-obstacle setup. Since it relies on random values, import random belongs at the top of the file:
spawn_obstacles(5) runs once before the while loop starts. Calling it inside the loop would create five new obstacles every frame. Each obstacle has its own position, while obstacle_speed remains one shared number.
Moving Every Obstacle with a for Loop
With the list in place, movement becomes a small loop inside the game loop:
Every frame, the loop visits each Rect and moves it down. The loop variable refers to the same Rect stored in the list, so changing obstacle.y updates the actual game object.
Recycling Obstacles Back to the Top
An obstacle that leaves the bottom is repositioned above the window and reused:
Re-randomizing x gives the obstacle a new lane. The tighter vertical range of -100 to 0 returns it to play quickly, while the wider initial range of -HEIGHT to 0 staggers the opening wave.
Drawing All the Obstacles
Updating and drawing remain separate tasks:
The background is cleared first, followed by the player and each obstacle. pygame.display.flip() then presents the completed frame.
The Complete Program
Running this program shows five red squares drifting into view at scattered moments and lanes, falling steadily, and reappearing above the screen after passing the bottom.
Conclusion and Next Steps
Excellent work: our scene finally behaves like an arcade game. A constant keeps shared dimensions consistent, a list stores many game objects, a for loop applies one rule to all of them, and a helper function keeps object creation tidy and reusable.
There is one glaring gap, though: the squares pass straight through the player. In the next lesson, we fix that with pygame.Rect.colliderect().
