Making the State Base Class Reusable

 As the design stands, it’s necessary to create a separate State base class for each character type to derive its states from. Instead, let’s make it reusable by turning it into a class template.

template <class entity_type>

class State

{

public:

 

  virtual void Enter(entity_type*)=0;

 

  virtual void Execute(entity_type*)=0;

 

  virtual void Exit(entity_type*)=0;

 

  virtual ~State(){}

};

 

The declaration for a concrete state — using the EnterMineAndDigForNugget miner state as an example — now looks like this:

class EnterMineAndDigForNugget : public State<Miner>

{

 

public:

 

  /* OMITTED */

};

 This, as you will see shortly, makes life easier in the long run.


 

Global States and State Blips

 More often than not, when designing finite state machines you will end up with code that is duplicated in every state. For example, in the popular game The Sims by Maxis, a Sim may feel the urge of nature come upon it and have to visit the bathroom to relieve itself. This urge may occur in any state the Sim may be in and at any time. Given the current design, to bestow the gold miner with this type of behavior, duplicate conditional logic would have to be added to every one of his states, or alternatively, placed into the Miner::Update function. While the latter solution is accept- able, it’s better to create a global state that is called every time the FSM is updated. That way, all the logic for the FSM is contained within the states and not in the agent class that owns the FSM.

To implement a global state, an additional member variable is required:

//notice  how  now  that  State  is  a  class  template  we  have  to  declare  the  entity  type

State<Miner>*  m_pGlobalState;

 In addition to global behavior, occasionally it will be convenient for an agent to enter a state with the condition that when the state is exited, the agent returns to its previous state. I call this behavior a state blip. For example, just as in The Sims, you may insist that your agent can visit the bathroom at any time, yet make sure it always returns to its prior state. To give an FSM this type of functionality it must keep a record of the previous state so the state blip can revert to it. This is easy to do as all that is required is another member variable and some additional logic in the Miner::ChangeState method.

By now though, to implement these additions, the Miner class has acquired two extra member variables and one additional method. It has ended up looking something like this (extraneous detail omitted):

class Miner : public BaseGameEntity

{

private:

 

  State<Miner>*   m_pCurrentState;

  State<Miner>*   m_pPreviousState;

  State<Miner>*   m_pGlobalState;

  ...

 

public:

 

  void ChangeState(State<Miner>* pNewState);

  void RevertToPreviousState();

  ...

};

 Hmm, looks like it’s time to tidy up a little.

 

Creating a State Machine Class

 The design can be made a lot cleaner by encapsulating all the state related data and methods into a state machine class. This way an agent can own an instance of a state machine and delegate the management of current states, global states, and previous states to it.

With this in mind take a look at the following StateMachine class template.

template <class entity_type>

class StateMachine

{

private:

 

  //a pointer to the agent that owns this instance

  entity_type*          m_pOwner;

 

  State<entity_type>*   m_pCurrentState;

 

  //a record of the last state the agent was in

  State<entity_type>*   m_pPreviousState;

 

  //this state logic is called every time the FSM is updated

  State<entity_type>*   m_pGlobalState;

 

public:

 

  StateMachine(entity_type* owner):m_pOwner(owner),

                                   m_pCurrentState(NULL),

                                   m_pPreviousState(NULL),

                                   m_pGlobalState(NULL)

  {}

 

  //use these methods to initialize the FSM

  void SetCurrentState(State<entity_type>* s){m_pCurrentState = s;}

  void SetGlobalState(State<entity_type>* s) {m_pGlobalState = s;}

  void SetPreviousState(State<entity_type>* s){m_pPreviousState = s;}

 

  //call this to update the FSM

  void  Update()const

  {

    //if a global state exists, call its execute method

    if (m_pGlobalState)   m_pGlobalState->Execute(m_pOwner);

 

    //same for the current state

    if (m_pCurrentState) m_pCurrentState->Execute(m_pOwner);

  }

 

  //change to a new state

  void  ChangeState(State<entity_type>* pNewState)

  {

    assert(pNewState &&

           "<StateMachine::ChangeState>: trying to change to a null state");

 

    //keep a record of the previous state

    m_pPreviousState = m_pCurrentState;

 

    //call the exit method of the existing state

    m_pCurrentState->Exit(m_pOwner);

 

    //change state to the new state

    m_pCurrentState = pNewState;

 

    //call the entry method of the new state

    m_pCurrentState->Enter(m_pOwner);

  }

 

  //change state back to the previous state

  void  RevertToPreviousState()

  {

    ChangeState(m_pPreviousState);

  }

 

  //accessors

  State<entity_type>*  CurrentState()  const{return m_pCurrentState;}

  State<entity_type>*  GlobalState()   const{return m_pGlobalState;}

  State<entity_type>*  PreviousState() const{return m_pPreviousState;}

 

  //returns true if the current state’s type is equal to the type of the

