delta3d

StateManager Tutorial

Tutorials

This example will show the basic usage of the StateManage class. The StateManager class is a finite state machine which controls logic flow by defining what transitions are possible. A transition is defined as the map of a State-Event pair to another State.




Note : Code for this example can be found at "delta3d/examples/testStateManager"

So, you want to define the behavior for something? Well, dtABC::StateManager can help you. It is useful for many objects requiring logical definitions, not only applications. For now, I will be presenting how to use the StateManager to control your application.

The StateManager has 2 major roles.
  • To hold a set of States
  • To manage a map of Transitions

A 'State' is actually a dtABC::State which is a very lightweight class that is meant to be derived from, and it owns and a 'Type'. The 'Type' is an instance of the dtUtil::Enumeration class, and comes in very handy when we want to instantiate a concrete State via XML.

A 'Transition' is actually defined as the unique mapping from the pair of State and dtABC::Event::Type to a State. Event::Type is very similar to the State::Type mentioned above because it is also just an instance of the dtUtil::Enumeration class. We use the Event::Type rather than an actual Event, because we like to think of an Event as something that can be used and thrown away. The Event::Type is also useful for instantiating concrete Events via XML.

To summarize, a Transition looks like this:
std::pair<Event::Type*,State*> --> State*

Now that we have some definitions, let's talk about the problem we want to solve.

We want to define application behavior. dtABC::StateManager can help you do this by maintaining a current State for your application. State changes occur when an Event, whose type causes a transition, has been sent to the StateManager. The StateManager eventually assigns a State to be the "current" State. A State is made to be the "current" State if a Transtion has been satisfied. The StateManager uses the current State, or "from" State, and the passed Event::Type pairing as the key for finding a "to" State from its map of Transitions. Lost yet? Don't feel confused. Each of these transitions are plainly defined like:

	<Transition>
		<Event Type="LOAD_DATA"/>
		<FromState Type="START" Name="start"/>
		<ToState Type="LOADING_DATA" Name="loader"/>
	</Transition>

Again, the pair formed by the "Event" and "FromState" point the StateManager to a different State, known as the "ToState". Can you see it? A game's logical behavior can be defined by forming a XML file with many Transitions specified. We already have one Transition specified. Later I'll show you how to add more.

Putting this to use in code is pretty simple. The general steps a developer will need to do when using the StateManager are:

  • Define some States.
  • Define some Events.
  • Allocate a StateManager
  • Logically add combinations of these Events and States to the StateManager's container of Transitions.
  • Write a class which will send Events to the StateManager, possibly causing Transitions to occur.
XML can help a lot when performing the step about adding transitions. To be able to add transitions to your StateManager via a XML description, the StateManager needs you to do a little more work. This basically means providing a way to help the XML parser know which concrete State or Event class to instantiate from the XML description. This is done by creating instances of State::Types and Event::Types, and registering the correct class with each type. So, let me revise my list of steps:
  • Define some States.
  • Define corresponding State::Types
  • Define some Events.
  • Define corresponding Event::Types
  • Allocate a StateManager
  • Register the State::Types with the StateManager, thus binding the State class to its Type
  • Register the Event::Types with the StateManager, thus binding the Event class to its Type
  • Logically add combinations of these Events and States to the StateManager's container of Transitions.
  • Write a class which will send Events to the StateManager, possibly causing Transitions to occur.

Look at how I register my States and Events:


// states
#include "States/Types.h"
#include "States/Start.h"
#include "States/LoadingData.h"
#include "States/SelectingPlayer.h"
#include "States/Playing.h"

// events
#include "Events/Types.h"
#include "Events/LoadData.h"
#include "Events/SelectPlayer.h"
#include "Events/Play.h"

// state manager
#include <dtABC/StateManager.h>

