Guide

To get started with this library first create a new project and create a virtual environment & activate it (optional but recommended). Once you’ve done that you may continue.

Installation

Install game_state through pip in your terminal-

(.venv) $ pip install game_state

Since game_state does not have any dependancies, we need to manually install the pygame library (or pygame-ce if you’re using that).

(.venv) $ pip install pygame-ce

Using the Library

Note

This library has been updated to version 2.0, introducing breaking changes that are not backward compatible with version 1.x.

If you are using a version older than 2.0.0, refer to the v1 documentation for guidance: game-state v1.1.3 Documentation

We highly recommend upgrading to version 2.0, as it offers significant optimizations and improvements over v1.

Let’s create a simple pygame script having two screens. One screen will display green colour and the other will display blue with a moveable red square.

Setting up the basics

import pygame

from game_state import State, StateManager


pygame.init()
pygame.display.init()
pygame.display.set_caption("Game State Example")

speed = 200  # Player speed
BLUE = (0, 255, 0)
GREEN = (0, 0, 255)

Now that we have imported and set the display of our app, let’s create a main menu screen.

Creating a simple screen

class MainMenuState(State, state_name="MainMenu"):
   def process_event(self, event: pygame.event.Event) -> None:
      # This is executed in our our game loop for every event.

      if event.type == pygame.QUIT:
            # We set the state manager's is_running variable to false
            # which stops the game loop from continuing.
            self.manager.is_running = False

   def process_update(self, dt: float) -> None:
      # This is executed in our game loop.

      self.window.fill(GREEN)
      pygame.display.update()

Note

In this library screens are referred to as States and screen manager as StateManager

Now that we have created a screen, let’s add it to our screen manager and run it!

Adding our screen to the state manager.

def main() -> None:
   screen = pygame.display.set_mode((500, 600))
   # Create a basic 500x600 pixel window

   state_manager = StateManager(screen)
   state_manager.load_states(MainMenuState)
   # We pass in all the screens that we want to use in our game / app.

   state_manager.change_state("MainMenu")
   # We need to use the name we supplied in the __init_sublcass__'s `state_name`.
   # If no state_name was passed, we use the class name itself.

   clock = pygame.time.Clock()

   while state_manager.is_running:
      # The state manager has a `is_running` attribute which is `True` by default

      dt = clock.tick(60) / 1000  # The delta time from the clock for frame rate independance.

      for event in pygame.event.get():
            state_manager.current_state.process_event(event)
            # Calling the event function of the running state.

      state_manager.current_state.process_update(dt)
      # Calling the update function of the running state.

if __name__ == "__main__":
   main()

There you have it! We have set up a simple screen using the Game State library. Adding more screens is just as simple as the subclassing State & adding it to the StateManager.

Adding the main game screen to our state manager.

class MainMenuState(State, state_name="MainMenu"):
    def process_event(self, event: pygame.event.Event) -> None:
        # This is executed in our our game loop for every event.

        if event.type == pygame.QUIT:
            # We set the state manager's is_running variable to false
            # which stops the game loop from continuing.
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "Game" screen from the manager.

            self.manager.change_state("Game")

    def process_update(self, *args: float) -> None:
        # This is executed in our game loop.

        self.window.fill(GREEN)
        pygame.display.update()


class GameState(State, state_name="Game"):
    def __init__(self) -> None:
        self.player_x: float = 250.0

    def process_event(self, event: pygame.event.Event) -> None:
        if event.type == pygame.QUIT:
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "MainMenu" screen from the manager.

            self.manager.change_state("MainMenu")

    def process_update(self, *args: float) -> None:
        dt = args[0]

        self.window.fill(BLUE)

        # Player movement-
        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_a]:
            self.player_x -= speed * dt

        if pressed[pygame.K_d]:
            self.player_x += speed * dt

        pygame.draw.rect(
            self.window,
            "red",
            (
                self.player_x,
                100,
                50,
                50,
            ),
        )

        pygame.display.update()

Finally, we need to add our GameState to our StateManager just like how we did for our MainMenuState.

state_manager.load_states(MainMenuState, GameState)

There you go! We have made a simple pygame to handle multiple screens via Game State! The final code will look something like this-

import pygame
from game_state import State, StateManager

GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
speed = 200
pygame.init()
pygame.display.init()
pygame.display.set_caption("Game State Example")


