Python Forum
[PyGame] Creating a state machine
Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Creating a state machine
#1
A state (or screen, scene, etc.) machine, is a way to handle different scenes of the game, and flip between them. A good example of multiple states would be...splash screen, title screen, game screen, credit screen, menu screen, level screen, end of game screen, loading screen, etc. You can use them also as a pause, inventory screen, or character stats screen, or anything like the such. A state machine handles switching between any two states, even when there are more states in total. One big reason one wants a state machine is to make a menu system. This posts gives that example, but you must understand the states concept first. Each sequential post here on this thread adds a functionality to the state machine for more clarity.

There are two ways of handling states. The first way is the bad way, and the second way is a good way.
  1. [Bad] One way is creating a separate while loop for each state. Or each state having its own independent event loop. This means you have while/for loops sporadically throughout your code wherever you have a new state. Each handling logic on its own, looping events on its own, drawing on its own, etc. With this method you are splitting up your main game loop into many. And the main problem with that is you have data flowing in and out of that that needs to get to other while loops. Hence you end up with passing variables back and forth or using the global keywords (which is NOT best method). In addition to that it splits yours code for maintenance. Since you have numerous main game loops, you have numerous main game logic and drawing all over your code. Its simply hard to find, and is prone to causing bugs. This is NOT how you should be doing it. This will cause spaghetti code and make you pull your hair out later as your game gets larger as you add more and more. This is not a major issue if you have a few while loops, but imagine as a game gets larger that each component having its own while loops will just be unmaintainable at some point. There are tutorials out there that instruct people to do it this way and it leads them down a rabbit hole of chaos later for them. A very simple way to determine if someone is using this method is if they have a second line and more likely more of pygame.display.update() anywhere in their program.
  2. [Good] The other way is by using inheritance of classes. Which is exactly what will be used in this tutorial. This method will only ever have one pygame.display.update() and one while loop as a main game loop. The beneefit to this is because there is only ever one while loop as the main game loop, you can easily filter each occurrence (events, logic, drawing) to the next state. So if one state needs to pass along information to the next, you just pass it along to the next state. You also keep organization by having one loop to follow in the event of bugs, instead of many. And less code is always more maintainable. There are different variations of a state machine, but they all run under the same principle. That there is never more than one main game loop and handles switching between states. To keep it simple this tutorial will show just how to switch between 2 states, a menu state and the actual game state (where the game occurs). Both of which are blank states just showing color. More complications later in the thread. If you want to see a full fledged game that uses this method and shows more complication, then check out our public community game pyroller's states. This is where each developer makes their own state (which is a mini game) and plugs it into the game.

import pygame as pg
import sys
 
class States(object):
    def __init__(self):
        self.done = False
        self.next = None
        self.quit = False
        self.previous = None
 
class Menu(States):
    def __init__(self):
        States.__init__(self)
        self.next = 'game'
    def cleanup(self):
        print('cleaning up Menu state stuff')
    def startup(self):
        print('starting Menu state stuff')
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            print('Menu State keydown')
        elif event.type == pg.MOUSEBUTTONDOWN:
            self.done = True
    def update(self, screen, dt):
        self.draw(screen)
    def draw(self, screen):
        screen.fill((255,0,0))
 
class Game(States):
    def __init__(self):
        States.__init__(self)
        self.next = 'menu'
    def cleanup(self):
        print('cleaning up Game state stuff')
    def startup(self):
        print('starting Game state stuff')
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            print('Game State keydown')
        elif event.type == pg.MOUSEBUTTONDOWN:
            self.done = True
    def update(self, screen, dt):
        self.draw(screen)
    def draw(self, screen):
        screen.fill((0,0,255))
 
