Building Pygame Game States

Introduction

Welcome to Polishing your first Pygame game! We already have a playable dodge-game prototype: the player moves with the arrow keys, red obstacles fall from above, the score increases when an obstacle is dodged, and sound effects play for points and collisions.

The game currently has only one phase. It begins immediately and closes after a collision. Real games usually move between phases such as a title screen, active play, and a game-over screen.

In this lesson, we will add a game_state variable that identifies the current phase and build a start screen that waits for the Enter key.

The game_state Variable: One String, Many Phases

A simple state machine lets the game be in one named phase at a time. Add game_state next to the other setup values before the loop:

Python
    score = 0
    game_state = "start"

    running = True
    while running:

Strings work well because names such as "start" and "playing" are readable, easy to compare with ==, and easy to extend later.

The assignment belongs before the loop. If game_state = "start" were assigned inside the loop, it would run on every iteration and reset the phase to "start", preventing a transition to "playing" from persisting.

Branching the Game Loop with if / elif

The state variable controls which block runs during each frame:

Python
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        if game_state == "start":
            ...

        elif game_state == "playing":
            ...

        pygame.display.flip()
        clock.tick(FPS)
        await asyncio.sleep(0)
Flowchart showing shared frame tasks and the start and playing game-state branches

Some work is shared by every state:

  • Event processing must always run so the window can close.
  • pygame.display.flip() presents the completed frame.
  • clock.tick(FPS) controls the frame rate.
  • await asyncio.sleep(0) yields control in the asynchronous environment.

Movement, obstacle updates, collision checks, and gameplay drawing belong only in the "playing" branch. Moving those lines into the branch changes their indentation but not their behavior.

Because game_state starts as "start", gameplay no longer updates immediately. Before we draw the start screen, the result is simply a blank window filled with the background color.

Adding a Title Font

The title should be larger than the score and prompt text, so create a second font during setup:

Python
    font = pygame.font.SysFont(None, 36)
    title_font = pygame.font.SysFont(None, 72)

Font objects should be created once before the loop and reused instead of being recreated every frame.

Drawing the Start Screen

The "start" branch fills the background and displays two centered lines:

Python
        if game_state == "start":
            screen.fill(DARK_BLUE)
            title_text = title_font.render("Dodge Game", True, WHITE)
            screen.blit(title_text, title_text.get_rect(center=(WIDTH // 2, 200)))
            prompt_text = font.render("Press Enter to Play", True, WHITE)
            screen.blit(prompt_text, prompt_text.get_rect(center=(WIDTH // 2, 350)))

get_rect(center=(...)) creates a rectangle for the rendered text and positions it by its center. This avoids manually measuring the text width.

Reacting to Keypresses with KEYDOWN Events

Movement uses pygame.key.get_pressed() because it should continue while a key is held. A state transition should happen once when a key is pressed, so it belongs in a KEYDOWN event:

Python
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RETURN:
                    if game_state == "start":
                        game_state = "playing"

The state check ensures that Enter starts gameplay only from the start screen. Later, the same key can have a different purpose in the game-over state.

Tracing the Finished Program

The final loop has a clear structure:

Python
    score = 0
    game_state = "start"

    running = True
    while running:
        for event in pygame.event.get():
            # quit and one-time key events

        if game_state == "start":
            # draw the start screen

        elif game_state == "playing":
            # update and draw gameplay

        pygame.display.flip()
        clock.tick(FPS)
        await asyncio.sleep(0)

The start screen is drawn until the player presses Enter. That event changes game_state to "playing", allowing the gameplay branch to run. The player, obstacles, and score were already created during setup, so the game begins immediately.

A collision still ends the program in this unit. In the next unit, we will replace that exit with a "game_over" state.

Conclusion and Next Steps

A game_state string now identifies the current phase, if/elif branches route each frame to the correct behavior, and KEYDOWN events trigger one-time transitions. This structure will support the game-over screen, restarting, music, and difficulty scaling added later in the course.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal