All 5.2 34 min read 10 question quiz 17 exam marks

Making Games with pygame

The game loop, drawing, movement, keyboard input, collision detection and sprites, ending with a complete playable game.

The game loop

Python
import pygame

pygame.init()

WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("My Game")
clock = pygame.time.Clock()

TEAL = (3, 150, 157)
LILAC = (202, 171, 213)
DEEP = (3, 92, 88)

running = True
while running:
    # 1. HANDLE EVENTS
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # 2. UPDATE
    # game logic goes here

    # 3. DRAW
    screen.fill(DEEP)
    pygame.draw.circle(screen, LILAC, (400, 300), 40)
    pygame.display.flip()

    clock.tick(60)      # cap at 60 frames per second

pygame.quit()

Every game does these three things, in this order

  1. Handle events. What did the player do? Keys, mouse, closing the window.
  2. Update. Move everything, check collisions, update the score, apply the rules.
  3. Draw. Clear the screen, draw everything in its new position, show it.
Why you must clear the screen every frame

If you do not call screen.fill(), everything from the previous frame is still there, so a moving object leaves a smear behind it. Clear, draw, show, every single frame.

Coordinates

Pygame's origin (0, 0) is the top left corner. x increases to the right and y increases downwards, which is the opposite of maths graphs. Moving something down means increasing y.

The game loop: handle events, update, draw and tick, 60 times a second, with the code that does itEvery game runs a loop, one pass of which is a frame. One, handle events: read the keys, the mouse and the quit button. Two, update: move things and check for hits. Three, draw: clear the screen, draw everything and flip the display. Four, tick: clock dot tick 60 pauses just long enough to hold the game at 60 frames a second. The code is a while loop with a for loop over pygame.event.get, an update call, a draw call and clock.tick 60. Because the frames come so fast the game feels smooth.Every frame, 60 times a secondEventskeys, mouse, quit1Updatemove things, check hits2Drawclear, draw, flip3Tickclock.tick(60)460Each pass is one frame. Games feel smoothbecause the frames come so fast.game.pyrunning = Truewhile running:    for event in pygame.event.get():        if event.type == pygame.QUIT:            running = False    update()    draw()    clock.tick(60)lines 3 to 5: eventsline 6: updateline 7: drawline 8: tickclock.tick(60) pauses just long enoughto hold the game at 60 frames a second
  1. Handle events: keys, mouse and quit.
  2. Update: move things and check for collisions.
  3. Draw: clear the screen, draw everything, flip the display.
  4. Tick: hold the game at 60 frames a second.
Input, update, draw, repeat. Moving things in update and drawing them in draw keeps the code easy to follow.

Drawing and movement

Shapes

Python
pygame.draw.rect(screen, TEAL, (x, y, width, height))
pygame.draw.rect(screen, TEAL, (x, y, w, h), 3)          # outline, 3 pixels
pygame.draw.circle(screen, LILAC, (cx, cy), radius)
pygame.draw.line(screen, TEAL, (x1, y1), (x2, y2), 2)
pygame.draw.polygon(screen, LILAC, [(0, 0), (50, 0), (25, 40)])

Text

Python
font = pygame.font.Font(None, 36)
text = font.render(f"Score: {score}", True, (255, 255, 255))
screen.blit(text, (10, 10))

Images

Python
player_image = pygame.image.load("player.png").convert_alpha()
player_image = pygame.transform.scale(player_image, (48, 48))
screen.blit(player_image, (x, y))

Keyboard input, two ways

Continuous, for movement, checked every frame:

Python
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
    player_x -= speed
if keys[pygame.K_RIGHT]:
    player_x += speed
if keys[pygame.K_UP]:
    player_y -= speed
if keys[pygame.K_DOWN]:
    player_y += speed

Single press, for actions that should happen once:

Python
for event in pygame.event.get():
    if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_SPACE:
            fire_bullet()
Use the right one

If firing is handled with get_pressed(), holding the key fires sixty bullets a second. If movement is handled with KEYDOWN, the player moves one pixel per press.

Keeping things on screen

Python
player_x = max(0, min(WIDTH - player_width, player_x))
player_y = max(0, min(HEIGHT - player_height, player_y))

Rects, collisions and sprites

Rect, the most useful object in pygame

Python
player = pygame.Rect(100, 100, 40, 40)      # x, y, width, height

player.x += 5
player.centerx = WIDTH // 2
player.bottom = HEIGHT