// Registers concrete States
void RegisterKnownStates(dtABC::StateManager* gm)
{
   gm->RegisterState<Game::Start>( &Game::StateTypes::START );
   gm->RegisterState<Game::LoadingData>( &Game::StateTypes::LOADING_DATA );
   gm->RegisterState<Game::SelectingPlayer>( &Game::StateTypes::SELECTING_PLAYER );
   gm->RegisterState<Game::Playing>( &Game::StateTypes::PLAYING );
}

// Registers concrete Events
void RegisterKnownEvents(dtABC::StateManager* gm)
{
   gm->RegisterEvent<Game::LoadData>( &Game::EventTypes::LOAD_DATA );
   gm->RegisterEvent<Game::SelectPlayer>( &Game::EventTypes::SELECT_PLAYER );
   gm->RegisterEvent<Game::Play>( &Game::EventTypes::PLAY );
}

void SomeFunction()
{
   // make a StateManager
   dtABC::StateManager* gm = new dtABC::StateManager();

   // register concrete States
   RegisterKnownStates(gm);

   // register concrete Events
   RegisterKnownEvents(gm);
}
What do you think about that? I've omitted the code involved for defining each State, Event, and corresponding Types. I think this is a good idea to only show the registration process at this point. I think you can see the basic steps at work here. The header files included define States, State::Types, Events, and Event::Types. Functions to "register" Events and States are also shown. In fact, the only steps that are not complete are:
  • Logically add combinations of these Events and States to the StateManager's container of Transitions.
  • Write a class which will send Events to the StateManager, possibly causing Transitions to occur.
Both of these steps are quite easy. Soon, I'll have to post examples of the semantics for defining Events, States, and their Types, but I hope you already have a feel for the bulk of the work involved when using a StateManager.

If you want to logically add transitions from one state to another, I highly recommend using an XML description. If you build this XML file:


<?xml version="1.0"?>
<TransitionList>
	<Transition>
		<Event Type="PLAY"/>
		<FromState Type="SELECTING_PLAYER" Name="selecting"/>
		<ToState Type="PLAYING" Name="gameplay"/>
	</Transition>

	<Transition>
		<Event Type="SELECT_PLAYER"/>
		<FromState Type="LOADING_DATA" Name="loader"/>
		<ToState Type="SELECTING_PLAYER" Name="selecting"/>
	</Transition>

	<Transition>
		<Event Type="LOAD_DATA"/>
		<FromState Type="START" Name="start"/>
		<ToState Type="LOADING_DATA" Name="loader"/>
	</Transition>

	<StartState Name="start" />
</TransitionList>
AND modify the function above to look like this:

void SomeFunction(const std::string& xmlfile)
{
   // make a StateManager
   dtABC::StateManager* gm = new dtABC::StateManager();

   // register concrete States
   RegisterKnownStates(gm);

   // register concrete Events
   RegisterKnownEvents(gm);

   // define possible State transitions via XML
   gm->Load<Game::EventTypes,Game::StateTypes>( xmlfile );
}
Notice how I changed this function to require a string which specifies a the XML file found on your computer, and I am also telling the StateManager to "load" that file. The load function requires some more help to bind the XML Events and States to real class types, and that is why you must supply the class types as template parameters in this call. You now have completed the logical description, and will have only one step remaining to use a StateManager.

In the Delta3D engine, a class must derive from dtCore::Base in order to broadcast a message to other classes. For this example, I wanted a class to be able to generate an Event causing a transition, so it needed to derive from dtCore::Base and subscribe the StateManager to it. The basic steps involved with broadcasting a message from one class instance to other class instances of interest, is to:

  • Add a message 'sender' class instance
  • Actually send the message.
I'll define a class here:

#ifndef _APP_STATE_WALKER_INC_
#define _APP_STATE_WALKER_INC_

#include <dtCore/base.h>       // for base class
#include "dtABC/StateManager.h"  // for member

namespace example
{
   class StateWalker : public dtCore::Base
   {
   public:
      typedef dtCore::Base BaseClass;

      StateWalker(dtABC::StateManager* gm);
      ~StateWalker() {}

      void OnMessage(dtCore::Base::MessageData* msg);

