In this tutorial I while introduce how to use state machines to keep a program always in a well defined state.
A state machine has mainly two parts:
By using a state machine as the basis of a program, the program is in just one state at any given time. The State can only be changed by a transition.
So the state defines the complete behaviour of the program at this point of time and the programmer can avoid by this side effects and his program is always in a well defined state.
To change the state either the user (via the gui) or the program itself triggers a transition. To enforce to reach only a well defined state, transitions can be guarded, so that not all transitions are possible.
This for example allows only entrance to a given state, if all preconditions (like initiliazing of program resources) are met. The guard functions are also needed as soon as you have a multi-threaded program to garantuee exclusive access to a critical sections.
To summarize a state machine offers a program:
So let's start by defining a BaseState class from which we can derive our concrete program states.
A state in this case is just identified by a number (stateId_). so we need first a simple constructor:
class BaseState
{
public:
/* @param stateValue identifier of this state */
BaseState( int stateValue ): stateId_( stateValue ) {};
|
And a function to read this identifier:
int getStateId() const { return stateId_; };
private:
const int stateId_;
};
|
Finally we need our guard functions. In this case one check if we are allowed to enter this state from a given other state and one to check if we can leave this state for an other state:
public:
/// @result if the state could be entered
virtual bool onEnter( int oldState ){ return true; };
/// @result if the state could be left
virtual bool onLeave( int newState ){ return true; };
|
So now let us take a look at the state machine itself:
BaseStateMachine::BaseStateMachine( BaseState* initState )
: stateMap_(), actState_( initState )
{
// assert state defined
assert( actState_ );
registerState( initState );
// enter initial state
assert( initState->onEnter( initState->getStateId() ) );
} |
void
BaseStateMachine::registerState( BaseState* st )
{
stateMap_[st->getStateId()] = st;
}
|
bool
BaseStateMachine::changeState( int newState )
{
std::map |
void
BaseStateMachine::releaseStates()
{
for( std::map |
But now one question is still open: How takes a concrete state effect in the program?
For this I will show here a concrete instance of a state machine developed for a game engine based on Direct3D.
To get events to a state we define a new GraphicState derived from BaseState with:
/// State with special functions, which will be called from GraphicStateMachine on special events.
class GraphicState
: public BaseState
{
public:
GraphicState( int stateId, GraphicEngine* graphEng ): BaseState( stateId ), graphEng_( graphEng ) {};
virtual ~GraphicState(){};
/// Method to propagate drawing events from graphicengine
virtual void onFrameRender( double fTime, float fElapsedTime ) = 0;
/// Method to propagate msgEvents from graphicengine
virtual bool msgProc( GuiEvent::Event ge ) = 0;
virtual void keyboardProc( KeyEvent::Event ke ) = 0;
}; |
In the same way we derive from BaseStateMachine a GraphicStateMachine with just the same functions as GraphicState and which calls the functions of the current state:
// handles message events, and calls the corresponding function of actState_
bool
GraphicStateMachine::msgProc( GuiEvent::Event ge )
{
return static_cast<GraphicState*>(actState_)->msgProc( ge );
}
void
GraphicStateMachine::keyboardProc( KeyEvent::Event ke )
{
static_cast<GraphicState*>(actState_)->keyboardProc( ke );
}
// renders the actual display, via onFrameRender of actState_
void
GraphicStateMachine::onFrameRender( double fTime, float fElapsedTime )
{
static_cast<GraphicState*>(actState_)->onFrameRender( fTime, fElapsedTime );
} |
By this we call only the functions from the state machine itself, when events occur and our interface to Direct3D does not need to know, in which state we are at the time of the event.
So finally I will introduce in a short excurse one way to easily register states:
Just assign an enum for your state ids and implement a class with one or more factory functions for states:
/// Class for generating States for this game
class StateFactory
{
public:
/*
** returns a new State, for registering with the StateMachine of this game
**
** @param stateId State to create
** @param graphEng pointer for GraphicEngine, if needed by state to create
** @param confPars pointer to ConfigParser, if needed by state to create.
*/
static BaseState* New( int stateId, GraphicEngine* graphEng = NULL )
{
switch( stateId )
{
case GameStates::mainMenu:
return new MainMenuState( GameStates::mainMenu, graphEng );
break;
case GameStates::simpleIntro:
return new SimpleIntroState( GameStates::simpleIntro, graphEng );
break;
}
int unkownStateForStateFactory = 0;
assert( unkownStateForStateFactory );
return NULL;
}
}; |
The introduction of unknownStateForStateFactory is just one more point to garantuee that our program is always in a well defined state, as we now get an assertion as soon as we want to create a state, which is not yet implemented in the StateFactory. As all states should be normally registered at the start of the program, this asserts that we do not forget any state.
Then you can easily use this functon to register needed states to your state machine:
void
GameEngineImpl::registerState( int stateId )
{
assert( stateMachine_ );
stateMachine_->registerState( StateFactory::New( stateId, graphEng_, &confPars_ ) );
} |
Often it is not sufficient to have just one state machine. So I will shortly show how you can embbed a state machine in a state of an other state machine.
As shown on the screen above there can be more than one level of embedding. The only limitation to this technique is that with each level your code gets a little bit harder to understand.