class MainMenuState(State, state_name="MainMenu"):
    def process_event(self, event: pygame.event.Event) -> None:
        # This is executed in our our game loop for every event.

        if event.type == pygame.QUIT:
            # We set the state manager's is_running variable to false
            # which stops the game loop from continuing.
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "Game" screen from the manager.

            self.manager.change_state("Game")

    def process_update(self, *args: float) -> None:
        # This is executed in our game loop.

        self.window.fill(GREEN)
        pygame.display.update()


class GameState(State, state_name="Game"):
    def __init__(self) -> None:
        self.player_x: float = 250.0

    def process_event(self, event: pygame.event.Event) -> None:
        if event.type == pygame.QUIT:
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "MainMenu" screen from the manager.

            self.manager.change_state("MainMenu")

    def process_update(self, *args: float) -> None:
        dt = args[0]

        self.window.fill(BLUE)

        # Player movement-
        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_a]:
            self.player_x -= speed * dt

        if pressed[pygame.K_d]:
            self.player_x += speed * dt

        pygame.draw.rect(
            self.window,
            "red",
            (
                self.player_x,
                100,
                50,
                50,
            ),
        )

        pygame.display.update()


def main() -> None:
    screen = pygame.display.set_mode((500, 600))
    # Create a basic 500x600 pixel window

    state_manager = StateManager(screen)
    state_manager.load_states(MainMenuState, GameState)
    # We pass in all the screens that we want to use in our game / app.

    state_manager.change_state("MainMenu")
    # We need to use the name we supplied in the __init_sublcass__'s `state_name`.
    # If no state_name was passed, we use the class name itself.

    clock = pygame.time.Clock()

    assert state_manager.current_state is not None

    while state_manager.is_running:
        # The state manager has a `is_running` attribute which is `True` by default

        dt = clock.tick(60) / 1000
        # The delta time from the clock for frame rate independance.

        for event in pygame.event.get():
            state_manager.current_state.process_event(event)
            # Calling the event function of the running state.

        state_manager.current_state.process_update(dt)
        # Calling the update function of the running state.


if __name__ == "__main__":
    main()

Demo Output

Upon following this guide correctly, you will obtain an output similar to this-

Demo output video

(Click to open the video)

State Hooks

State hooks allow you to dynamically load state files without needing to import them.

Let’s take our previous example and split the MainMenuState and GameState into separate files and load the file paths instead of importing their classes.

Let’s structure our project like this-

Our project structure. With game_state_hooks being the root of the project.

game_state_hooks/
    │ - main.py
    │
    └───states/
        │ - game.py
        │ - main_menu.py

Inside our main.py file let’s initialize our pygame and dynamically load the state files.

main.py file

import pygame
from game_state import StateManager

pygame.init()
pygame.display.init()
pygame.display.set_caption("Game State Hooks Example")


def main() -> None:
    screen = pygame.display.set_mode((500, 600))
    # Create a basic 500x600 pixel window

    state_manager = StateManager(screen)
    state_manager.connect_state_hook("states.main_menu")
    state_manager.connect_state_hook("states.game")
    # Here we pass in the path to the state files.

The state_manager.connect_state_hook method is what calls the hook function in our state file to load it.

Note that the path must be dot separated like regular Python imports if accessing a sub-module. e.g. states.game if you want to import states/game.py.

We can also automate loading our state files by looping through our states/ directory.

main.py file

import pygame
from game_state import StateManager
from pathlib import Path

pygame.init()
pygame.display.init()
pygame.display.set_caption("Game State Hooks Example")


def main() -> None:
    screen = pygame.display.set_mode((500, 600))
    # Create a basic 500x600 pixel window

    state_manager = StateManager(screen)
    STATES_DIR = "states."
    path_obj = Path(STATES_DIR)

    # Use glob to find all files ending with .py
    for file_path in path_obj.glob("*.py"):
        if file_path.is_file():  # Ensure it's a file and not a directory
            state_manager.connect_state_hook(STATES_DIR + file_path)
    # With this, you don't need to manually mention every state file in that directory.
    # Given that, that directory only contains state files to be loaded.

And adding the running mechanism, the final main.py file will look like-

main.py file

import pygame
from game_state import StateManager
from pathlib import Path

pygame.init()
pygame.display.init()
pygame.display.set_caption("Game State Hooks Example")


