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. Additionally, we’ll keep our base state in a separate file.
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/
| - base.py
│ - game.py
│ - main_menu.py
This is what our states/base.py will look like-
main.py file
import pygame
from game_state import State
from game_state.utils import MISSING
class MyBaseState(State["MyBaseState"]):
screen: pygame.Surface = MISSING
# Mention the attributes we want all our states to share.
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
from states.base import MyBaseState
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(bound_state_type=MyBaseState, screen=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.
And adding the running mechanism, the final main.py file will look like-
main.py file
import pygame
from game_state import StateManager
from states.base import MyBaseState
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(bound_state_type=MyBaseState, screen=screen)
state_manager.connect_state_hook("states.main_menu")
state_manager.connect_state_hook("states.game")
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
from states.base import MyBaseState
BLUE = (0, 0, 255)
class GameState(MyBaseState, 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.screen.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.screen,
"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
from states.base import MyBaseState
BLUE = (0, 0, 255)
class GameState(MyBaseState, 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.screen.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.screen,
"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
from states.base import MyBaseState
GREEN = (0, 255, 0)
class MainMenuState(MyBaseState, 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.screen.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.