Read Part One for a refresher first. We left that article with the hazy outline of how we can implement a state machine from an application's perspective by deferring state transitions and behavior to State subclass objects (which we still need to write) and have these objects swapped in/out depending on our requirements. We now get into some more detail on this and show a full (yet trivial) example.
As a reminder, we have a State base class that will provide some common functionality that all states should have and we have an Application class that maintains a pointer to the current state. The App's run() method is called and this starts the main loop in which the current state's update() method is continuously called. Every loop we also listen and perform state transitions as required.
The ability to transition from one state to another is actually handled by the current state calling the App's queueStateTransition() function, so we need to give all State objects the ability access the App object. We do this in State's constructor. Here is our updated State.h:
class State
{
protected:
App* pApp;
virtual void handleMouse(); // get mouse input and act
virtual void handleKeyboard(); // get keyboard input and act
virtual void draw(); // draw screen if needed
public:
State(App* _app) : pApp(_app) { }
virtual void update(); // updated every tick
// functions used to transition between states
virtual bool transitionInto(State* fromState);
virtual void transitionOutOf(State* toState);
};
Our State base class has only three public functions: update(), transitionInto() and transitionOutOf() and we now show the implementation of those virtual functions here:
void State::update()
{
updateKeyboard();
updateMouse();
draw();
}
void State::transitionInto(State* fromState) { }
void State::transitionOutOf(State* toState) { }
As you can see, our transition functions are empty as these functions will be specific to each subclass' needs. There is no functionality that needs to be performed at the base class level (though if we wanted to log all state transitions we could certainly add this to the base State class). For our update() function, we defer to protected member functions updateKeyboard(), updateMouse() and draw().
The protected draw() method is also an empty function at the base State level as each subclass will have their own requirements for drawing to the screen. However, there is an opportunity to commonize polling the keyboard and mouse input at the State level and forwarding these input events to specific handlers. Because the implementation of these two functions are very platform-specific, I will leave them transparent for now.
Based on the above, we now have everything we need to start implementing specific State objects except one: How does the application and State objects know and identify the state subclasses? As an example of this problem, consider a simple example:
Our application starts with a Title Screen then waits utnil the user presses a key before transitioning into the Main Menu. Candidates for states are: TitleScreen and MainMenu. So we create two subclasses:
class TitleScreen : public State { ... }
class MainMenu : public State { ... }
Now that we've created the classes, we need to instantiate objects of the above types. We want our App to start in the TitleScreen object. When the user presses a key, we want the TitleScreen state to initiate a transition to the MainMenu state. The questions are: a) How does the App object acquire a pointer to the TitleScreen object? and b) How does the TitleScreen object acquire a pointer to the MainMenu state?
There are several possible solutions for this problem:
For simplicity's sake we will choose the first option of just including all the subclasses into the App class and letting the App class be the "State Manager". However, use of the Proxy Design Pattern would probably be wisest if you have a lot of states.
Let's put in that infrastructure using the example of above of TitleScreen and MainMenu states:
We prepend State.h with an enumeration:
enum EStateType {
State_TitleScreen = 0,
State_MainMenu // 1
NUMBER_OF_STATES
};
Sidenote: The NUMBER_OF_STATES identifier is a nice little enum trick so we always know the total number of states available.
And we add a EStateType member to the State class and constructor:
class State {
protected:
EStateType id; // newly added
App* pApp;
// ...
public:
State(App* _app, EStateType _id) : pApp(_app), id(_id) { }
// ...
And now we flesh out our state subclasses. Below we show how to construct the TitleScreen class...
#include "State.h";
class TitleScreen : public State
{
public:
TitleScreen(App* pApp) : State(pApp, State_TitleScreen) { ... }
};
We do a similar thing in MainMenu...
And now inside App we add a collection of State objects and initialize things in the App constructor:
#include <map>
#include "State.h"
#include "TitleScreen.h"
#include "MainMenu.h"
class App
{
std::map<EStateType, State*> stateMap;
public:
App()
{
stateMap[State_TitleScreen] = new TitleScreen(this);
stateMap[State_MainMenu] = new MainMenu(this);
pCurrentState = stateMap[State_TitleScreen];
}
State* getState(EStateType type) { return stateMap[type]; }
};
Of course in App's destructor we will make sure and delete all those State objects so we don't cause a memory leak, right? Right. Use your imagination (and your in-depth knowledge of the std::map template) for this...
Note that we don't really need to keep track of the enumeration values because that is each subclass' responsibility (as shown above in the StateTitleScreen constructor). Also note that we provided an accessor function in the App class called getState() that will allow anyone to get a pointer to a specific State object.
Ok, now that we've fleshed out in total how the state machine will work, we're ready to put all the pieces together with a full code example. As stated above, since we don't want to get into any platform-specific dependencies at this stage, the example will have to be really trivial but hopefully it will properly show how everything fits.
Let's say our application has the following requirements: Print out "Hello, world!" ten times and then print out "Goodbye cruel world" once. Let's also say that instead of a 4-line C++ program we want to use the above State Design Pattern and we choose our states this way:
I decided I'd put the code on its own page because I haven't figured out a good way to embed code in a blog entry (I have to manually format indentations and special characters like < and >). The following four files show the code to implement the simple state machine above.
State.h : defines the base State class and its two subclasses (HelloWorld and GoodbyeWorld)
State.cpp: shows implementation of HelloWorld and GoodbyeWorld
App.h: defines the App class
App.cpp: shows implementation of App class and includes the main() function
Of course the trivial nature of the example (partly attributable to the fact that we haven't supplied any implementation for keyboard or mouse handling) highlights the fact that the State Design Pattern should really only be used when there are a larger number of states with a variety of transitions that need to be properly separated. The above application could have been done in just a few lines of C/C++ code. However, I hope that the usefulness of the State Design Pattern is not lost completely...If someone has a better suggestion for an example (that doesn't involve mouse/keyboard input) please let me know.
This really concludes the section on the State Design Pattern. In subsequent articles I'll be focusing on an SDL implementation of mouse/keyboard binding using the above State Pattern and getting into designing a user interface with simple buttons.