def main() -> None:
    screen = pygame.display.set_mode((500, 600))
    # Create a basic 500x600 pixel window

    state_manager = StateManager(screen)
    STATES_DIR = "states."
    path_obj = Path(STATES_DIR)

    # Use glob to find all files ending with .py
    for file_path in path_obj.glob("*.py"):
        if file_path.is_file():  # Ensure it's a file and not a directory
            state_manager.connect_state_hook(STATES_DIR + file_path)
    # With this, you don't need to manually mention every state file in that directory.
    # Given that, that directory only contains state files to be loaded.

    state_manager.change_state("MainMenu")
    # We need to use the name we supplied in the __init_sublcass__'s `state_name`.
    # If no state_name was passed, we use the class name itself.

    clock = pygame.time.Clock()

    while state_manager.is_running:
        # The state manager has a `is_running` attribute which is `True` by default

        dt = clock.tick(60) / 1000
        # The delta time from the clock for frame rate independance.

        for event in pygame.event.get():
            state_manager.current_state.process_event(event)
            # Calling the event function of the running state.

        state_manager.current_state.process_update(dt)
        # Calling the update function of the running state.

if __name__ == "__main__":
    main()

Now we need to make a few changes to our state files for it to accept hook loading.

Let’s first take a look at our states/games.py file. It initially looks like this-

states/games.py file

import pygame
from game_state import State

BLUE = (0, 0, 255)


class GameState(State, state_name="Game"):
    def __init__(self) -> None:
        self.player_x: float = 250.0
        self.speed = 200

    def process_event(self, event: pygame.event.Event) -> None:
        if event.type == pygame.QUIT:
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "MainMenu" screen from the manager.

            self.manager.change_state("MainMenu")

    def process_update(self, *args: float) -> None:
        dt = args[0]

        self.window.fill(BLUE)

        # Player movement-
        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_a]:
            self.player_x -= self.speed * dt

        if pressed[pygame.K_d]:
            self.player_x += self.speed * dt

        pygame.draw.rect(
            self.window,
            "red",
            (
                self.player_x,
                100,
                50,
                50,
            ),
        )

        pygame.display.update()

To add the state hook functionality, all we need to do is create a hook function in the file which loads the State to the StateManager. Like this-

states/games.py file

from typing import Any

import pygame
from game_state import State

BLUE = (0, 0, 255)


class GameState(State, state_name="Game"):
    def __init__(self) -> None:
        self.player_x: float = 250.0
        self.speed = 200

    def process_event(self, event: pygame.event.Event) -> None:
        if event.type == pygame.QUIT:
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "MainMenu" screen from the manager.

            self.manager.change_state("MainMenu")

    def process_update(self, *args: float) -> None:
        dt = args[0]

        self.window.fill(BLUE)

        # Player movement-
        pressed = pygame.key.get_pressed()
        if pressed[pygame.K_a]:
            self.player_x -= self.speed * dt

        if pressed[pygame.K_d]:
            self.player_x += self.speed * dt

        pygame.draw.rect(
            self.window,
            "red",
            (
                self.player_x,
                100,
                50,
                50,
            ),
        )

        pygame.display.update()

def hook(**kwargs: Any) -> None:
    # This function should be present below the State you want to load and should call
    # the `StateManager.load_states` method while passing in the State you want to laod
    GameState.manager.load_states(GameState, **kwargs)

Doing the same for states/main_menu.py-

states/games.py file

from typing import Any

import pygame
from game_state import State

GREEN = (0, 255, 0)


class MainMenuState(State, state_name="MainMenu"):
    def process_event(self, event: pygame.event.Event) -> None:
        # This is executed in our our game loop for every event.

        if event.type == pygame.QUIT:
            # We set the state manager's is_running variable to false
            # which stops the game loop from continuing.
            self.manager.is_running = False

        if event.type == pygame.KEYDOWN and event.key == pygame.K_w:
            # Check if we're clicking the " w " button.
            # If the condition is met, we change our screen to the
            # "Game" screen from the manager.

            self.manager.change_state("Game")

    def process_update(self, *args: float) -> None:
        # This is executed in our game loop.

        self.window.fill(GREEN)
        pygame.display.update()


def hook(**kwargs: Any) -> None:
    # This function should be present below the State you want to load and should call
    # the `StateManager.load_states` method while passing in the State you want to laod
    MainMenuState.manager.load_states(MainMenuState, **kwargs)

That’s all you need to do to use state hooks!

Running your main.py file will give you the same output as Demo Output

All the code in this guide is available in the examples directory of it’s repository.