  //class passed as a parameter.

  bool  isInState(const State<entity_type>& st)const;

};

 Now all an agent has to do is to own an instance of a StateMachine and implement a method to update the state machine to get full FSM functionality.

 

The improved Miner class now looks like this:

class Miner : public BaseGameEntity

{

private:

 

  //an instance of the state machine class

  StateMachine<Miner>*  m_pStateMachine;

 

  /* EXTRANEOUS DETAIL OMITTED */

 

public:

 

  Miner(int id):m_Location(shack),

                m_iGoldCarried(0),

                m_iMoneyInBank(0),

                m_iThirst(0),

                m_iFatigue(0),

                BaseGameEntity(id)

                              

  {

    //set up state machine

    m_pStateMachine = new StateMachine<Miner>(this);

   

    m_pStateMachine->SetCurrentState(GoHomeAndSleepTilRested::Instance());

    m_pStateMachine->SetGlobalState(MinerGlobalState::Instance());

  }

 

  ~Miner(){delete m_pStateMachine;}

 

  void Update()

  {

    ++m_iThirst;

    m_pStateMachine->Update();

  }

 

  StateMachine<Miner>* GetFSM()const{return m_pStateMachine;}

 

  /* EXTRANEOUS DETAIL OMITTED */

};

 Notice how the current and global states must be set explicitly when a StateMachine is instantiated. The class hierarchy is now like that shown in Figure 2.4.

 

Figure 2.4. The updated design

 

Introducing Elsa

 To demonstrate these improvements, I’ve created another project: WestWorldWithWoman. In this project, West World has gained another inhabitant, Elsa, the gold miner’s wife. Elsa doesn’t do much; she’s mainly preoccupied with cleaning the shack and emptying her bladder (she drinks way too much cawfee). The state transition diagram for Elsa is shown in Figure 2.5.

 

Figure 2.5. Elsa’s state transition diagram. The global state is not shown in the figure because its logic is effectively implemented in any state and never changed.

 

When you boot up the project into your IDE, notice how the VisitBathroom state is implemented as a blip state (i.e., it always reverts back to the previous state). Also note that a global state has been defined, WifesGlobalState, which contains the logic required for Elsa’s bathroom visits. This logic is contained in a global state because Elsa may feel the call of nature during any state and at any time.

Here is a sample of the output from WestWorldWithWoman.

Miner  Bob:  Pickin'  up  a  nugget

Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold

Miner  Bob:  Goin'  to  the  bank.  Yes  siree

Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose

Elsa:  Ahhhhhh!  Sweet  relief!

 Elsa:  Leavin'  the  john

Miner  Bob:  Depositin'  gold.  Total  savings  now:  4

Miner  Bob:  Leavin'  the  bank

Miner  Bob:  Walkin'  to  the  gold  mine

Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose

Elsa:  Ahhhhhh!  Sweet  relief!

Elsa:  Leavin'  the  john

Miner  Bob:  Pickin'  up  a  nugget

Elsa:  Moppin'  the  floor

Miner  Bob:  Pickin'  up  a  nugget

Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold

Miner  Bob:  Boy,  ah  sure  is  thusty!  Walkin'  to  the  saloon

Elsa:  Moppin'  the  floor

Miner  Bob:  That's  mighty  fine  sippin'  liquor

Miner  Bob:  Leavin'  the  saloon,  feelin'  good

Miner  Bob:  Walkin'  to  the  gold  mine

Elsa:  Makin'  the  bed

Miner  Bob:  Pickin'  up  a  nugget

Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold

Miner  Bob:  Goin'  to  the  bank.  Yes  siree

Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose

Elsa:  Ahhhhhh!  Sweet  relief!

Elsa:  Leavin'  the  john

Miner  Bob:  Depositin'  gold.  Total  savings  now:  5

Miner  Bob:  Woohoo!  Rich  enough  for  now.  Back  home  to  mah  li'l  lady

Miner  Bob:  Leavin'  the  bank

Miner  Bob:  Walkin'  home

Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose

Elsa:  Ahhhhhh!  Sweet  relief!

Elsa:  Leavin'  the  john

Miner  Bob:  ZZZZ...

 

Well, that's it folks. The complexity of the behavior you can create with finite state machines is only limited by your imagination. You don’t have to restrict your game agents to just one finite state machine either. Sometimes it may be a good idea to use two FSMs working in parallel: one to control a character’s movement and one to control the weapon selection, aiming, and firing, for example. It’s even possible to have a state itself contain a state machine. This is known as a hierarchical state machine. For instance, your game agent may have the states Explore, Combat, and Patrol. In turn, the Combat state may own a state machine that manages the states required for combat such as Dodge, ChaseEnemy, and Shoot.

 

 If you'd like to ask questions or give me some feedback about this article (good or bad, it all helps) then please visit the forum.


1   2   3   Home