      void DisplayEventChoicesAndWaitForInput();

   protected:
      void DisplayExtraEventChoices(unsigned int index);
      void HandleExtraEventChoices(unsigned int index_size, unsigned int choice);

   private:
      dtABC::StateManager* mStateManager;
   };
};

#endif // _APP_STATE_WALKER_INC_

Notice how it derives from dtCore::Base. This is necessary for sending messages to other classes.
Here is the constructor's implementation:

StateWalker::StateWalker(dtABC::StateManager* gm) : BaseClass(), mStateManager(gm)
{
   if( mStateManager )
      mStateManager->AddSender( this );

   AddSender( dtCore::System::Instance() );
}
Notice how it adds itself as a message "sender" to the StateManager. This must happen for the StateManager to receive messages from my class.

As for sending messages, well, you can send many different kinds of messages. The StateManager is extremely interested in "event" messages. Here is how the StateWalker class has been implemented to send messages to its subscribers. Given an Event::Type*, eventtype:


      dtCore::RefPtr<dtABC::StateManager::EventFactory> ef = mStateManager->GetEventFactory();
      if( ef->IsTypeSupported( eventtype ) )
      {
         dtCore::RefPtr<dtABC::Event> event = ef->CreateObject( eventtype );
         SendMessage("event", event.get() );
      }
      else
      {  // some logging feedback
         std::cout << "StateWalker: Can not create Event of type: " << eventtype->GetName() << std::endl;
      }
Finally, our message has been sent, which will possibly cause a State transition, but it is the job of the StateManager to determine if that occurs.

That is it! You have seen all the code necessary for defining transitions and sending Events, which possibly trigger State transitions. The code below shows the semantics necessary for defining an Event, a State, and their Types. Please check out the API for the Event and State classes.

Consider the code below as an appendix to the lesson.


The derived Event::Types (header):

#ifndef _EVENT_TYPES_INC_
#define _EVENT_TYPES_INC_

#include <dtABC/event.h>  // for base class
#include <string>         // for ctor parameter

namespace Game
{
   class EventTypes : public dtABC::Event::Type
   {
      DECLARE_ENUM(EventTypes); 
   public:
      typedef dtABC::Event::Type BaseClass;
      EventTypes(const std::string& name);

      static const EventTypes LOAD_DATA;
      static const EventTypes SELECT_PLAYER;
      static const EventTypes PLAY;

   protected:
      virtual ~EventTypes();

   private:
      EventTypes();  /// not implemented by design
   };
};

#endif  // _EVENT_TYPES_INC_
The derived Event::Types (implementation):

#include "../Events/Types.h"

using namespace Game;

IMPLEMENT_ENUM(EventTypes);

EventTypes::EventTypes(const std::string& name) : BaseClass(name)
{
   AddInstance(this); 
}

EventTypes::~EventTypes() {}

const EventTypes EventTypes::LOAD_DATA("LOAD_DATA");
const EventTypes EventTypes::SELECT_PLAYER("SELECT_PLAYER");
const EventTypes EventTypes::PLAY("PLAY");


The derived State::Types (header):

#ifndef _GAME_STATE_TYPES_INC_
#define _GAME_STATE_TYPES_INC_

#include <dtABC/state.h>  // for base class
#include <string>         // for ctor parameter

namespace Game
{
   /** \brief A class which defines the possible Game-state "types".
     */
   class StateTypes : public dtABC::State::Type
   {
      DECLARE_ENUM(StateTypes);
   public:
      typedef dtABC::State::Type BaseClass;
      StateTypes(const std::string& id);

      static const StateTypes START;
      static const StateTypes LOADING_DATA;
      static const StateTypes SELECTING_PLAYER;
      static const StateTypes PLAYING;

   private:
      StateTypes();  /// not implemented by design
   };
};

#endif  // _GAME_STATE_TYPES_INC_
The derived State::Types (implementation):

#include "../States/Types.h"

using namespace Game;