class Control:
    def __init__(self, **settings):
        self.__dict__.update(settings)
        self.done = False
        self.screen = pg.display.set_mode(self.size)
        self.clock = pg.time.Clock()
    def setup_states(self, state_dict, start_state):
        self.state_dict = state_dict
        self.state_name = start_state
        self.state = self.state_dict[self.state_name]
    def flip_state(self):
        self.state.done = False
        previous,self.state_name = self.state_name, self.state.next
        self.state.cleanup()
        self.state = self.state_dict[self.state_name]
        self.state.startup()
        self.state.previous = previous
    def update(self, dt):
        if self.state.quit:
            self.done = True
        elif self.state.done:
            self.flip_state()
        self.state.update(self.screen, dt)
    def event_loop(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.done = True
            self.state.get_event(event)
    def main_game_loop(self):
        while not self.done:
            delta_time = self.clock.tick(self.fps)/1000.0
            self.event_loop()
            self.update(delta_time)
            pg.display.update()
 
 
settings = {
    'size':(600,400),
    'fps' :60
}
 
app = Control(**settings)
state_dict = {
    'menu': Menu(),
    'game': Game()
}
app.setup_states(state_dict, 'menu')
app.main_game_loop()
pg.quit()
sys.exit()
So this is the entire code snippet. It might look daunting but if you break it down class by class, it is more easier to understand. So we have 4 classes in total. Control, Game, Menu, and States. 

States class is the super class of all states. Any data that you wish to persist between all states would go in here. And logic that persists between all states would go in here, as opposed to each sub class. This could be information such as font objects that span across the entire game, retained player health or modifications between levels, common background, etc. If you are repeating a method/function across a lot of states (even if not all), the super class is there to put it in so you dont have to do that. It is to stop redundancy. Why have a function in every level state to create the same font object and such?

Menu and Game states are sub classes of States. These are the actual states. You can have as many of these as you wish. These of themselves can even be super classes of a sub-sub class state, and so on. The possibilities are endless. The "current" state that is active has control. So what is rendered on the active state gets drawn. The same for update, and their event methods. You dont have to worry about two states drawing at the same time because only one is ever active.

Control does not have to be a class. It could in fact be in the global scope with the rest. But to keep things organized it is in a class. There will never be more than one Control object though. Control is named control because it controls the entire program. The main game loop is in it, the main update is in it, and the main event loop is in it. Control switches between the states. It simply checks if the state is completed by the current state's done attribute and switches to the next state by the next attribute (being a key in its state_dict dictionary) and switches to that state and repeats. Once you establish the control class like it is now, you should hardly ever have to modify it. You only add new states instead. Control more handles the boilerplate code for the window and states. If i was to copy this example for a game i was using, i would not even touch Control class and only modify Menu and Game classes as that is the meat of the program.


settings = {
    'size':(600,400),
    'fps' :60
}
  
app = Control(**settings)
state_dict = {
    'menu': Menu(),
    'game': Game()
}
app.setup_states(state_dict, 'menu')
app.main_game_loop()
The program starts off by creating a dictionary of settings. This gets passed in Control class. Control creates the app object. Then each state (Menu and Game) object get assigned to a dictionary. This allows Control to be able to switch to and from any state as needed. After that setup_states is called, and sets the initial state of the program. Control.state becomes the active state's object here, as Menu's object or Games object. But initially set as menu. And finally the main game loop gets called. From this point on the whole game is constantly just looping Control.main_game_loop(). This IS the main game loop, thus its name.

def main_game_loop(self):
    while not self.done:
        delta_time = self.clock.tick(self.fps)/1000.0
        self.event_loop()
        self.update(delta_time)
        pg.display.update()
In the main game loop, the if clause checks to close the program or not. Then we get the delta time. This is not used in this example, but i put it in here to show how to pass it from the main game loop to each state, where you need to use it later. Next it runs the main event loop. This loops through the event queue, checks for game quit, and runs the active state method get_event() while passing the event to it. This happens for EACH event. At this point Menu.get_event is what is being ran. This is because menu is the intial set state. And that is Control.state is Menu() object. Menu.get_event get passed each event and handles it as needed. It checks for a keydown and a mouse button down event. If the event is a mouse button down, it sets Menu.done to True. This in turn allows Control.update to pass the if clause for state.done, executing Control.flip_state on the next frame. Next the main update method gets called. This method checks for quit, and checks for if the state is done. It then execute the states update method. This would be Menu.update. After that, it runs pygame.dispaly.update() to update the screen every frame. And it just keeps looping over and over again.

def flip_state(self):
    self.state.done = False
    previous,self.state_name = self.state_name, self.state.next
    self.state.cleanup()
    self.state = self.state_dict[self.state_name]
    self.state.startup()
    self.state.previous = previous
Control.flip_state actually flips the state to the next state. Each state has a next attribute which defines the next state to switch to. flip_state runs the states cleanup method, sets this next state to the current active state, runs the new active states startup method, and sets the previous method.
Only the active state (Control.state) will run its methods. So you can keep your menu draw, menu updates, and menu event checks inside the Menu class. The same for Game...or any other state. This clumps the related code together in each states class. So if you need to modify something for your menu, it will be in Menu class. If you need to modify something for your game, it will be in your Game class. With just these two classes, it looks more work than it is worth. But when you have tons and tons of states, this makes it easy to find your code you are looking for. Its organized and structured. It also ensure you have only one event loop, and it is in your Control class. The state classes just have check events that have if clauses inside of it. Usually each state will be in a different module. And states would be grouped together in the same directory.


Because some people have trouble implementing this into their game, usage, etc. i have created more posts to illustrate how it can be used and things added to be more usable. Some of which these have been asked on the forum enough, that i have just implemented them here on appending posts.
This tutorial shows a little bit about how to modify a game to implement this structure, organizing states, and adding more states, menu states, etc.
This post shows how to add a game menu system to this state machine
This post shows game logic occuring in one state and pausing when the state is inactive.
This post implements buttons to switch the states
This post illustrates using class variables to share data between two states (classes) instead of passing information back and forth
Recommended Tutorials:
Reply


Messages In This Thread
Creating a state machine - by metulburr - Oct-06-2016, 04:55 AM
RE: Creating a state machine - by metulburr - Nov-26-2018, 10:09 PM
RE: Creating a state machine - by metulburr - Dec-10-2019, 09:55 PM
RE: Creating a state machine - by metulburr - Jan-29-2020, 08:38 PM

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020