Adding Scores with Pygame
Introduction: From Survival to Scoring
Our game now has reusable obstacle creation, movement, recycling, and collision detection. Mechanically it is complete: you can play it, and you can lose it. What it still lacks is any sense of progress. A run that lasted two seconds and a run that lasted two minutes end in exactly the same way, with the window closing and nothing to show for the effort.
A score fixes that. It turns "avoid the squares" into "avoid the squares for as long as possible", gives the player a number to beat, and gives us a reason to keep playing one more round.
This lesson adds three things:
- A persistent numeric
scorethat survives from frame to frame; - A font object capable of turning numbers into pictures;
- Rendered score text drawn in the top-left corner of the window.
Along the way we will meet two of Pygame's most useful methods: font.render() and screen.blit().
Creating a Font Object Before the Loop
Pygame draws pixels, not letters. To show text we first need a font object, which knows how to turn a string into a drawable image:
pygame.font.SysFont() takes two arguments. The first is the font name; passing None asks Pygame for the default system font, which is convenient because it always exists and requires no extra files. The second, 36, is the font height in pixels — roughly how tall the rendered characters will be.
We create the font once, after pygame.init() and before the while loop. Building a font object involves loading and preparing font data, which is far too expensive to repeat sixty times per second. One font object can render as many different strings as we like.
Starting the Score at Zero
The score itself is an ordinary integer, and it belongs in the setup section next to the other game variables:
Placement is everything here. Because score = 0 runs once, before the loop, the variable keeps its value from one frame to the next and can grow over the course of the run. If we accidentally wrote it inside the loop, the score would be reset to zero at the start of every single frame and would never climb above one.
Incrementing the Score
Now we need a rule for earning points. A natural choice for a dodge game: the player earns one point each time an obstacle safely passes the bottom of the screen — that is, exactly where we already recycle obstacles.
Notice the indentation. score += 1 sits inside the if block, so it runs only in the frame where an obstacle actually crosses the bottom edge — once per dodged obstacle. Moving that line one level to the left would put it in the movement loop instead, adding five points every frame and turning the score into meaningless noise.
Rendering the Score
Pygame cannot draw a Python string directly onto the screen. The font object must first convert it into a Surface, an image in memory containing the drawn characters:
The three arguments are:
- The text to draw.
str(score)converts the integer into a string so it can be joined to"Score: "; concatenating a string with anintdirectly would raise aTypeError. Trueenables antialiasing, which smooths the edges of the letters. PassingFalserenders faster but looks jagged.WHITEis the color of the text, defined among our constants as(255, 255, 255).
Because a Surface is a fixed picture, it does not update by itself when score changes. Each new value needs a new call to render(), which is why this line lives inside the game loop.
A note on performance: the code calls
font.render(...)inside the main game loop on every single frame, even when the score hasn't changed. While fine for our simple tutorial with a low object count, standard Pygame optimization practices recommend re-rendering the text surface only when the score value actually changes to avoid redundant CPU overhead. A common approach is to keep the last rendered Surface in a variable and refresh it only inside theifblock wherescore += 1happens.
Blitting the Score
Once we have a Surface, blit() copies it onto another Surface — in our case, the screen:
The word blit is short for "block image transfer", and it is the standard Pygame way to draw one image onto another. The second argument is the position of the text's top-left corner, so (10, 10) leaves a comfortable ten-pixel margin from the top and left edges of the window.
Draw Order
Drawing in Pygame is like painting: whatever is drawn last appears on top. That makes the order of our drawing block meaningful:
The background is cleared first, then the player and obstacles are painted over it, and finally the score is placed on top. Drawing the score last guarantees that a falling obstacle can never hide it, which is exactly what we want from a HUD (heads-up display) element. And, as always, pygame.display.flip() comes last of all, presenting the finished frame to the player.
The Complete Program
Conclusion and Next Steps
Three simple pieces gave the game a goal: a persistent variable tracks the score, font.render() turns the current value into a Surface, and screen.blit() places that Surface on the screen above everything else. Dodging is now rewarded, and every run ends with a number worth beating.
The game still communicates only through pixels, though. In the final unit we add sound effects, so a point dings and a crash thuds.