IMPLEMENT_ENUM(StateTypes);

StateTypes::StateTypes(const std::string& id) : BaseClass(id)
{
   AddInstance(this); 
}

const StateTypes StateTypes::LOADING_DATA("LOADING_DATA");
const StateTypes StateTypes::SELECTING_PLAYER("SELECTING_PLAYER");
const StateTypes StateTypes::PLAYING("PLAYING");
const StateTypes StateTypes::START("START");

An example Event (header):

#ifndef _GAME_EVENT_LOAD_DATA_INC_
#define _GAME_EVENT_LOAD_DATA_INC_

#include <dtABC/event.h>  // for base class

namespace Game
{
   class LoadData : public dtABC::Event
   {
   public:
      typedef dtABC::Event BaseClass;
      LoadData();

   protected:
      virtual ~LoadData();
   };
};

#endif // _GAME_EVENT_LOAD_DATA_INC_
An example Event (implementation):

#include "../Events/Types.h"
#include "../Events/LoadData.h"

using namespace Game;

LoadData::LoadData() : BaseClass(&EventTypes::LOAD_DATA) {}
LoadData::~LoadData() {}


An example State (header):

#ifndef _PLAYING_INC_
#define _PLAYING_INC_

#include <dtABC/state.h>  // for base class
#include <string>         // for ctor parameter

namespace Game
{
   class Playing : public dtABC::State
   {
   public:
      typedef dtABC::State BaseClass;
      Playing(const std::string& name="PlayingInstance");

      virtual void HandleEvent( dtABC::Event* event );

   protected:
      virtual ~Playing();

   private:
      //Playing(); /// not implemented by design
   };
};

#endif  // _PLAYING_INC_
An example State (implementation):

#include "../States/Types.h"
#include "../States/Playing.h"

using namespace Game;

Playing::Playing(const std::string& name) : BaseClass(&StateTypes::PLAYING,name) {}
Playing::~Playing() {}

void Playing::HandleEvent(dtABC::Event* event) {}

The example message sender class's full implementation:

#include "StateWalker.h"
#include <iostream>

using namespace example;

StateWalker::StateWalker(dtABC::StateManager* gm) : BaseClass(), mStateManager(gm)
{
   if( mStateManager )
      mStateManager->AddSender( this );

   AddSender( dtCore::System::Instance() );
}

void StateWalker::OnMessage(dtCore::Base::MessageData* msg)
{
   if( msg->message == "preframe" )
   {
      DisplayEventChoicesAndWaitForInput();
   }
}

void StateWalker::DisplayEventChoicesAndWaitForInput()
{
   std::cout << "For the State, " << mStateManager->Current()->GetName() << ", 
                 the Event choices are:" << std::endl;

   unsigned int mSize = mStateManager->GetNumOfEvents( mStateManager->Current() );
   std::vector<const dtABC::Event::Type*> eventvec(mSize);
   mStateManager->GetEvents( mStateManager->Current(), eventvec );

   // display the Event choices for those transitions
   unsigned int i(0);
   for(; i<mStateManager->GetNumOfEvents( mStateManager->Current() ); i++)
   {
      std::cout << i << ": " << eventvec[i]->GetName() << std::endl;
   }

   // print "extra" event choices
   DisplayExtraEventChoices(i);

   // --------------------------- handle input --------------------------- //
   std::cout << "Please choose an Event:";

   unsigned int eventchoice;
   std::cin >> eventchoice;
   std::cout << "Your choice was: " << eventchoice << std::endl;

   if( eventchoice < mSize )
   {
      const dtABC::Event::Type* eventtype = eventvec[eventchoice];
      std::cout << "StateWalker: Sending Event of type: " << eventtype->GetName() << std::endl;
      dtCore::RefPtr<GameStateManager::EventFactory> ef = mStateManager->GetEventFactory();
      if( ef->IsTypeSupported( eventtype ) )
      {
         dtCore::RefPtr<dtABC::Event> event = ef->CreateObject( eventtype );
         SendMessage("event", event.get() );
      }
      else
      {  // some logging feedback
         std::cout << "StateWalker: Can not create Event of type: "
                   << eventtype->GetName() << std::endl;
      }
   }

   else
   {  // honor the "extra" event choices
      HandleExtraEventChoices(mSize,eventchoice);
   }
}

