I mentioned earlier that I'm refactoring some game code to use a State Design Pattern, so I thought I'd write about what I've done in my blog.

The basic concept is that the software goes through various states throughout its lifetime so we'd like to put a framework into place where it makes it easy for us to add/change the behavior of the states. For example, it starts in the "Title Screen" state, then transitions to the "Main Menu" state. From there, depending on user input it can go to "Character Creation" or "View Character" or "Start Game" states or it can exit.

While in each state the high-level behaviour is the same: Continuously monitor the keyboard and mouse for input event while drawing the screen. When an event occurs (such a press a key or click a button), forward this event to a handler which takes action (some actions might be "Add a letter to a character's name" or "Increase the character's Strength attribute" or "Transition to the ViewCharacter State").

So to set up our high-level "State" base class like so:

class State
{
protected:
  virtual void handleMouse(); // get mouse input and act
  virtual void handleKeyboard(); // get keyboard input and act
  virtual void draw(); // draw screen if needed
public:
  virtual void update(); // updated every tick
  
  // functions used to transition between states
  virtual bool transitionInto(State* fromState);
  virtual void transitionOutOf(State* toState);
};

I won't get into any specifics at the moment of what the abstract State class does but thanks to the "virtual" keyword, when we make subclasses we can specify how we want the application to behave in each state (and each state could behave totally different than the other).

Speaking of the application, without getting into too many details what we have is:

class App
{
private:
  State* pCurrentState;
  State* pNextState;
public:
  void run();
};

Now inside the Application's run() function it's the following main loop:

void App::run()
{
  // transition into our starting state
  pCurrentState->transitionInto(NULL);
  
  // loop forever until we transition into the end state
  while( true )
  {
    pCurrentState->update();
    
    // now check if we need to change states
    if(pNextState)
    {
      pCurrentState->transitionOutOf(pNextState);
      if(!pNextState->transitionInto(pCurrentState)) {
        break;
      }
      pCurrentState = pNextState;
      pNextState = NULL;

    } // done state transition
  } // while
  
  pCurrentState->transitionOutOf(NULL);
}

While I've still avoided discussing just what the heck this State class is all about, let's talk a little bit about the App's run() function so we understand what happens here.

We assume before the run() function is called we have initialized the value of pCurrentState to some valid State object. When the run() function is called we inform the state that we are transitioning into it (with a "from state" of NULL) and then begin the main loop.

The main loop consists of calling the "update()" function of the state every iteration (or "tick"). This gives the state object a chance to update its logic (for instance, blink the cursor, move the enemies or cycle some colour, whatever), poll for keyboard/mouse input and then draw to the screen. We leave implementations of this as transparent for now - just assume that everything we need to do within the state is done by the update() function. This loop continues forever.

At the end of the loop we check for any queued state transitions. If our pNextState variable is non-NULL that means the application wants to transition states, so we tell the current state to transition out and then we tell the next state to transition in. If we want to end the application, the new state's transitionInto() function should return false, this will break us out of our loop. Otherwise, we reset our pCurrentState variable to point to the new current state and clear the pNextState variable.

The transitionInto() and transitionOutOf() functions allow those operations that may need to be performed once (instead of every loop). For example, if we are transitioning into the "View Character" state, we would want to set up which character we are going to view inside the View Character state's transitionInto() function. As another example, if we are transitioning from In Game and going back to the Main Menu state, we would want to save the game to disk and then delete the game from memory in the In Game state's transitionOutOf() function.

The remaining detail is how to affect state transitions. We enable this functionality by adding a simple function to the App class:

class App
{
private:
  State* pCurrentState;
  State* pNextState;
public:
  void run();
  void queueStateTransition(State* newState) { this->_pNextState = newState; }
};

Now each state will have access back to the App object so if a particular action needs to cause a state change (i.e. a button has been clicked) then the state's logic can force the state transition by calling the App's queueStateTransition() function.

I've given how the overall state machine works from the outside, I'll get into a couple concrete examples of states in the next article and how we deal with that...stay tuned. I'll even give a simple full code example that you can run...

§22 · January 23, 2005 · C++, Software, Technology · · [Print]

Comments are closed.