Pygame State Machine

What is a state machine

A state machine handles which state is active. But my pygame state machine handles a little more. Basic pygame setup and extension that you might want to add.

State Interface

class State:
    # Convenient way to setup state machine. Set state machine variable.
    @classmethod
    def machine_setup(cls, caption, width, height, center=False, position=None):
        if center:
            Machine.screen_center()

        if position:
            Machine.screen_position(*position)

        cls.machine = Machine(caption, width, height)

    # A convenient way to customize state
    def machine_draw(self, surface):
        self.on_draw(surface)

    def machine_event(self, event):
        self.on_event(event)

    def machine_update(self, delta):
        self.on_update(delta)

    def machine_drop(self):
        self.on_drop()

    def machine_focus(self, *args, **kwargs):
        self.on_focus(*args, **kwargs)

    def machine_new(self, *args, **kwargs):
        self.on_new(*args, **kwargs)

    # State Methods
    # Draw code.
    def on_draw(self, surface):
        surface.fill(pygame.Color('gray40'))

    # Clean up before state phases out.
    def on_drop(self):
        pass

    # Event code.
    def on_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                State.machine.running = False
        elif event.type == pygame.QUIT:
            State.machine.running = False

    # State just got control.
    def on_focus(self, *args, **kwargs):
        pass

    # State get control and refreshes state.
    def on_new(self, *args, **kwargs):
        pass

    # Logic code.
    def on_update(self, delta):
        pass

State interface with state machine. I separated drawing from logic. I did this because when in menu state or pause state. I like drawing game state as background. I have some convenient code already here. I always hated when I forgot to put in an exit.

Custom State Example

# Key state interface
class KeyState(State):
    def machine_update(self, delta):
        keys = pygame.key.get_pressed()
        self.on_update(self, delta)
        self.on_keys(self, keys, delta)

    def on_keys(self, keys, delta):
        pass

State Machine

class Machine:
    def __init__(self, caption, width, height):
        # Basic pygame setup
        pygame.display.set_caption(caption)
        self.surface = pygame.display.set_mode((width, height))
        self.rect = self.surface.get_rect()
        self.clock = pygame.time.Clock()
        self.running = False
        self.delta = 0
        self.fps = 30

        # State
        self._state = State()
        self.store = {}

        # Add extension callbacks
        self.extension = []

    # Store live states
    def add_state(self, state, name=None):
        if name is None:
            name = state.__class__.__name__

        self.store[name] = state

    def _flip(self, state_name, new=False, *args, **kwargs):
        self._state.machine_drop()
        # State already init.
        if isinstance(state_name, State):
            self._state = state_name
        # Grab live state from store.
        else:
            self._state = self.store[state_name]

        if new:
            self._state.machine_new(*args, **kwargs)
        else:
            self._state.machine_focus(*args, **kwargs)

    # Change state
    def flip(self, state_name, *args, **kwargs):
        self._flip(state_name, False, *args, **kwargs)

    # Change state call on_new
    def flip_new(self, state_name, *args, **kwargs):
        self._flip(state_name, True, *args, **kwargs)

    def mainloop(self):
        self.running = True
        while self.running:
            for event in pygame.event.get():
                self._state.machine_event(event)

            self._state.machine_update(self.delta)
            self._state.machine_draw(self.surface)
            for extension in self.extension:
                extension(self)

            pygame.display.flip()
            self.delta = self.clock.tick(self.fps)

    # Must happen before state machine initialize.
    @staticmethod
    def screen_center():
        os.environ['SDL_VIDEO_CENTERED'] = '1'

    # Must happen before state machine initialize.
    @staticmethod
    def screen_position(x, y):
        os.environ['SDL_VIDEO_WINDOW_POS'] = '{0}, {1}'.format(x, y)

My state machine flip states and has some useful data. Delta how much time between frames. Fps to control frame rates. Convenient methods for center screen or position screen. It holds live states. What a live state. When flip to another state. That state holds it data. So if you flip back to state. It right where you left off.

Example

import pygame
from statemachine import State

class Intro(State):
    def __init__(self):
        font = pygame.font.Font(None, 32)
        self.text = font.render("Intro", 1, pygame.Color('darkgreen'))
        self.rect = self.text.get_rect(centerx=State.machine.rect.centerx, y=10)

    def on_draw(self, surface):
        surface.fill(pygame.Color('lawngreen'))
        surface.blit(self.text, self.rect)

    def on_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                State.machine.running = False
            else:
                State.machine.flip('Game')
        elif event.type == pygame.QUIT:
            State.machine.running = False

class Game(State):
    def __init__(self):
        font = pygame.font.Font(None, 32)
        self.text = font.render("Game", 1, pygame.Color('navy'))
        self.rect = self.text.get_rect(centerx=State.machine.rect.centerx, y=10)

    def on_draw(self, surface):
        surface.fill(pygame.Color('dodgerblue'))
        surface.blit(self.text, self.rect)

    def on_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                State.machine.running = False
            elif event.key == pygame.K_SPACE:
                State.machine.flip('Pause')
        elif event.type == pygame.QUIT:
            State.machine.running = False

class Pause(State):
    def __init__(self):
        font = pygame.font.Font(None, 32)
        self.text = font.render("Pause", 1, pygame.Color('darkred'))
        self.rect = self.text.get_rect(center=State.machine.rect.center)

    def on_draw(self, surface):
        surface.fill(pygame.Color('firebrick'))
        surface.blit(self.text, self.rect)

    def on_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                State.machine.running = False
            else:
                State.machine.flip('Game')
        elif event.type == pygame.QUIT:
            State.machine.running = False

if __name__ == '__main__':
    pygame.init()
    State.machine_setup("State Example", 800, 600, True)
    State.machine.add_state(Game())
    State.machine.add_state(Pause())
    State.machine.flip(Intro())
    State.machine.mainloop()