In order to simplify our code, we’ll implement a state-based model for the game. This will allow us to represent each significant ‘mode’ of the game as a separate, encapsulated item, thus keeping code for differing parts of the program isolated from others. This will also have the benefit of allowing us to extend the program at a later time to add more modes and functionality without impacting the existing codebase any more than is necessary.
Granted, the simplicity of the Pong game that we’re working on now, doesn’t necessarily require a full-blown state model, but this is a useful, reusable pattern that can be easily adapted for future developments, so … why not?
Let’s first explore what makes a state.
In the case of our basic Pong game, we’ll have three states. These being:
- The main menu
- The game itself
- The pause state
Each of the above states will inherit their basic pattern from a generic state class. This class will provide placeholder methods for updating the gamestate and drawing the current frame, which will give our main game object the ability to simply implement the main game loop without having to worry about which state it is currently referring to.
Each state will also need to have the ability to perform any preparatory work (both on initial load, and also when it becomes ‘live’) along with any cleanup prior to exiting. We’ll also separate out the event processing loop from the update loop, just to keep things clean.
Hence, our Event() class will look something like this:
class State:
def __init__(self):
"""Perform any on-load initialisation of the state. e.g. preload
resources, set persistent buffer lengths, etc. """
def handle_events(self, game):
"""Run the 'event pump' and handle any events that arise."""
def update(self, game):
"""Update the simulation"""
def display(self, game):
"""Draw the current frame"""
def startup(self, game):
"""Perform any state specific initialisation each time the state
becomes current (e.g. resetting scores, setting player positions,
etc."""
def cleanup(self, game):
"""Perform any cleanup of resources once the state is no longer
current (e.g. clearing buffers, deallocating resources, etc."""So far in here, there’s not a whole lot which we haven’t actually seen before. One thing you will notice however is that each method within the State class takes a reference to the actual Game object it is being called from. This allows us to reference any instance variables stored within the game object, or to call any of its methods as required.
The startup() and cleanup() methods will be automatically called by the StateManager object which we’ll take a look at next.
The State Manager
States are nothing without something to control them. Hence, we need to also implement a State Manager class. This class will have responsibility for the initial loading of the state scripts into memory, and also for managing which state is currently executing. For this, we’ll use a Python list as a rudimentary stack, with the currently executing state being the last item in the list. If the list ever becomes empty, then the game is done and we can terminate the program.
The state manager itself will sit as part of the main game object, hence individual states will be able to call it’s functionality via the game object reference that is passed into them.
In order that we don’t need to modify the StateManager for each game we develop, the actual set of states to load will be defined externally, and passed into the manager when the game first initialises, from here, the State manager will load and initialise the library of states that makes up the game itself. It will also ensure that the first defined state is automatically loaded into the stack so it is ready to run from the outset.
Individual states will then call the State Manager’s push() and pop() methods to either transition into a new state, or to signal that they’re done and should be terminated.
Hence our state manager has the following outline:
class StateManager:
def __init__(self, states, game):
"""Initialises the StateManager object and checks and loads the
initial set of states into the system. Ensures that the default
state is pushed onto the state stack and sets the current_state
variable to point to it so that the program can begin execution"""
# Store a reference to the enclosing 'game' object. This allows us to
# terminate the program execution should the state stack become empty.
self._game = game
# Load the states (needs refinement)
# States is stored as a dictionary so we can refer to states by name
self.states = {}
# State_stack is a list which contains the set of currently executing
# states. The state at the end of the list is the current active state
self.state_stack = []
self.current_state = None
# Bootstrap the state manager by adding the first state in 'states'
# to the state_stack
self.push("""Initial State""") # TODO: Update this once we know what it looks like
def push(self, state):
"""Adds the specified state to the end of the state_stack list and
updates the current_state instance variable. Also causes the new
state to perform any required setup tasks before it kicks in"""
self.state_stack.append(state)
self.current_state = self.state_stack[-1]
self.current_state.startup(self._game)
def pop(self):
"""Removes the currently executing state from the stack and, if there
are any remaining states, updates the current_state instance variable
to point back to it. If there are no remaining states, then exits
the game"""
# Perform any necessary cleanup functionality before terminating the
# state
self.current_state.cleanup(self._game)
self.state_stack.pop()
# Terminate the game if we've popped the last item from the stack...
if len(self.state_stack) == 0:
self._game.is_running = False
else:
self.current_state = self.state_stack[-1]
As you can see, as an object it has an extremely simple interface, and contains all the required functionality to load and run the game. We’ll need to go back and make some modifications to the existing game loop so that the State Manager forms part of the Game object and is initialised as part of game startup. We’ll also need to ensure that our event handler, update and draw method calls are also updated to reference the current state.
This, however, forms the basis of a workable game engine. I still need to determine how I’m going to define the library of game states to pass into the State Manager, and how this will be initialised – some form of dynamic module loading will be required, which is getting into some Python that’s far more advanced than I’ve currently investigated in detail, so that’s my next focus, and when that’s done I’ll hopefully have a managed-state version of the sample program I whipped up in a few minutes previously. Sounds like I’ve taken several steps backwards in introducing the State Management functionality, but in the long run, this will vastly simplify the entire resulting game.
Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better.
Edsger Wybe Dijkstra