pygame.draw.rect(screen, TEAL, player)

Rects have left, right, top, bottom, centerx, centery, center, size and more, and setting any of them moves the rectangle sensibly.

Collision detection

Python
if player.colliderect(enemy):
    lives -= 1

if player.collidepoint(mouse_x, mouse_y):
    print("Clicked the player")

hit_index = player.collidelist(coin_rects)
if hit_index != -1:
    score += 10
    coin_rects.pop(hit_index)

Sprites

For anything beyond a few objects, use pygame's sprite system.

Python
class Player(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((40, 40))
        self.image.fill((3, 150, 157))
        self.rect = self.image.get_rect(topleft=(x, y))
        self.speed = 5

    def update(self, keys):
        if keys[pygame.K_LEFT]:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        self.rect.x = max(0, min(WIDTH - self.rect.width, self.rect.x))


class Coin(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((20, 20), pygame.SRCALPHA)
        pygame.draw.circle(self.image, (202, 171, 213), (10, 10), 10)
        self.rect = self.image.get_rect(topleft=(x, y))

    def update(self, keys):
        self.rect.y += 3
        if self.rect.top > HEIGHT:
            self.kill()


all_sprites = pygame.sprite.Group()
coins = pygame.sprite.Group()

player = Player(380, 540)
all_sprites.add(player)

# in the loop
all_sprites.update(keys)
collected = pygame.sprite.spritecollide(player, coins, dokill=True)
score += len(collected) * 10
all_sprites.draw(screen)

Group.update() calls update() on every sprite, and Group.draw() draws them all. This is polymorphism doing real work.

A Rect with its corner at the top left, and the four tests that make colliderect trueA pygame Rect is a rectangle given by its x, y, width and height, and the screen's origin, 0 0, is at the top left, with x going right and y going down. The player is Rect 40, 50, 150, 100 and the coin is Rect 128, 110, 100, 90, and the two overlap in a shaded region. player dot colliderect coin is true when four tests are all true: the player's left is less than the coin's right, the player's right is greater than the coin's left, the player's top is less than the coin's bottom, and the player's bottom is greater than the coin's top. If any one test is false, for example when there is a gap between the player's right edge and the coin's left edge, the rectangles do not overlap.A Rect: x, y, width, height1xy(0, 0) top leftplayerRect(40,50,150,100)coinRect(128,110,100,90)the overlapplayer.colliderect(coin)2player.left < coin.rightplayer.right > coin.leftplayer.top < coin.bottomplayer.bottom > coin.topall four true: they overlapOne false test is enough for a miss3gapplayer.right > coin.leftis False, so colliderect() is False
  1. A Rect has x, y, width and height. The origin is the top left, and y increases downwards.
  2. colliderect is true only if all four tests are true: the rectangles overlap on both the x axis and the y axis.
  3. If any one test is false there is a gap, so there is no collision.
Two rectangles overlap only when they overlap in both directions. Check each axis separately.

A complete game

Python
import pygame
import random

pygame.init()

WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Catcher")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 36)
big_font = pygame.font.Font(None, 72)

DEEP = (3, 92, 88)
TEAL = (3, 150, 157)
LILAC = (202, 171, 213)
AQUA = (107, 196, 202)
WHITE = (244, 241, 248)


class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((90, 18))
        self.image.fill(AQUA)
        self.rect = self.image.get_rect(midbottom=(WIDTH // 2, HEIGHT - 20))
        self.speed = 8

    def update(self, keys):
        if keys[pygame.K_LEFT]:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        self.rect.x = max(0, min(WIDTH - self.rect.width, self.rect.x))


class Falling(pygame.sprite.Sprite):
    def __init__(self, good=True):
        super().__init__()
        self.good = good
        size = 22 if good else 26
        self.image = pygame.Surface((size, size), pygame.SRCALPHA)
        colour = LILAC if good else (163, 48, 73)
        pygame.draw.circle(self.image, colour, (size // 2, size // 2), size // 2)
        self.rect = self.image.get_rect(topleft=(random.randint(0, WIDTH - size), -size))
        self.speed = random.randint(3, 7)

    def update(self, keys):
        self.rect.y += self.speed
        if self.rect.top > HEIGHT:
            self.kill()


def draw_text(text, font_obj, colour, y):
    surface = font_obj.render(text, True, colour)
    screen.blit(surface, surface.get_rect(center=(WIDTH // 2, y)))


def run_game():
    player = Player()
    all_sprites = pygame.sprite.Group(player)
    falling = pygame.sprite.Group()

    score = 0
    lives = 3
    spawn_timer = 0
    running = True
    game_over = False

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    return False
                if game_over and event.key == pygame.K_RETURN:
                    return True

        keys = pygame.key.get_pressed()

        if not game_over:
            spawn_timer += 1
            if spawn_timer >= 30:
                spawn_timer = 0
                item = Falling(good=random.random() > 0.25)
                falling.add(item)
                all_sprites.add(item)

            all_sprites.update(keys)

            for item in pygame.sprite.spritecollide(player, falling, dokill=True):
                if item.good:
                    score += 10
                else:
                    lives -= 1
                    if lives <= 0:
                        game_over = True

        screen.fill(DEEP)
        all_sprites.draw(screen)

        screen.blit(font.render(f"Score: {score}", True, WHITE), (14, 12))
        screen.blit(font.render(f"Lives: {lives}", True, WHITE), (WIDTH - 130, 12))

        if game_over:
            draw_text("Game Over", big_font, LILAC, HEIGHT // 2 - 30)
            draw_text(f"Final score: {score}", font, WHITE, HEIGHT // 2 + 30)
            draw_text("Enter to play again, Escape to quit", font, AQUA, HEIGHT // 2 + 70)

        pygame.display.flip()
        clock.tick(60)

    return False


play_again = True
while play_again:
    play_again = run_game()

pygame.quit()

What to notice

  • One class per kind of thing, each with its own update
  • Groups handle updating and drawing everything, whatever it is
  • dokill=True removes the caught item automatically
  • Off screen sprites call kill(), so they do not accumulate forever and slow the game down
  • The game over state stays inside the same loop rather than needing a separate one
  • clock.tick(60) caps the frame rate, so the game runs at the same speed on every machine

Key terms you must be able to define

Game loop
The loop that repeatedly handles events, updates the game state and draws the frame.
Surface
An image in memory that can be drawn onto and then drawn onto the screen.
blit
Copying one surface onto another, which is how images and text are drawn.
Rect
A rectangle object holding position and size, used for placement and collision detection.
Sprite
An object in a game with an image and a rect, usually with its own update method.
Group
A collection of sprites that can be updated and drawn together.
colliderect
A Rect method that returns True when two rectangles overlap.
Frame rate
How many times per second the game loop runs, capped with clock.tick.
Event queue
The list of things the player has done since the last frame, read with pygame.event.get().

Flashcards

Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.

DefineGame loop
AnswerThe loop that repeatedly handles events, updates the game state and draws the frame.
DefineSurface
AnswerAn image in memory that can be drawn onto and then drawn onto the screen.
Defineblit
AnswerCopying one surface onto another, which is how images and text are drawn.
DefineRect
AnswerA rectangle object holding position and size, used for placement and collision detection.
DefineSprite
AnswerAn object in a game with an image and a rect, usually with its own update method.
DefineGroup
AnswerA collection of sprites that can be updated and drawn together.
Definecolliderect
AnswerA Rect method that returns True when two rectangles overlap.
DefineFrame rate
AnswerHow many times per second the game loop runs, capped with clock.tick.

Reach the top grade on this topic

  • Always structure the loop as events, then update, then draw, in that order
  • Clear the screen at the start of every draw phase or you get smearing
  • Use get_pressed for continuous movement and KEYDOWN for one off actions
  • Remove sprites that leave the screen with kill(), or the game slows down over time
  • Cap the frame rate with clock.tick so the game runs the same speed on any machine
  • Put each kind of object in its own class so a group can update everything polymorphically

Mistakes that cost marks here

Avoid these
  • Forgetting screen.fill(), so moving objects leave trails behind them.
  • Forgetting pygame.display.flip(), so nothing ever appears.
  • Using KEYDOWN for movement, so the player moves one pixel per key press.
  • Never removing off screen sprites, so the game gradually slows to a crawl.
  • Forgetting that y increases downwards, so everything moves the wrong way vertically.
  • Leaving out clock.tick, so the game runs at a completely different speed on a faster computer.
Fun fact

Every game ever made runs the same three step loop thousands of times a second: handle input, update the world, draw the world. Once you have seen it, you cannot unsee it.

Knowledge check

0 of 10 answered
1What are the three stages of a game loop, in order?
2Why must screen.fill() be called each frame?
3In pygame, moving an object down the screen means:
4Which should be used for continuous player movement?
5What does player.colliderect(enemy) return?
6What does clock.tick(60) do?
7What does pygame.display.flip() do?
8Why should sprites that move off screen be removed with kill()?
9What does all_sprites.update(keys) do?
10Which should be used for firing a single shot when space is pressed?
Score 0 / 10

Exam-style questions

Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.

17 marks
1Describe the three stages of a game loop.3 marks
Describe

Mark scheme0 / 3
  • LHandle events such as key presses and closing the window
  • LUpdate the game state, moving objects and checking collisions
  • LDraw everything to the screen and display the frame
Model answer

The first stage handles events, reading everything the player has done since the last frame, such as key presses, mouse clicks and the window being closed. The second stage updates the game state: objects are moved according to input and their own behaviour, collisions are checked, and things such as the score and the number of lives are adjusted. The third stage draws the frame, clearing the screen and then drawing every object in its new position before calling display.flip to make the completed frame visible. These three stages repeat, typically sixty times a second.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
2Explain why the screen must be cleared at the start of each frame in a pygame program.2 marks
Explain

Mark scheme0 / 2
  • LPygame does not clear the screen automatically between frames
  • LWithout clearing, previous frames remain visible so moving objects leave trails
Model answer

Pygame does not clear the display between frames, so whatever was drawn last time is still there when the next frame is drawn on top of it. If the screen is not cleared with screen.fill at the start of the drawing stage, an object that has moved will be drawn in its new position while its old position is still visible, so it leaves a continuous trail across the screen rather than appearing to move.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
3Explain the difference between using pygame.key.get_pressed() and the KEYDOWN event, and state when each should be used.4 marks
Explain

Mark scheme0 / 4
  • Lget_pressed reports which keys are currently held down
  • LIt is checked every frame so the action repeats while the key is held
  • LKEYDOWN fires once at the moment a key is pressed
  • LUse get_pressed for movement and KEYDOWN for one off actions such as firing or jumping
Model answer

The get_pressed function returns the current state of every key, so checking it inside the game loop tells you which keys are being held down at that moment, and any action taken repeats on every frame the key remains down. The KEYDOWN event, by contrast, appears in the event queue once at the instant a key is pressed and does not repeat while it is held. Movement should use get_pressed, so that holding the left arrow moves the player smoothly and continuously. One off actions such as firing a shot, jumping or selecting a menu item should use KEYDOWN, because using get_pressed for firing would produce sixty shots a second while the key was held.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
4Write pygame code for a sprite class representing a coin that falls down the screen and removes itself when it goes off the bottom.5 marks
Write

Mark scheme0 / 5
  • LDefines a class inheriting from pygame.sprite.Sprite
  • LCalls super().__init__() in the constructor
  • LCreates an image and a rect for the sprite
  • LDefines update to increase the y coordinate
  • LRemoves the sprite with kill() when it passes the bottom of the screen
Model answer

class Coin(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((20, 20), pygame.SRCALPHA) pygame.draw.circle(self.image, (202, 171, 213), (10, 10), 10) self.rect = self.image.get_rect(topleft=(x, y)) self.speed = 4 def update(self): self.rect.y += self.speed if self.rect.top > HEIGHT: self.kill() The class inherits from pygame.sprite.Sprite so it can be added to a group, and super().__init__() must be called or the sprite machinery will not be set up. Every sprite needs an image and a rect, because groups use those to draw and position it. The update method increases y each frame, since y increases downwards in pygame, and calls kill once the top of the coin has passed the bottom of the screen, which removes it from every group it belongs to so it is no longer updated or drawn.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
5Explain why clock.tick(60) is used in a pygame game loop and what would happen without it.3 marks
Explain

Mark scheme0 / 3
  • LIt limits the loop to a maximum of 60 iterations per second
  • LThis makes the game run at the same speed on different computers
  • LWithout it the loop runs as fast as possible, so the game is far too fast on a powerful machine
Model answer

The tick method limits the game loop to a maximum number of iterations per second, in this case sixty, by pausing briefly at the end of each pass if the frame was completed early. This matters because the amount an object moves per frame is fixed in the code, so the speed things appear to travel depends entirely on how many frames happen each second. Without the cap, the loop would run as fast as the hardware allows, which might be several hundred frames a second on a modern machine, and the game would be completely unplayable while running at a totally different speed on a slower computer. Capping the frame rate makes the game behave identically regardless of the hardware it is running on.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
Machine mark 0 / 17