// --- "extra" choices --- //
void StateWalker::DisplayExtraEventChoices(unsigned int index)
{
   std::cout << index << ": " << "Quit" << std::endl;
}

void StateWalker::HandleExtraEventChoices(unsigned int eventvecsize, unsigned int choice)
{
   if( choice == eventvecsize )
   {
      std::cout << "StateWalker: Quit was chosen." << std::endl;

      ///\todo replace ::GetSystem() with ::Instance()
      dtCore::System::Instance()->Stop();
   }
}

The main that brings all of this together:

/** \author John K. Grant
  * \date August 17, 2005
  * \file main.cpp
  * \brief An example for using dtABC::StateManager
  * This example shows how to run an application via logic defined in an external file.
  */

// states
#include "States/Types.h"
#include "States/Start.h"
#include "States/LoadingData.h"
#include "States/SelectingPlayer.h"
#include "States/Playing.h"

// events
#include "Events/Types.h"
#include "Events/LoadData.h"
#include "Events/SelectPlayer.h"
#include "Events/Play.h"

// state manager
#include <dtABC/StateManager.h>

// the game
#include "StateWalker.h"


/// Print the proper usage of this program to the shell for user feedback.
void PrintUsage(const std::string& exe_name)
{
   std::cout << "Proper Usage:" << std::endl << exe_name << 
                " <transition_file.xml>" << std::endl;
}

// Registers concrete States
void RegisterKnownStates(dtABC::StateManager* gm)
{
   gm->RegisterState<Game::Start>( &Game::StateTypes::START );
   gm->RegisterState<Game::LoadingData>( &Game::StateTypes::LOADING_DATA );
   gm->RegisterState<Game::SelectingPlayer>( &Game::StateTypes::SELECTING_PLAYER );
   gm->RegisterState<Game::Playing>( &Game::StateTypes::PLAYING );
}

// Registers concrete Events
void RegisterKnownEvents(dtABC::StateManager* gm)
{
   gm->RegisterEvent<Game::LoadData>( &Game::EventTypes::LOAD_DATA );
   gm->RegisterEvent<Game::SelectPlayer>( &Game::EventTypes::SELECT_PLAYER );
   gm->RegisterEvent<Game::Play>( &Game::EventTypes::PLAY );
}

int main(unsigned int argc, char* argv[])
{
   if( argc<2 ) // no input file was provided
   {
      PrintUsage(argv[0]);
      return 0;
   }

   // make a StateManager
   dtCore::RefPtr<dtABC::StateManager> gm = new dtABC::StateManager();

   // register concrete States
   RegisterKnownStates(gm);

   // register concrete Events
   RegisterKnownEvents(gm);

   // define possible State transitions via XML
   gm->Load<Game::EventTypes,Game::StateTypes>( argv[1] );

   // display what was loaded
   gm->PrintTransitions();
   gm->PrintStates();

   // create a "game"
   dtCore::RefPtr<example::StateWalker> app = new example::StateWalker(gm);

   // advance the game by running the System
   dtCore::System::Instance()->Config();
   dtCore::System::Instance()->Run();

   return 1;
}

Note : Code for this example can be found at "delta3d/examples/testStateManager"

Trackback

Trackback URL for this entry: http://delta3dengine.org/trackback.php?id=20050816182719149

No trackback comments for this entry.

About delta3d

delta3d is a game and simulation engine appropriate for a wide variety of simulation and entertainment applications. delta3d uses best-of-breed open source technologies to create a fully integrated game engine and with content creation tools.MORE

Twitter

User Functions





Don't have an account yet? Sign up as a New User!

Lost your password?